【云岚到家】-day09-1-项目迁移6-秒杀抢购介绍
【云岚到家】-day09-1-项目迁移6-秒杀抢购介绍
- 1 购物车
- 1.1 交互流程
- 1.2 Redis数据结构
- 1.3 表设计
- 1.4 定时同步购物车
- 2 签到活动
- 2.1 需求分析
- 2.2 系统设计
- 2.2.1 表设计
- 2.2.2 接口设计
- 2.2.3 如何计算连续签到天数
- 2.2.4 如何获取匹配签到奖励规则
- 2.2.5 签到代码
- 3 秒杀抢购介绍
- 1)秒杀抢购业务特点
- 2)常用技术方案
- 4 抢券
- 4.1 需求分析
- 1)抢券界面
- 2)进行抢券
- 3)我的优惠券列表
- 4.2 活动查询
- 4.2.1 系统设计
- 1)活动查询分析
- 2)数据流
- 3)Redis数据结构设计
- 4)如何保证活动状态自动改变
- 5)小结
- 4.2.2 定时预热程序
- 1)搭建环境
- 2)明确需求
- 3)编写Service方法
- 4)定义XXL-Job调度方法
- 5)配置定时任务
- 6)预热程序测试
- 4.2.3 活动查询接口
- 1)接口分析
- 2)接口定义
- 3)接口实现
- 4)活动查询测试
1 购物车
我负责的购物车模块,提供查询购物车、添加购物车、删除购物车、设置商品数量等接口。用户将商品添加到购物车,为了提高效率首先在 Redis 的 hash 结构记录用户的购物车信息,hashkey 是商品的 id,hashvalue 是商品数量。用户提交订单后会将购物车中的记录删除,当用户在一个小时内不再操作购物车会由一个定时任务将 redis 中存储的购物车信息同步到 MySQL 进行持久化,持久化完成后删除该用户在 redis 的购物车信息。
我们在 redis 设计了一个 ZSet 结构记录操作购物车的用户 id,当有用户操作购物车会在 ZSet 中存储用户 id,value 为用户 id,score 是操作购物车的时间戳,定时任务会每隔一个小时以内一个小时以外操作购物车的用户 id,从而拿到一个小时内不再操作购物车的用户,用用户 id 找到用户在 redis 的购物车数据,同步到 MySQL 同时删除 redis 中购物车的记录。
1.1 交互流程
1.2 Redis数据结构
-
购物车缓存(Hash)
对象id可以是商品id/商品sku id,服务id,课程id等
商品信息缓存(String):key:item:商品id,value:商品信息(json)
购物车变化记录(ZSet):记录所有购物车的变化的用户
key:SHOPPING_ZSET、value:userid、score:购物车变化时间
购物车的缓存方案
1、购物车缓存结构(hash)
hashkey:商品 id
hashvalue: 数量
2、购物车的变化记录(ZSet)
当用户向购物车添加记录或修改记录
向ZSet写入记录
value:用户id
score:操作时间戳
缓存过期时间:
1、购物车缓存结构(hash):24小时
2、购物车的变化记录(ZSet):永不过期
缓存同步:
定时将redis中的购物车记录进行持久化数据库,可以将redis中的购物车删除了
每隔一段时间(一天、两小时),扫描ZSet一段时间内没有变动的记录,将其从redis删掉
1.3 表设计
购物车表:
CREATE TABLE `shopping_car` (`id` bigint NOT NULL COMMENT '商品id/商品skuid/服务id',`user_id` bigint NOT NULL COMMENT '用户id',`pur_num` int NOT NULL COMMENT '下单数量',`price` decimal(10, 2) NOT NULL DEFAULT 0 COMMENT '加入购物车的价格',`add_shopping_car_time` datetime NOT NULL COMMENT '加入购物车时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='购物车';
1.4 定时同步购物车
定时任务每天凌晨将购物车的数据从radios同步到mysql
先查询Zset,根据score分页查询今天变化的用户,找到用户ID则从购物车缓存查询购物车的信息写入master的购物车表
2 签到活动
2.1 需求分析
每日签到活动是一种常见的用户参与活动,通常通过用户在每天登录或使用应用时进行签到,以获得奖励或积分。以下是每日签到活动的一些常见业务需求:
-
用户奖励机制:
确定每日签到可以获得的奖励,这可以包括虚拟货币、积分、优惠券、折扣码、实物奖品等。奖励机制要具有吸引力,以激发用户的参与积极性。
-
连续签到奖励:
考虑设置连续签到奖励,即用户在多天内持续签到可获得额外的奖励。这可以激发用户保持长期参与。
-
签到排行榜:
如果适用,可以设置签到排行榜,展示用户签到次数的排名,为用户提供一种竞争和展示的机会。
用户端签到的界面原型如下:
2.2 系统设计
2.2.1 表设计
表清单 如下:
表名称 | 表名 | 存储内容 |
---|---|---|
签到规则表 | sign_rule | 存放签到规则的表 |
签到记录表 | sign_record | 每次签到记录表,记录签到以及获得的积分和奖励 |
表sql:
//签到记录表
CREATE TABLE `sign_record` (`id` bigint NOT NULL COMMENT '签到id',`user_id` bigint NOT NULL COMMENT '用户id',`sign_time` datetime NOT NULL COMMENT '签到时间',`integral` int NOT NULL DEFAULT '0' COMMENT '积分',`growth_value` int NOT NULL DEFAULT '0' COMMENT '成长值',`sign_day_num` int NOT NULL COMMENT '累计连续签到天数,包含本次签到',`sign_day` int NOT NULL COMMENT '签到日,格式yyyymmdd',PRIMARY KEY (`id`),UNIQUE KEY `index_sign_unique` (`user_id`,`sign_day`) COMMENT '签到唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
// 签到规则
CREATE TABLE `sign_rule` (`id` bigint NOT NULL COMMENT '规则id',`sign_day_num` int NOT NULL COMMENT '连续签到天数',`integral` int DEFAULT '0' COMMENT '积分',`growth_value` int DEFAULT NULL COMMENT '成长值',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`create_by` bigint DEFAULT NULL COMMENT '创建人id',`update_by` bigint DEFAULT NULL COMMENT '更新人id',`is_deleted` bit(1) DEFAULT NULL COMMENT '逻辑删除',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='签到规则';
2.2.2 接口设计
接口清单:
签到规则列表接口、签到规则新增接口、签到规则修改接口、签到规则删除接口、签到接口
签到规则列表接口:
接口名称:签到规则列表接口
接口场景:签到管理中规则列表
接口路径:GET /sign/rule
请求参数:无
响应内容:内容列表信息,json格式,示例如下:
{"code": "200","msg": "OK","result": [{"id":"string,签到规则","signDayNum":"int,连续签到天数","integral":"int,签到获得的积分数","growthValue":"int,签到获得的成长值"}]
}
签到规则新增接口:
接口名称:签到规则新增接口
接口场景:签到管理中规则新增
接口路径:POST /sign/rule
请求参数:json格式,示例如下:
{"signDayNum":"int,连续签到天数","integral":"int,签到获得的积分数","growthValue":"int,签到获得的成长值"
}
响应内容:内容列表信息,json格式,示例如下:
{"code": "200","msg": "OK"
}
签到规则修改接口:
接口名称:签到规则修改接口
接口场景:签到管理中规则修改
接口路径:PUT /sign/rule/{id}
请求参数:
路径参数:id,签到规则id
body请求参数,json格式,示例如下:
{"signDayNum":"int,连续签到天数","integral":"int,签到获得的积分数","growthValue":"int,签到获得的成长值"
}
响应内容:内容列表信息,json格式,示例如下:
{"code": "200","msg": "OK"
}
签到规则删除接口:
接口名称:签到规则删除接口
接口场景:签到管理中规则删除
接口路径:PUT /sign/rule/{id}
请求参数:
路径参数:id,签到规则id
响应内容:内容列表信息,json格式,示例如下:
{"code": "200","msg": "OK"
}
签到接口:
接口名称:签到接口
接口场景:用于c端用户的签到、会员签到场景
接口路径:POST /sign
请求参数:无
响应内容:内容列表信息,json格式,示例如下:
{"code": "200","msg": "OK"
}
2.2.3 如何计算连续签到天数
通过签到记录中保存连续签到天数来完成连续签到天数的计算。先获取昨天的签到记录,如果有记录,在记录的连续签到天数上+1就是本次连续签到的天数;没有没有该记录,当前连续签到天数为1。
该方式有一个前提,一天只能签到一次,为了保证每天的记录唯一,需要创建用户id和签到日的唯一的组合索引。
优点:
1.不需要借助定时任务来完成连续签到天数计算
2.中间有一天未签到,自动重新计算连续签到
3.借助mysql数据库的唯一索引完成签到次数的限制,减少了分布式锁的使用
2.2.4 如何获取匹配签到奖励规则
获取签到规则时查询条件:签到天数小于等于连续签到天数(程序中计算出来的),按照签到天数从大到小排序,并限制查询1条数据。
举个例子:
例子1: 查询3天的签到奖励规则
签到条数小于等于3,并按照从大到小顺序:3,2,1;获取第1条数据得到的是3,结果是正确的
例子2:查询10天的签到奖励规则
签到条数小于等于10,并按照从大到小顺序:8,7,6,5,4,3,2,1;获取第一条数据得到8,结果是正确
2.2.5 签到代码
public void sign() {Long userId = UserContext.currentUserId();// 校验是否已经签到int signDayOfToday = DateUtils.getIntFormatDate(DateUtils.now(), "yyyyMMdd");LambdaQueryWrapper<SignRecord> signRecordLambdaQueryWrapper = new LambdaQueryWrapper<>();signRecordLambdaQueryWrapper.eq(SignRecord::getUserId, userId).eq(SignRecord::getSignDay, signDayOfToday);SignRecord signRecord = signRecordMapper.selectOne(signRecordLambdaQueryWrapper);if( signRecord != null) {throw new CommonException("已经签到,请勿重新签到");}// 计算连续签到天数int signDayOfYesterday = DateUtils.getIntFormatDate(DateUtils.now(), "yyyyMMdd");signRecordLambdaQueryWrapper = new LambdaQueryWrapper<>();signRecordLambdaQueryWrapper.eq(SignRecord::getUserId, userId).eq(SignRecord::getSignDay, signDayOfYesterday);signRecord = signRecordMapper.selectOne(signRecordLambdaQueryWrapper);// 默认连续签到1天int signDayNum = 1;if(signRecord != null) {// 累计签到天数+1signDayNum = signRecord.getSignDayNum() + 1;}// 获取签到规则LambdaQueryWrapper<SignRule> signRuleLambdaQueryWrapper = new LambdaQueryWrapper<>();// 准则数据signRuleLambdaQueryWrapper.le(SignRule::getSignDayNum, signDayNum).orderByDesc(true, SignRule::getSignDayNum).last("limit 1");SignRule signRule = signRuleMapper.selectOne(signRuleLambdaQueryWrapper);SignRecord signRecordToday = new SignRecord();if(signRule != null) {//签到积分signRecordToday.setIntegral(NumberUtils.null2Zero(signRule.getIntegral()));// 签到成长值signRecordToday.setGrowthValue(NumberUtils.null2Zero(signRule.getGrowthValue()));}//保存签到记录signRecordToday.setSignTime(DateUtils.now());signRecordToday.setSignDay(signDayOfToday);signRecordToday.setUserId(userId);signRecordToday.setSignDayNum(signDayNum);int addResult = signRecordMapper.insert(signRecordToday);if(addResult != 1) {throw new CommonException("签到异常");}// 将签到积分和成长值写入用户账户表// 此处调用用户中心接口为用户添加积分和成长值
}
3 秒杀抢购介绍
1)秒杀抢购业务特点
秒杀抢购是电商平台的一种业务模式,它可以聚焦流量和吸引注意力,通过秒杀活动长时间吸引人群,最终吸引用户下单。
秒杀抢购业务有哪些特点呢?
时间限制: 秒杀抢购活动通常在特定的时间段内进行,例如1小时或更短的时间。在这个时间段内,消费者可以购买特定商品或服务,通常是限量销售。
限量销售: 秒杀抢购商品通常数量有限,销售数量是提前确定的。一旦库存售罄,抢购活动就结束,未能购买的消费者需要等待下一次活动。
价格优惠: 秒杀抢购商品通常以折扣价格销售,价格较平时低廉。这种价格优势是吸引消费者参与抢购的重要因素。
高并发和服务器压力: 抢购开始时大量用户会同时访问在线商城,导致网站服务器承受巨大压力。因此,网站的服务器和网络基础设施需要具备高并发处理能力,以应对瞬时大量的用户请求。
技术要求高: 秒杀抢购业务对技术要求非常高,包括网站性能优化、数据库优化、缓存技术、负载均衡等方面的技术应用。
如今秒杀抢购不仅应用在电商平台,随着互联网用户的增加在餐饮、旅游、娱乐等很多领域都有应用,比如:12306抢票、抢优惠券、骑手抢单、酒店预定等。
2)常用技术方案
实现秒杀抢购业务会用到哪些技术呢?可以参考行业上一些成熟的解决方案。
- 缓存方案
使用缓存技术(如Redis)来存储热点数据,例如商品信息和库存数量。这样可以减轻数据库的压力,提高读取数据的速度。
- 异步处理方案
当用户成功秒杀后,将抢购信息发送到队列,然后由消费者多线程异步处理订单,减轻系统的实时压力,使用Redis、RabbitMQ等技术都可以实现队列。
- 防止超卖方案
超卖是最终下单购买数量大于库存数量,比如:库存100个用户最终购买了101个,多出这一个就是超卖了,在秒杀抢购业务中这也是需要解决的问题,可以使用分布式锁、Redis等技术都可以防止超卖。
- 限流与防刷方案
使用限流算法(如令牌桶、漏桶算法)来控制请求的并发数,防止服务器被过多请求压垮。可以在服务端使用限流技术,比如:sentinel、nginx、验证码等技术。
- 数据库优化方案
对数据库进行优化,包括索引的设计、SQL语句的优化、数据库连接池的使用等,以提高数据库的查询和更新速度。
- 数据库分库分表方案
在数据库层面进行分库分表,将数据分散存储在不同的数据库实例或表中,提高数据库的读写性能。
- 负载均衡
使用负载均衡技术,例如Nginx、Spring Cloud Gateway等,将请求分发到多个服务器上,增加系统的处理能力。
- CDN加速
CDN(Content DeliveryNetwork)即内容分发网络,CDN用于加速静态资源的访问,将内容分发到CDN节点就近为客户提供服务。
如下图所示,通过CDN用户访问就近节点,提高访问速度。
- 安全性处理
确保系统的安全性,防止SQL注入、XSS攻击(跨站脚本攻击)等,同时在后端实现防刷、验证码等安全措施,保护系统免受恶意攻击。
4 抢券
4.1 需求分析
用户进行抢券经过三个过程:
1)抢券界面
用户进入抢券界面如下图。
“疯抢中”界面展示进行中还未到结束的优惠券活动,按照开始时间升序排列,不进行分页。
“即将开始”界面是展示1个月内待发放的优惠券活动,按照开始时间升序排列,不进行分页。
在抢券界面显示如下信息:
1、优惠券活动名称
2、优惠券满减或折扣信息
3、活动的起止时间
4、是否抢光。
5、活动的状态。
用户点击立即领取进行抢券,以下情况无法抢券:
1、已抢光
“已抢光”表示发放的优惠券已领取完,该优惠券的领取数量等于发放数量。
2、未到开始时间或已到结束时间
3、用户已领(每个用户限领一张)
4、活动已撤销
2)进行抢券
优惠券到达发放时间后,用户点击“立即领取”进行抢券。
抢券成功提示“领取成功”,如果领取失败会提示失败信息。
3)我的优惠券列表
用户抢券成功在我的优惠券列表查看。
用户进入【我的】-【优惠券】查看已抢到的优惠券,按抢券时间降序显示当前用户抢到的优惠券。
本查询为滚动查询,向上拖屏幕查询下一屏,一屏显示10条。
如下图:
对优惠券的三个状态说明如下:
未使用:未过有效期的优惠券。
优惠券的有效期:从领取优惠券的时间加上优惠券的使用期限(“使用期限”在优惠券活动管理界面进行设置)。
已使用:已经在订单中使用的优惠券。
已过期:未使用且已过有效期的优惠券,已过期的优惠券将无法使用。
4.2 活动查询
4.2.1 系统设计
1)活动查询分析
下边根据需求分析对抢券界面的活动查询功能进行分析与设计,如下图:
- 活动查询界面显示了哪些数据?
活动信息:
包括两个部分数据:
- 进行中还未到结束的优惠券活动。
- 1个月内待开始的优惠券活动。
信息内容包括:
优惠券活动名称
优惠券满减或折扣信息
活动的起止时间
是否抢光(根据库存剩余量判断)
活动的状态。
- 面向高并发如何提高活动查询性能?
此部分信息来源于优惠券活动表,由于抢券页面面向C端用户且请求并发量大,如何在高并发下提高活动查询的性能呢?
如果直接查询数据库无法满足需求并且对数据库造成巨大的压力从而影响其它功能使用数据库,我们可以使用缓存,将优惠券活动信息存入缓存,比如Redis,从Redis查询避免查询数据库。
- 如何保证缓存一致性?
通过定时预热程序保证缓存一致性,抢券页面列出的活动信息属于热点信息,对于热点信息通过定时预热防止缓存击穿,定时预热程序通过定时任务定时执行,定时将活动信息存入Redis。
2)数据流
根据以上分析设计数据流如下:
活动管理:运营人员进行优惠券活动管理,对活动表进行增删改查操作。
活动状态更新任务:根据活动的开始和结束时间更新活动状态。
活动预热任务:定时查询活动表信息存入Redis
抢券查询:从Redis查询活动预热信息。
3)Redis数据结构设计
活动预热任务定时查询活动表信息存入Redis,下边设计活动信息的缓存结构:
根据需求可知在抢券查询界面需要的数据全部来源于活动表,因为要预热的活动信息内容有限,我们可以将要预热的活动信息转为json串存入redis,活动查询程序读取json串也方便进行解析。
活动信息缓存结构设计如下:
redis结构:String类型
key: “ACTIVITY:LIST”
value: 符合条件的优惠券活动列表JSON数据。
过期时间:永不过期
缓存一致性方案:通过预热程序保证缓存一致性
4)如何保证活动状态自动改变
在活动查询界面如何保证活动状态实时改变,当到达活动开始时间活动状态变为“进行中”,在“疯抢中”界面显示,当活动结束在“疯抢中”界面将无法查询到。
关于活动状态的改变,在“优惠券活动管理实战”中完成了通过定时任务自动更新活动的状态,实现了如下需求:
1)对待生效的活动更新为进行中
到达发放开始时间状态改为“进行中”。
2)对待生效及进行中的活动更新为已失效
到达发放结束时间状态改为“已失效”
具体代码如下:
Service接口实现
@Override
public void updateStatus() {LocalDateTime now = DateUtils.now();// 1.更新已经进行中的状态lambdaUpdate().set(Activity::getStatus, ActivityStatusEnum.DISTRIBUTING.getStatus())//更新活动状态为进行中.eq(Activity::getStatus, NO_DISTRIBUTE.getStatus())//检索待生效的活动.le(Activity::getDistributeStartTime, now)//活动开始时间小于等于当前时间.gt(Activity::getDistributeEndTime,now)//活动结束时间大于当前时间.update();// 2.更新已经结束的lambdaUpdate().set(Activity::getStatus, LOSE_EFFICACY.getStatus())//更新活动状态为已失效.in(Activity::getStatus, Arrays.asList(DISTRIBUTING.getStatus(), NO_DISTRIBUTE.getStatus()))//检索待生效及进行中的活动.lt(Activity::getDistributeEndTime, now)//活动结束时间小于当前时间.update();
}
定时任务方法:
@Component
public class XxlJobHandler {/*** 活动状态修改,* 1.活动进行中状态修改* 2.活动已失效状态修改* 1分钟一次*/
@XxlJob("updateActivityStatus")
public void updateActivitySatus(){log.info("定时修改活动状态...");try {activityService.updateStatus();} catch (Exception e) {e.printStackTrace();}
}
...
通过上边的定时任务无法实现状态的实时改变,每分钟执行一次状态变更理论上存在最多1分钟的状态变更延迟。
如何实现在页面到达活动开始时间立即变更活动状态?
- 在前端进行控制,根据活动开始时间进行倒计时,达到开始时间将活动移到进行中界面。
- 请求后端查询数据,根据当前时间和活动开始、活动结束时间判断活动的状态。
当活动开始时间小于等于当前时间并且结束时间大于当前时间说明活动已经开始并且还没有结束,活动状态为进行中。
当活动结束时间小于当前时间说明活动结束,活动状态为失效。
5)小结
抢券查询的缓存是怎么做的?
如何实现活动状态的自动改变?
- 通过定时任务每几分钟更新活动状态
对于待生效的活动,当活动开始时间小于等于当前时间并且结束时间大于当前时间,将活动状态更新为进行中。
对于待生效和进行中的活动,当活动结束时间小于当前时间,将活动状态更新为失效。
如何实现状态变化的实时性,当到达活动开始时间状态立即变化:
两种方法结合:
- 前端请求后端接口查询活动信息,后端根据活动时间(开始、结束时间)及当前时间得到活动当前准确的状态,并将状态返回给前端。
- 前端进行控制,根据活动开始时间进行倒计时,达到开始时间将活动移到进行中界面。
4.2.2 定时预热程序
1)搭建环境
进入jzo2o-market工程,创建dev_02分支,并切换到该分支。
从课程资料中获取源码:jzo2o-market-01-1.zip,此源码是优惠券活动管理实战后的代码。
解压jzo2o-market-01-1.zip,将其代码拷贝到dev_02分支下。
提交dev_02分支代码。
导入jzo2o-market-01-1
2)明确需求
根据需求预热的活动信息包括两个部分:
进行中还未到结束的优惠券活动。
1个月内待开始的优惠券活动。
对应sql语句
SELECT *
FROM activity t
WHERE t.distribute_start_time <= DATE_ADD(NOW(), INTERVAL 30 DAY)AND t.status IN (1, 2)
ORDER BY t.distribute_start_time ASC;
3)编写Service方法
下边定义Service接口从数据库查询要预热的活动信息,存入redis。
在com.jzo2o.market.service.IActivityService中
代码如下:
/*** 活动预热*/
void preHeat();
实现
@Override
public void preHeat() {/***SELECT *FROM activity tWHERE t.distribute_start_time <= DATE_ADD(NOW(), INTERVAL 30 DAY) AND t.status IN (1, 2)ORDER BY t.distribute_start_time ASC;*/// 1.查询准备LocalDateTime now = DateUtils.now();LocalDateTime preHeatTime = now.plusDays(30);LambdaQueryWrapper<Activity> lambdaQueryWrapper = new LambdaQueryWrapper<>();// 查询条件lambdaQueryWrapper.le(Activity::getDistributeStartTime, preHeatTime).in(Activity::getStatus, Arrays.asList(NO_DISTRIBUTE.getStatus(), DISTRIBUTING.getStatus())).orderByAsc(Activity::getDistributeStartTime);// 查询List<Activity> activities = baseMapper.selectList(lambdaQueryWrapper);if (CollUtils.isEmpty(activities)) {//防止缓存穿透activities = new ArrayList<>();}// 2.数据转换: 将List<Activity> 转为List<SeizeCouponInfoResDTO>List<SeizeCouponInfoResDTO> seizeCouponInfoResDTOS = BeanUtils.copyToList(activities, SeizeCouponInfoResDTO.class);// 3.再转为json字符串String json = JsonUtils.toJsonStr(seizeCouponInfoResDTOS);// 4.存入redis,操作string用opsForValue(),操作哈希用opsForHash()redisTemplate.opsForValue().set(ACTIVITY_CACHE_LIST, json);}
4)定义XXL-Job调度方法
下边定义定时任务方法,执行定时预热程序。每小时执行一次。
在com.jzo2o.market.handler.XxlJobHandler中
/*** 活动预热,整点预热**/
@XxlJob("activityPreheat")
public void activityPreHeat() {log.info("优惠券活动定时预热...");try {activityService.preHeat();} catch (Exception e) {e.printStackTrace();}
}
5)配置定时任务
下边在XXL-JOB中配置调度方法进行测试:
通常生产中会提前一周去策划活动并在系统中创建活动,所以在生产中每小时执行一次预热程序,开发中为了测试方便5秒执行一次。
6)预热程序测试
测试流程如下:
首先在运营端创建优惠券活动,创建一个月内即将开始的活动
创建一个码农洗澡大派送
这里还有我们以前的码农洗脚大派送,但是因为没有执行过定时任务,自然也没因为时间已经过期而更改状态
我们执行一下xxl-job的定时任务
再观察一下状态,预期结果,活动状态变更修改过期状态,活动定时预热将未来一个月的活动写进redis
活动状态变更修改过期状态已成功
查看redis
洗澡大派对已经缓存进redis中,测试成功
4.2.3 活动查询接口
1)接口分析
根据界面原型分析接口:
界面有两个tab,疯抢中和即将开始,前端传入后端一个参数标记是查询进行中的活动还是即将开始的活动。
后端需要给前端返回以下数据:
- 活动id
- 活动名称
- 优惠券类型,1:满减,2:折扣
- 满减限额,0:表示无门槛,其他值:最低消费金额
- 折扣率,折扣类型的折扣率,例如:8,打8折
- 优惠金额,满减或无门槛的优惠金额
- 发放开始时间
- 发放结束时间
- 活动状态,1:待生效,2:进行中,3:已失效
- 优惠券剩余数量
这些信息在预热的活动信息缓存中都存在,但是有两个字段不够实时:活动状态,优惠券剩余数量。
活动状态:通过定时任务更新活动状态,写入活动表,定时预热程序读出活动信息存储到Redis,由于是通过定时任务更新活动状态、定时预热程序更新活动信息缓存,最终从redis取出的活动状态于实际的活动状态存在延迟,导致实际活动的状态与页面显示的状态不一致。
示例:活动开始时间为8点钟,当前时间已过了8点钟,查看抢券页面活动状态仍然显示“即将开始”。
优惠券剩余数量:在抢券模块会操作此字段,这里暂不考虑此字段。
如何解决活动状态延迟问题?
请求后端查询数据,根据当前时间和活动开始、活动结束时间判断活动的状态。
当活动开始时间小于等于当前时间并且结束时间大于当前时间说明活动已经开始并且还没有结束,活动状态为进行中。
当活动结束时间小于当前时间说明活动结束,活动状态为失效。
2)接口定义
接口名称:用户端抢券列表分页接口
接口路径:GET/market/consumer/activity/list
请求数据类型 application/x-www-form-urlencoded
controller
@RestController("consumerActivityController")
@RequestMapping("/consumer/activity")
@Api(tags = "用户端-活动相关接口")
public class ActivityController {@Resourceprivate IActivityService activityService;@GetMapping("/list")@ApiOperation("用户端抢券列表分页接口")@ApiImplicitParams({@ApiImplicitParam(name = "tabType", value = "页面tab类型,1:疯抢中,2:即将开始", required = true, dataTypeClass = Integer.class)})public List<SeizeCouponInfoResDTO> queryForPage(@RequestParam(value = "tabType",required = true) Integer tabType) {return activityService.queryForListWithCache(tabType);}
}
3)接口实现
- service接口
/*** 用户端抢券列表分页查询活动信息** @param tabType 页面类型* @return*/
List<SeizeCouponInfoResDTO> queryForListWithCache(Integer tabType);
实现
@Override
public List<SeizeCouponInfoResDTO> queryForListWithCache(Integer tabType) {//从redis查询活动信息Object seizeCouponInfoStr = redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);if (ObjectUtils.isNull(seizeCouponInfoStr)) {return CollUtils.emptyList();}//将json转为ListList<SeizeCouponInfoResDTO> seizeCouponInfoResDTOS = JsonUtils.toList(seizeCouponInfoStr.toString(), SeizeCouponInfoResDTO.class);//根据tabType确定要查询的状态int queryStatus = tabType == TabTypeConstants.SEIZING ? DISTRIBUTING.getStatus() : NO_DISTRIBUTE.getStatus();//过滤数据,并设置剩余数量、实际状态List<SeizeCouponInfoResDTO> collect = seizeCouponInfoResDTOS.stream().filter(item -> queryStatus == getStatus(item.getDistributeStartTime(), item.getDistributeEndTime(), item.getStatus())).peek(item -> {//剩余数量item.setRemainNum(item.getStockNum());//状态item.setStatus(queryStatus);}).collect(Collectors.toList());return collect;
}/*** 获取状态,* 用于xxl或其他定时任务在高性能要求下无法做到实时状态** @return*/
private int getStatus(LocalDateTime distributeStartTime, LocalDateTime distributeEndTime, Integer status) {if (NO_DISTRIBUTE.equals(status) &&distributeStartTime.isBefore(DateUtils.now()) &&distributeEndTime.isAfter(DateUtils.now())) {//待生效状态,实际活动已开始return DISTRIBUTING.getStatus();}else if(NO_DISTRIBUTE.equals(status) &&distributeEndTime.isBefore(DateUtils.now())){//待生效状态,实际活动已结束return LOSE_EFFICACY.getStatus();}else if (DISTRIBUTING.equals(status) &&distributeEndTime.isBefore(DateUtils.now())) {//进行中状态,实际活动已结束return LOSE_EFFICACY.getStatus();}return status;
}
4)活动查询测试
测试流程如下:
启动网关
启动优惠券工程
参考预热程序测试的方法,创建活动信息,启动xxl-job,保证定时预热程序正常执行。
打开小程序,点击优惠券图标。
我手动把洗脚大派对的时间延后到11月1日,这样洗脚大派对是正在进行,而洗澡大派对是未生效
这是他俩对应库存,洗脚已经没有了,洗澡还有100张
打开小程序,符合预期测试成功