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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 秒杀服务-----功能实现逻辑

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

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;
}

  1. 实现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;
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
NoSQL 前端开发 Redis
《我们一起进大厂》系列-秒杀系统设计
《我们一起进大厂》系列-秒杀系统设计
137 2
|
5月前
|
XML 前端开发 JavaScript
视频弹幕设计网站02-----数据库设计与后端配置
视频弹幕设计网站02-----数据库设计与后端配置
|
5月前
|
前端开发 数据库 索引
前后端分离------后端创建笔记(05)用户列表查询接口(下)
前后端分离------后端创建笔记(05)用户列表查询接口(下)
|
4月前
|
前端开发 API
支付系统27-------梳理一下支付按钮,前端的代码
支付系统27-------梳理一下支付按钮,前端的代码
|
6月前
|
小程序
外卖小程序-购物车模块表结构设计和后端代码
外卖小程序-购物车模块表结构设计和后端代码
58 0
|
6月前
|
存储 数据库 UED
秒杀系统数据库设计核心要点详解
秒杀系统数据库设计核心要点详解
228 1
|
6月前
|
缓存 前端开发 JavaScript
若依框架中的权限控制逻辑 ---- 菜单
若依框架中的权限控制逻辑 ---- 菜单
601 0
|
消息中间件 缓存 JSON
秒杀服务-----功能实现逻辑2
秒杀服务-----功能实现逻辑2
155 0
|
缓存 NoSQL Redis
订单服务-----功能实现逻辑1
订单服务-----功能实现逻辑
89 0
|
JavaScript Java 数据库
订单服务-----功能实现逻辑2
订单服务-----功能实现逻辑2
201 0