秒杀服务-----功能实现逻辑

发布时间:2025-12-10 11:35:11 浏览次数:4

管理员上架秒杀商品的流程

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@Servicepublic class SeckillScheduled @Autowiredprivate SeckillService seckillService;@Autowiredprivate 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方法的代码

@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate CouponFeignService couponFeignService;@Autowiredprivate ProductFeignService productFeignService;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate 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:"; //+商品随机码/*** 上架三天需要秒杀的商品到缓存里*/@Overridepublic 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-47List<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:skusBoundHashOperations<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方法的总流程:

  • 先远程调用优惠劵服务的SeckillSessionController控制器的getLates3DaySession方法来扫描最近三天的商品需要参加秒杀的活动(SeckillSessionWithSkusVo实体类其实就是下面的SeckillSessionEntity秒杀场次实体类)
  • /*** 上架三天需要秒杀的商品到缓存里*/@Overridepublic 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*/@Overridepublic 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-23LocalDate 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*/@TableIdprivate 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-47List<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:skusBoundHashOperations<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的信息**/@Datapublic 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;} /*** 秒杀活动的秒杀商品的秒杀信息**/@Datapublic 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;}

    通过HashMap结构把每个秒杀商品的详细信息以下面的格式存到Redis中,Redis缓存中具体格式如下:

      • hash值是seckill:skus key: 4_47 value: SeckillSkuRedisTo对象
      • 其中key的4表示秒杀的场次id,47表示秒杀商品的skuId,由于用的是hashMap结构,其中hash值是seckill:skus

    Redission实现分布式信号量设置时就会把信号量以key-value的格式存到reids缓存中,Redis缓存中信号量格式如下:

      • key: seckill:stock:随机码 value:每个秒杀商品的总数量
      • 其中key的seckill:stock是固定前缀,随机码就是随机成功的uuid值,把每个秒杀商品的总数量作为信号量的值

    saveSessionSkuInfo方法具体流程如下:

        • 准备hash操作,绑定seckill:skus关键字的hash
        • 遍历封装存入redis的秒杀活动的秒杀商品项
          • 生成随机码
          • 封装SeckillSkuRedisTo对象并序列化后存入redis缓存
            • 远程调用product商品服务
            • 使用Redission实现分布式信号量来把秒杀商品的库存总数量作为信号量存入redis缓存(限流)
            • 秒杀活动的商品项的详细信息存入redis缓存
            • 设置商品的随机码(防止恶意攻击)
            • 设置当前商品的秒杀时间信息
            • 封装秒杀活动中秒杀商品项信息

    2、查询秒杀商品

    查询秒杀商品具体实现

    1、商城首页每次刷新都发这个请求来获取当前的所有秒杀商品信息

    // 秒杀服务请求$.get("http://seckill.saodaimall.com/getCurrentSeckillSkus", function (res) {if (res.data.length > 0) {res.data.forEach(function (item) {$("<li onclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />")).append($("<p>"+item.skuInfo.skuTitle+"</p>")).append($("<span>" + item.seckillPrice + "</span>")).append($("<s>" + item.skuInfo.price + "</s>")).appendTo("#seckillSkuContent");})}})

    2、编写秒杀服务的SeckillController控制器来处理上面的请求/getCurrentSeckillSkus

    @Autowiredprivate SeckillService seckillService;/*** 当前时间可以参与秒杀的商品信息* @return*/@GetMapping(value = "/getCurrentSeckillSkus")@ResponseBodypublic R getCurrentSeckillSkus() {//获取到当前可以参加秒杀商品的信息List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();return R.ok().setData(vos);}/*** 获取到当前可以参加秒杀商品的信息* @return*/@Overridepublic List<SeckillSkuRedisTo> getCurrentSeckillSkus() {//1、确定当前属于哪个秒杀场次//获取当前时间long currentTime = System.currentTimeMillis();//查找所有redis里的秒杀活动的信息,从Redis中查询到所有key以seckill:sessions开头的所有数据(其实就是查找所有redis里的秒杀活动的信息)Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*");for (String key : keys) {// redis中秒杀活动取出来的格式是seckill:sessions:1594396764000_1594453242000//把seckill:sessions:前缀给去掉String replace = key.replace(SESSION__CACHE_PREFIX, "");String[] s = replace.split("_");//获取存入Redis商品的开始时间long startTime = Long.parseLong(s[0]);//获取存入Redis商品的结束时间long endTime = Long.parseLong(s[1]);//判断是否是当前秒杀场次 seckill:sessions:1648099200000_1648123200000if (currentTime >= startTime && currentTime <= endTime) {//2、获取这个秒杀场次需要的所有商品信息也就是key对应的value(这个key是当前秒杀活动的key)-100到100表示取出所有的keyList<String> range = redisTemplate.opsForList().range(key, -100, 100);//SECKILL_CHARE_PREFIX是seckill:skusBoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);//3、multiGet是多个key对应的value值(获取所有的秒杀商品的详细信息)List<String> listValue = hasOps.multiGet(range);if (listValue != null && listValue.size() >= 0) {List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);// redisTo.setRandomCode(null);当前秒杀开始需要随机码return redisTo;}).collect(Collectors.toList());return collect;}break;}}return null;}

    首先拿到所有秒杀活动信息的key值keys,然后判断秒杀活动的开始和结束时间是否在当前时间内,也就是选中当前时间参加秒杀的秒杀活动,存到Redis缓存中秒杀活动的格式如下:

      • key:seckill:sessions:1648099200000_1648123200000,value:4-47
      • 其中key的seckill:sessions:是前缀,1648099200000表示秒杀活动开始的时间,1648123200000表示秒杀活动结束的时间,value的4表示秒杀的场次id,47表示秒杀商品的skuId,一个秒杀活动里有多个秒杀商品。

    通过遍历所有的key,判断每个key的时间戳来选中当前时间参加秒杀的秒杀活动,然后拿出所有参加的秒杀活动里的所有秒杀商品信息range,通过HashMap结构把每个秒杀商品的详细信息以下面的格式存到Redis中,Redis缓存中具体格式如下:

      • hash值是seckill:skus key: 4_47 value: SeckillSkuRedisTo对象
      • 其中key的4表示秒杀的场次id,47表示秒杀商品的skuId,由于用的是hashMap结构,其中hash值是seckill:skus

    这里可以看出上面秒杀活动的值是作为下面秒杀商品的key值的,通过这个来把秒杀活动和秒杀商品关联起来,可以获取所有的秒杀商品的详细信息,最后把json字符串转为SeckillSkuRedisTo对象(秒杀商品的详细信息)封装成一个集合List<SeckillSkuRedisTo>

    分流程:

    • 获取redis里所有的秒杀活动的key,key的格式seckill:sessions:1594396764000_1594453242000(这里注意从Redis中获取到的数据都是Json类型)
    • 遍历获取到的所有秒杀活动的key(下面的1、2步是操作redis中的秒杀活动)
      • 判断有没有属于当前时间的秒杀活动
      • 获取当前秒杀活动所有key对应的所有value,value的格式为4-47
      • 获取所有的秒杀活动商品项的详细信息
      • 把获取到的所有秒杀活动商品项的详细信息转为SeckillSkuRedisTo对象并收集成 List<SeckillSkuRedisTo>

    3、完善商品详细信息类(新增秒杀)

    (1)给sku实体类新增秒杀属性

    //新增的秒杀属性(用于判断该商品项有没有参加秒杀活动)

    private SeckillSkuVo seckillSkuVo;package com.saodai.saodaimall.product.vo;import com.saodai.saodaimall.product.entity.SkuImagesEntity;import com.saodai.saodaimall.product.entity.SkuInfoEntity;import com.saodai.saodaimall.product.entity.SpuInfoDescEntity;import lombok.Data;import lombok.ToString;import java.util.List;/*** sku详细信息**/@ToString@Datapublic class SkuItemVo {//1、sku基本信息的获取 pms_sku_infoprivate SkuInfoEntity info;private boolean hasStock = true;//2、sku的图片信息 pms_sku_imagesprivate List<SkuImagesEntity> images;//3、获取spu的销售属性组合private List<SkuItemSaleAttrVo> saleAttr;//4、获取spu的介绍private SpuInfoDescEntity desc;//5、获取spu的规格参数信息private List<SpuItemAttrGroupVo> groupAttrs;//6、秒杀商品信息private SeckillSkuVo seckillSkuVo;} package com.saodai.saodaimall.product.vo;import lombok.Data;import java.math.BigDecimal;/*** 秒杀商品的信息**/@Datapublic class SeckillSkuVo {/*** 活动id*/private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private Integer seckillCount;/*** 每人限购数量*/private Integer seckillLimit;/*** 排序*/private Integer seckillSort;//当前商品秒杀的开始时间private Long startTime;//当前商品秒杀的结束时间private Long endTime;//当前商品秒杀的随机码private String randomCode;}

    (2)在SkuInfoServiceImpl实现类的item方法中添加封装秒杀属性

    流程

    1、远程调用秒杀服务查看当前商品有没有从参加秒杀活动

    2、如果超过了秒杀活动的最后期限就设置秒杀信息为空

    3、把远程查询到的该商品的秒杀信息封装在SkuItemVo对象中

    //秒杀CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {//3、远程调用查询当前sku是否参与秒杀优惠活动R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);if (skuSeckilInfo.getCode() == 0) {//查询成功SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {});//如果超过了秒杀活动的最后期限就设置秒杀信息为空if (seckilInfoData != null) {long currentTime = System.currentTimeMillis();if (currentTime > seckilInfoData.getEndTime()) {skuItemVo.setSeckillSkuVo(null);}}skuItemVo.setSeckillSkuVo(seckilInfoData);}}, executor);//等到所有任务都完成CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();

    (3)远程调用秒杀服务的接口

    package com.saodai.saodaimall.product.feign;import com.saodai.common.utils.R;import com.saodai.saodaimall.product.fallback.SeckillFeignServiceFallBack;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;/*** 远程调用秒杀接口**/@FeignClient(value = "saodaimall-seckill",fallback = SeckillFeignServiceFallBack.class)public interface SeckillFeignService {/*** 根据skuId查询商品是否参加秒杀活动* @param skuId* @return*/@GetMapping(value = "/sku/seckill/{skuId}")R getSkuSeckilInfo(@PathVariable("skuId") Long skuId);}

    (4)秒杀服务的SeckillController来查询当前商品有没有参加秒杀活动

    /*** 根据skuId查询商品是否参加秒杀活动* @param skuId* @return*/@GetMapping(value = "/sku/seckill/{skuId}")@ResponseBodypublic R getSkuSeckilInfo(@PathVariable("skuId") Long skuId) {SeckillSkuRedisTo to = seckillService.getSkuSeckilInfo(skuId);return R.ok().setData(to);}

    (5)getSkuSeckilInfo具体实现

    /*** 根据skuId查询商品是否参加秒杀活动* @param skuId* @return*/@Overridepublic SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {//1、绑定HashMap操作,Hash值为seckill:skusBoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);//拿到所有的keySet<String> keys = hashOps.keys();if (keys != null && keys.size() > 0) {//4-45 正则表达式进行匹配String reg = "\\d-" + skuId;for (String key : keys) {//如果匹配上了if (Pattern.matches(reg,key)) {//从Redis中取出数据来String redisValue = hashOps.get(key);//进行序列化SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);Long currentTime = System.currentTimeMillis();Long startTime = redisTo.getStartTime();Long endTime = redisTo.getEndTime();//如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间if (currentTime >= startTime && currentTime <= endTime) {return redisTo;}redisTo.setRandomCode(null);return redisTo;}}}return null;}

    流程:

    1、绑定HashMap操作,Hash值为seckill:skus

    2、获取所有的key,key的格式为4-47

    3、遍历所有的key并且用正则表达式来判断这个商品有没有对应的key

    4、如果这个商品参加了秒杀就取出reids中的数据并且转为SeckillSkuRedisTo对象

    5、如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间

    4、渲染秒杀页面

    (1)首页的秒杀商品项的渲染

    <p class="swiper-slide"><!-- 动态拼装秒杀商品信息 --><ul id="seckillSkuContent"></ul></p>

    动态拼装秒杀商品信息

    发送http://seckill.saodaimall.com/getCurrentSeckillSkus这个get请求后由秒杀服务的SeckillController的getCurrentSeckillSkus方法来处理,处理后返回List<SeckillSkuRedisTo>集合,也就是res代指的是后端封装好的List<SeckillSkuRedisTo>集合

    // 秒杀服务请求$.get("http://seckill.saodaimall.com/getCurrentSeckillSkus", function (res) {if (res.data.length > 0) {res.data.forEach(function (item) {//给每一个秒杀商品项绑定一个点击事件toDetail $("<li onclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />")).append($("<p>"+item.skuInfo.skuTitle+"</p>")).append($("<span>" + item.seckillPrice + "</span>")).append($("<s>" + item.skuInfo.price + "</s>")).appendTo("#seckillSkuContent");})}})//点击秒杀商品后跳到商品详细界面function toDetail(skuId) {location.href = "http://item.saodaimall.com/" + skuId + ".html";}

    上面动态拼装秒杀商品信息就是按下面的模板拼装的,只是说用的dom动态拼装,而下面的是静态的

    <p class="swiper-slide"><ul><li><img src=" /static/index/img/section_second_list_img6.jpg" alt=""><p>Apple iMac 21.5英寸一体机(2017新款四核Core i5 处理器/8GB内存/1TB/RP555显卡/4K屏 MNDY2CH/A) Apple iMac 21.5英寸一体机(2017新款四核Core i5 处理</p><span>¥9588.00</span><s>¥10288.00</s></li></ul></p>

    (2)商品详细信息的渲染

    下面的item是商品服务web里的ItemController控制器返回的SkuItemVo对象(其实就是商品项详细页面发送的请求来获取商品详细信息的,seckillSkuVo是SkuItemVo对象里的秒杀商品信息)

    package com.saodai.saodaimall.product.vo;import com.saodai.saodaimall.product.entity.SkuImagesEntity;import com.saodai.saodaimall.product.entity.SkuInfoEntity;import com.saodai.saodaimall.product.entity.SpuInfoDescEntity;import lombok.Data;import lombok.ToString;import java.util.List;/*** sku详细信息**/@ToString@Datapublic class SkuItemVo {//1、sku基本信息的获取 pms_sku_infoprivate SkuInfoEntity info;private boolean hasStock = true;//2、sku的图片信息 pms_sku_imagesprivate List<SkuImagesEntity> images;//3、获取spu的销售属性组合private List<SkuItemSaleAttrVo> saleAttr;//4、获取spu的介绍private SpuInfoDescEntity desc;//5、获取spu的规格参数信息private List<SpuItemAttrGroupVo> groupAttrs;//6、秒杀商品的优惠信息private SeckillSkuVo seckillSkuVo;}

    代码讲解

    1、${#dates.createNow().getTime() < item.seckillSkuVo.startTime}

    #dates表示日期工具类,.createNow().getTime() < item.seckillSkuVo.startTime表示获取当前时间然后比秒杀开始时间要早就提示哪个时候开始秒杀

    2、[[${#dates.format(new java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]

    表示获取秒杀活动开始的时间并进行日期格式化,格式化的样式yyyy-MM-dd HH:mm:ss

    3、${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}

    如果当前商品的当前时间在秒杀活动的时间范围内就显示秒杀价

    4、[[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]

    对秒杀价进行格式化,1表示小数点左边为1位数,2表示小数点右边为两位数,例如9.99

    <li style="color: red" th:if="${item.seckillSkuVo != null}"><span th:if="${#dates.createNow().getTime() < item.seckillSkuVo.startTime}">商品将会在[[${#dates.format(new java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀</span><span th:if="${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}">秒杀价 [[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]</span></li>

    代码讲解

    th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}

    自定义属性名skuId(商品项的id)、sessionId(秒杀活动场次的id)、code(随机码,用于防止恶意抢单),自定义这三个属性是因为后面实现立即请购商品时需要用(也就是第四步)

    <p class="box-btns-two"th:if="${item.seckillSkuVo == null }"><a class="addToCart" href="http://cart.saodaimall.com/addToCart" th:attr="skuId=${item.info.skuId}">加入购物车</a></p><p class="box-btns-two"th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}"><a class="seckill" href="#"th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}">立即抢购</a></p> package com.saodai.saodaimall.product.vo;import lombok.Data;import java.math.BigDecimal;/*** 秒杀商品的信息**/@Datapublic class SeckillSkuVo {/*** 活动id*/private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private Integer seckillCount;/*** 每人限购数量*/private Integer seckillLimit;/*** 排序*/private Integer seckillSort;//当前商品秒杀的开始时间private Long startTime;//当前商品秒杀的结束时间private Long endTime;//当前商品秒杀的随机码private String randomCode;}

    5、点击立即抢购发送请求

    (1)判断登录状态,只有登录了才可以抢购

    (2)发请求"http://seckill.saodaimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num

    例如http://seckill.saodaimall.com/kill?killId=4-39&key=

    9b4b6e45923b4d88ae9739a5240d0459&num=1

    其中killId是秒杀Id(其实就是封装成redis中的格式),key是随机码,num是抢购数量

    // 立即抢购之前必须登录$(".seckill").click(function () {var isLogin = [[${session.loginUser != null}]]; if (isLogin) {var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");var code = $(this).attr("code");var num = $("#productNum").val();location.href = "http://seckill.saodaimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num;} else {alert("秒杀请先登录");}return false;});

    6、实现秒杀

    (1)立即抢购的请求交给秒杀服务的SeckillController的seckill方法来处理

    /*** 当前商品进行秒杀(秒杀开始)* @param killId 秒杀号* @param key 随机码* @param num 秒杀商品的数量* @return*///http://seckill.saodaimall.com/kill?killId=4-39&key=9b4b6e45923b4d88ae9739a5240d0459&num=1@GetMapping(value = "/kill")public String seckill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num,Model model) {String orderSn = null;try {//秒杀实现orderSn = seckillService.kill(killId,key,num);model.addAttribute("orderSn",orderSn);} catch (Exception e) {e.printStackTrace();}return "success";}

    (2)秒杀的具体实现

    /*** 当前商品进行秒杀(秒杀开始)* @param killId 秒杀号* @param key 随机码* @param num 秒杀商品的数量* @return*/@Overridepublic String kill(String killId, String key, Integer num) throws InterruptedException {//测试这个方法执行的时间要多久long s1 = System.currentTimeMillis();//获取当前用户的信息MemberResponseVo user = LoginUserInterceptor.loginUser.get();//1、绑定hashMap操作redis,绑定的Hash值为seckill:skusBoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);//2、根据killId获取当前sku商品的详细信息String skuInfoValue = hashOps.get(killId);if (StringUtils.isEmpty(skuInfoValue)) {return null;}SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);Long startTime = redisTo.getStartTime();Long endTime = redisTo.getEndTime();long currentTime = System.currentTimeMillis();//判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)if (currentTime >= startTime && currentTime <= endTime) {//2、效验随机码和商品idString randomCode = redisTo.getRandomCode();String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();if (randomCode.equals(key) && killId.equals(skuId)) {//3、验证购物数量是否合理和库存量是否充足//获取秒杀的商品的限制数量Integer seckillLimit = redisTo.getSeckillLimit();//获取信号量(也就是库存数量)String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);Integer count = Integer.valueOf(seckillCount);//判断信号量是否大于0,并且买的数量不能超过库存if (count > 0 && num <= seckillLimit && count > num ) {//4、验证这个人是否已经买过了(幂等性处理),防止有人每次的确只买限制次数的一个,但是他买多次// 如果秒杀成功,就去占位。也就是在redis中存个标记位,表示这个用户买过了,不能再买//SETNX 原子性处理/**格式key:userId-skuId value:用户秒杀时买的数量**/String redisKey = user.getId() + "-" + skuId;//设置自动过期(活动结束时间-当前时间)Long ttl = endTime - currentTime;Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);if (aBoolean) {//占位成功说明从来没有买过,分布式锁(获取信号量-1)RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);//TODO 秒杀成功,快速下单//尝试快速拿到信号量,100毫秒没有用拿到就返回falseboolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);//保证Redis中还有商品库存if (semaphoreCount) {//创建订单号和订单信息发送给MQ// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右//快速生成一个秒杀订单号String timeId = IdWorker.getTimeId();SeckillOrderTo orderTo = new SeckillOrderTo();//设置秒杀订单号orderTo.setOrderSn(timeId);orderTo.setMemberId(user.getId());orderTo.setNum(num);orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());orderTo.setSkuId(redisTo.getSkuId());orderTo.setSeckillPrice(redisTo.getSeckillPrice());rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);long s2 = System.currentTimeMillis();log.info("耗时..." + (s2 - s1));return timeId;}}}}}long s3 = System.currentTimeMillis();log.info("耗时..." + (s3 - s1));return null;}

    流程:

    1、绑定hashMap操作redis,绑定的Hash值为seckill:skus

    2、根据killId获取当前sku商品的详细信息,也就是获取这个key对应的value(这个killId其实就是redis中的key,格式为4-47)

    3、判断当前这个秒杀商品是否在秒杀活动时间区间内(效验时间的合法性)

    4、效验随机码是否和缓存里的一致

    5、判断信号量是否大于0(也就是看有没有库存),并且买的数量不能超过秒杀商品数量的限制

    6、进行幂等性处理(验证这个人是否已经买过了,防止有人每次的确只买限制次数的一个,但是他买多次,相当于发多次请求)

    7、占坑成功后进行信号量处理(其实就是减信号量,表示业务为减库存)

    8、快速生成一个秒杀订单号

    9、封装SeckillOrderTo对象,然后发给RabbitMQ队列

    7、订单服务来监听上面发送给RabbitMQ的order.seckill.order.queue队列的消息

    (1)在订单服务的listener的OrderSeckillListener来监听消息队列的消息,然后自己创建秒杀订单

    package com.saodai.saodaimall.order.listener;import com.rabbitmq.client.Channel;import com.saodai.common.to.mq.SeckillOrderTo;import com.saodai.saodaimall.order.service.OrderService;import lombok.extern.slf4j.Slf4j;import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.IOException;/*** 秒杀订单的监听*/@Slf4j@Component@RabbitListener(queues = "order.seckill.order.queue")public class OrderSeckillListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {log.info("准备创建秒杀单的详细信息...");try {orderService.createSeckillOrder(orderTo);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}

    (2)监听队列后在OrderServiceImpl创建秒杀订单

    流程:

    1、封装订单实体类

    1>封装秒杀的信息到订单里

    2>保存订单到数据库

    2、封装秒杀订单项

    1>封装秒杀信息

    2>远程调用查询spu信息来封装到秒杀订单项

    3>远程调用查询sku信息来封装到秒杀订单项

    4>保存秒杀订单项到数据库

    /*** 创建秒杀单*/@Overridepublic void createSeckillOrder(SeckillOrderTo orderTo) {//封装订单实体类OrderEntity orderEntity = new OrderEntity();// 保存秒杀信息(getOrderSn这个订单号是秒杀的订单号)orderEntity.setOrderSn(orderTo.getOrderSn());orderEntity.setMemberId(orderTo.getMemberId());orderEntity.setCreateTime(new Date());//秒杀总共价格BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));//应该支付的价格orderEntity.setPayAmount(totalPrice);//CREATE_NEW(0,"待付款")orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());//保存秒杀订单this.save(orderEntity);//保存秒杀订单项信息OrderItemEntity orderItem = new OrderItemEntity();orderItem.setOrderSn(orderTo.getOrderSn());//秒杀总共价格orderItem.setRealAmount(totalPrice);//秒杀商品购买的数量orderItem.setSkuQuantity(orderTo.getNum());//保存商品的spu信息//远程调用商品服务来查询商品信息R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {});orderItem.setSpuId(spuInfoData.getId());orderItem.setSpuName(spuInfoData.getSpuName());orderItem.setSpuBrand(spuInfoData.getBrandName());orderItem.setCategoryId(spuInfoData.getCatalogId());//保存sku的信息R skuInfoBySkuId = productFeignService.getSkuInfoBySkuId(orderTo.getSkuId());SkuInfoTo skuInfoTo = skuInfoBySkuId.getData("data",new TypeReference<SkuInfoTo>(){});orderItem.setSkuId(skuInfoTo.getSkuId());orderItem.setSkuPic(skuInfoTo.getSkuDefaultImg());orderItem.setSkuName(skuInfoTo.getSkuName());orderItem.setSkuPrice(skuInfoTo.getPrice());//赠送积分orderItem.setGiftIntegration(0);//赠送成长值orderItem.setGiftGrowth(0);//保存订单项数据orderItemService.save(orderItem);} package com.saodai.common.to.mq;import lombok.Data;import java.math.BigDecimal;/***秒杀订单类(秒杀商品成功后发给rabbit队列的封装类)**/@Datapublic class SeckillOrderTo {/*** 订单号*/private String orderSn;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 购买数量*/private Integer num;/*** 会员ID*/private Long memberId;}/*** 根据skuid查找sku对象*/@RequestMapping("/getSkuInfoBySkuId/{skuId}")//@RequiresPermissions("product:skuinfo:info")public R getSkuInfoBySkuId(@PathVariable("skuId") Long skuId){SkuInfoEntity skuInfo = skuInfoService.getById(skuId);return R.ok().setData(skuInfo);}

    需要做网站?需要网络推广?欢迎咨询客户经理 13272073477