管理员上架秒杀商品的流程
1、去后台管理系统添加新的秒杀场次(注意这里不能把之前的秒杀场次给修改后再次使用,还有时间必须要三天内)
2、在秒杀服务中的定时任务修改一下每5s上架一次(这样是为了更快上架)
然后就可以看到上架的秒杀商品了
秒杀服务实现的流程
1、设置秒杀活动的订时任务(预热秒杀商品)
通过设置订时任务来自动上架最近三天需要秒杀的商品信息到redis缓存实现秒杀服务预热效果
订时任务具体实现
1、首先设置定时器类来定时自动上架最近三天需要秒杀的商品信息到redis缓存
采用了分布式锁来保证幂等性(其实就是加个分布式锁来确保多个秒杀服务同时在线时只有一个服务成功上架秒杀商品)
分布式系统中的幂等性概念:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
幂等场景
可能会发生重复请求或消费的场景,在微服务架构中是随处可见的。
网络波动:因网络波动,可能会引起重复请求
分布式消息消费:任务发布后,使用分布式消息服务来进行消费
用户重复操作:用户在使用产品时,可能无意地触发多笔交易,甚至没有响应而有意触发多笔交易
未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
package com.saodai.saodaimall.saodaimall.seckill.scheduled; import com.saodai.saodaimall.saodaimall.seckill.service.SeckillService; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * 秒杀商品定时上架 * 每天晚上3点,上架最近三天需要三天秒杀的商品 * 当天00:00:00 - 23:59:59 * 明天00:00:00 - 23:59:59 * 后天00:00:00 - 23:59:59 */ @Slf4j @Service public class SeckillScheduled @Autowired private SeckillService seckillService; @Autowired private RedissonClient redissonClient; //秒杀商品上架功能的锁 private final String upload_lock = "seckill:upload:lock"; /**保证幂等性问题**/ // @Scheduled(cron = "*/5 * * * * ? ") //秒 分 时 日 月 周 @Scheduled(cron = "0 0 1/1 * * ? ") public void uploadSeckillSkuLatest3Days() { //1、重复上架无需处理 log.info("上架秒杀的商品..."); //分布式锁 RLock lock = redissonClient.getLock(upload_lock); try { //加锁(指定锁定时间为10s) lock.lock(10, TimeUnit.SECONDS); seckillService.uploadSeckillSkuLatest3Days(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
2、在SeckillServiceImpl实现uploadSeckillSkuLatest3Days方法
实现uploadSeckillSkuLatest3Days方法的代码
@Autowired private StringRedisTemplate redisTemplate; @Autowired private CouponFeignService couponFeignService; @Autowired private ProductFeignService productFeignService; @Autowired private RedissonClient redissonClient; @Autowired private RabbitTemplate rabbitTemplate; private final String SESSION__CACHE_PREFIX = "seckill:sessions:"; private final String SECKILL_CHARE_PREFIX = "seckill:skus"; private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码 /** * 上架三天需要秒杀的商品到缓存里 */ @Override public void uploadSeckillSkuLatest3Days() { //1、扫描最近三天的商品需要参加秒杀的活动 R lates3DaySession = couponFeignService.getLates3DaySession(); if (lates3DaySession.getCode() == 0) { //获取远程调用查询到的秒杀活动 List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() { }); //缓存到Redis //1、封装秒杀活动信息 saveSessionInfos(sessionData); //2、封装秒杀活动的关联商品信息 saveSessionSkuInfo(sessionData); } } /** * 封装秒杀活动信息到缓存里 * @param sessions 秒杀活动信息 */ private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) { if (sessions!=null){ sessions.stream().forEach(session -> { //获取当前活动的开始和结束时间的时间戳 long startTime = session.getStartTime().getTime(); long endTime = session.getEndTime().getTime(); //存入到Redis中的key格式例如seckill:sessions:1648099200000_1648123200000 //seckill:sessions:是前缀,1648099200000表示秒杀活动开始的时间,1648123200000表示秒杀活动结束的时间 String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime; //判断Redis中是否有该信息,如果没有才进行添加 Boolean hasKey = redisTemplate.hasKey(key); //缓存活动信息 if (!hasKey) { //获取到活动中所有商品的skuId 格式例如:4-47 List<String> skuIds = session.getRelationSkus().stream() .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList()); //leftPushAll是批量存入缓存 /**格式是每一个缓存中秒杀活动的格式是key:seckill:sessions:1648099200000_1648123200000,value:4-47**/ redisTemplate.opsForList().leftPushAll(key,skuIds); } }); }else { log.error("没有秒杀活动"); } } /** * 封装秒杀活动的关联商品信息到缓存里 * @param sessions 秒杀活动信息 */ private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) { if (sessions!=null){ sessions.stream().forEach(session -> { //准备hash操作,绑定hash值seckill:skus BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //遍历秒杀活动中的商品项(seckillSkuVo表示的就是每个遍历的商品项) session.getRelationSkus().stream().forEach(seckillSkuVo -> { //生成随机码 String token = UUID.randomUUID().toString().replace("-", ""); //查看redis中有没有这个key (秒杀场次id_秒杀商品id) String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(); if (!operations.hasKey(redisKey)) { //缓存我们商品信息(SeckillSkuRedisTo是存入缓存中的对象) SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); Long skuId = seckillSkuVo.getSkuId(); //1、先查询sku的基本信息,调用远程服务 R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0) { SkuInfoVo skuInfo = info.getData( "skuInfo",new TypeReference<SkuInfoVo>(){}); redisTo.setSkuInfo(skuInfo); } //2、sku的秒杀信息 BeanUtils.copyProperties(seckillSkuVo,redisTo); //3、设置当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //4、设置商品的随机码(防止恶意攻击) redisTo.setRandomCode(token); //序列化json格式存入Redis中 String seckillValue = JSON.toJSONString(redisTo); //秒杀活动的商品项的详细信息存入redis /**格式是key:4_47 value:SeckillSkuRedisTo对象的String类型**/ operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue); //如果当前这个场次的商品库存信息已经上架就不需要上架 /**5、使用库存作为分布式Redisson信号量(限流)**/ //把每个秒杀商品的总数量作为信号量存入redis缓存,信号量标识seckill:stock:+随机(相当于key) RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); /**库存的格式key:seckill:stock:5d1df46618d34f9f9808f25cda60ba01 value:秒杀商品的总数量 其中5d1df46618d34f9f9808f25cda60ba01是随机码**/ semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); }else { log.error("没有秒杀活动"); } }
实现uploadSeckillSkuLatest3Days方法的总流程:
1.先远程调用优惠劵服务的SeckillSessionController控制器的getLates3DaySession方法来扫描最近三天的商品需要参加秒杀的活动(SeckillSessionWithSkusVo实体类其实就是下面的SeckillSessionEntity秒杀场次实体类)
/** * 上架三天需要秒杀的商品到缓存里 */ @Override public void uploadSeckillSkuLatest3Days() { //1、扫描最近三天的商品需要参加秒杀的活动 R lates3DaySession = couponFeignService.getLates3DaySession(); if (lates3DaySession.getCode() == 0) { //获取远程调用查询到的秒杀活动 List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() { }); //缓存到Redis //1、封装秒杀活动信息 saveSessionInfos(sessionData); //2、封装秒杀活动的关联商品信息 saveSessionSkuInfo(sessionData); } }
第一步的实现细节
远程调用Coupon服务的SeckillSessionController控制器代码块
/** * 查询最近三天需要参加秒杀商品的信息 * @return */ @GetMapping(value = "/Lates3DaySession") public R getLates3DaySession() { List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession(); return R.ok().setData(seckillSessionEntities); }
远程调用Coupon服务的SeckillSessionController控制器getLates3DaySession方法代码块
分流程:
先用LocalDate来拼装当前时间作为开始时间,三天后的时间作为结束时间,并且进行时间格式化
在gulimall_sms数据库sms_seckill_session表中查出这三天内的所有秒杀活动(秒杀活动的开始时间在这三天范围内的都符合要求)
封装秒杀活动中秒杀商品项的数据
通过秒杀场次的id来获取在gulimall_sms数据库中sms_seckill_sku_relation表中 所有这个秒杀场次的秒杀商品(其中要注意的是promotion_session_id就是sms_seckill_session的id,也就是秒杀场次的id)
/** * 查询最近三天需要参加秒杀商品的信息 * @return */ @Override public List<SeckillSessionEntity> getLates3DaySession() { //计算最近三天 //查出这三天参与秒杀活动的商品(找出秒杀活动开始的时间在这三天内) List<SeckillSessionEntity> list = this.baseMapper.selectList (new QueryWrapper<SeckillSessionEntity>() .between("start_time", startTime(), endTime())); //封装秒杀活动中秒杀商品项的数据 if (list != null && list.size() > 0) { List<SeckillSessionEntity> collect = list.stream().map(session -> { //获取通过秒杀场次的id来封装 Long id = session.getId(); //查出sms_seckill_sku_relation表中所有关联的秒杀商品 List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list (new QueryWrapper<SeckillSkuRelationEntity>() .eq("promotion_session_id", id)); session.setRelationSkus(relationSkus); return session; }).collect(Collectors.toList()); return collect; } return null; } /** * 当前时间 * @return */ private String startTime() { //now的结果是2022-3-23(也就是本地时间的年月日) LocalDate now = LocalDate.now(); //min的结果是00:00:00(常量) LocalTime min = LocalTime.MIN; //start的结果是本地时间加上min(例如 2022-3-23 00:00:00) LocalDateTime start = LocalDateTime.of(now, min); //格式化时间 String startFormat = start.format (DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return startFormat; } /** * 结束时间 * @return */ private String endTime() { // now的结果是2022-3-23 LocalDate now = LocalDate.now(); //plus的结果是2022-3-25(也就是加两天) LocalDate plus = now.plusDays(2); //max的结果是23:59:59(常量) LocalTime max = LocalTime.MAX; //end的结果是本地时间加上max(例如 2022-3-25 23:59:59) LocalDateTime end = LocalDateTime.of(plus, max); //格式化时间 String endFormat = end.format (DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return endFormat; }
/** * 秒杀活动场次实体类 */ @Data @TableName("sms_seckill_session") public class SeckillSessionEntity implements Serializable { private static final long serialVersionUID = 1L; /** * id */ @TableId private Long id; /** * 场次名称 */ private String name; /** * 每日开始时间 */ private Date startTime; /** * 每日结束时间 */ private Date endTime; /** * 启用状态 */ private Integer status; /** * 创建时间 */ private Date createTime; /** * 本场次秒杀活动的商品项 */ @TableField(exist = false) private List<SeckillSkuRelationEntity> relationSkus; }
- 实现uploadSeckillSkuLatest3Days方法中的saveSessionInfos方法(封装秒杀活动信息到缓存里,用的是List结构)
/** * 封装秒杀活动信息到缓存里 * @param sessions 秒杀活动信息 */ private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) { if (sessions!=null){ sessions.stream().forEach(session -> { //获取当前活动的开始和结束时间的时间戳 long startTime = session.getStartTime().getTime(); long endTime = session.getEndTime().getTime(); //存入到Redis中的key格式例如seckill:sessions:1648099200000_1648123200000 //seckill:sessions:是前缀,1648099200000表示秒杀活动开始的时间,1648123200000表示秒杀活动结束的时间 String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime; //判断Redis中是否有该信息,如果没有才进行添加 Boolean hasKey = redisTemplate.hasKey(key); //缓存活动信息 if (!hasKey) { //获取到活动中所有商品的skuId 格式例如:4-47 List<String> skuIds = session.getRelationSkus().stream() .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList()); //leftPushAll是批量存入缓存 /**格式是每一个缓存中秒杀活动的格式是key:seckill:sessions:1648099200000_1648123200000,value:4-47**/ redisTemplate.opsForList().leftPushAll(key,skuIds); } }); }else { log.error("没有秒杀活动"); } }
Redis缓存中秒杀活动的格式如下:
key:seckill:sessions:1648099200000_1648123200000,value:4-47
其中key的seckill:sessions:是前缀,1648099200000表示秒杀活动开始的时间,1648123200000表示秒杀活动结束的时间,value的4表示秒杀的场次id,47表示秒杀商品的skuId,一个秒杀活动里有多个秒杀商品。
实现uploadSeckillSkuLatest3Days方法中saveSessionSkuInfo(封装秒杀活动的关联商品信息到缓存里,每一个Redis缓存中具体sku信息用的是HashMap结构),通过HashMap结构把每个秒杀商品的详细信息以下面的格式存到Redis中,然后通过Redisson实现分布式信号量来把秒杀商品的库存总数量作为信号量存入redis缓存
/** * 封装秒杀活动的关联商品信息到缓存里 * @param sessions 秒杀活动信息 */ private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) { if (sessions!=null){ sessions.stream().forEach(session -> { //准备hash操作,绑定hash值seckill:skus BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //遍历秒杀活动中的商品项(seckillSkuVo表示的就是每个遍历的商品项) session.getRelationSkus().stream().forEach(seckillSkuVo -> { //生成随机码 String token = UUID.randomUUID().toString().replace("-", ""); //查看redis中有没有这个key (秒杀场次id_秒杀商品id) String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(); if (!operations.hasKey(redisKey)) { //缓存我们商品信息(SeckillSkuRedisTo是存入缓存中的对象) SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); Long skuId = seckillSkuVo.getSkuId(); //1、先查询sku的基本信息,调用远程服务 R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0) { SkuInfoVo skuInfo = info.getData( "skuInfo",new TypeReference<SkuInfoVo>(){}); redisTo.setSkuInfo(skuInfo); } //2、sku的秒杀信息 BeanUtils.copyProperties(seckillSkuVo,redisTo); //3、设置当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //4、设置商品的随机码(防止恶意攻击) redisTo.setRandomCode(token); //序列化json格式存入Redis中 String seckillValue = JSON.toJSONString(redisTo); //秒杀活动的商品项的详细信息存入redis /**格式是key:4_47 value:SeckillSkuRedisTo对象的String类型**/ operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue); //如果当前这个场次的商品库存信息已经上架就不需要上架 /**5、使用库存作为分布式Redisson信号量(限流)**/ //把每个秒杀商品的总数量作为信号量存入redis缓存,信号量标识seckill:stock:+随机(相当于key) RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); /**库存的格式key:seckill:stock:5d1df46618d34f9f9808f25cda60ba01 value:秒杀商品的总数量 其中5d1df46618d34f9f9808f25cda60ba01是随机码**/ semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); }else { log.error("没有秒杀活动"); } /** * 根据skuid查找sku对象信息 */ @RequestMapping("/info/{skuId}") public R getSkuInfo(@PathVariable("skuId") Long skuId){ SkuInfoEntity skuInfo = skuInfoService.getById(skuId); return R.ok().put("skuInfo", skuInfo); }
/** * 给Redis中存放的skuInfo的信息 **/ @Data public class SeckillSkuRedisTo { /** * 活动id */ private Long promotionId; /** * 活动场次id */ private Long promotionSessionId; /** * 商品id */ private Long skuId; /** * 秒杀价格 */ private BigDecimal seckillPrice; /** * 秒杀总量 */ private Integer seckillCount; /** * 每人限购数量 */ private Integer seckillLimit; /** * 排序 */ private Integer seckillSort; /** * sku的详细信息 */ private SkuInfoVo skuInfo; /** * 当前商品秒杀的开始时间 */ private Long startTime; /** * 当前商品秒杀的结束时间 */ private Long endTime; /** * 当前商品秒杀的随机码 */ private String randomCode; }
/** * 秒杀活动的秒杀商品的秒杀信息 **/ @Data public class SeckillSkuVo { private Long id; /** * 活动id */ private Long promotionId; /** * 活动场次id */ private Long promotionSessionId; /** * 商品id */ private Long skuId; /** * 秒杀价格 */ private BigDecimal seckillPrice; /** * 秒杀总量 */ private Integer seckillCount; /** * 每人限购数量 */ private Integer seckillLimit; /** * 排序 */ private Integer seckillSort; }