Springboot秒杀系统(乐观锁+RateLimiter令牌+Redis缓存)

2023-10-05 13:02:44 浏览数 (1)

一、前言

本文主要是利用springboot,实现一个单机版秒杀demo,通过单机版实现,可以对基本并发秒杀的知识有一定的了解。

二、秒杀业务类实现

首先先提供秒杀业务实现类,该业务类中,定义秒杀方法,方法用synchronized修饰,单机应用增加悲观锁,注意,与@Transactional一起使用是,不会生效,如要要使用的话在调用该方法的地方使用synchronized代码块//原因:Transactional事务是在锁之前开始的,事务范围广,当一个线程锁释放了,但是事务还没提交,当下个线程过来是,一起提交上一次事务,一般不建议使用,线程会单个使用,降低效率,并且不要在业务代码增加synchronized

代码语言:javascript复制
/**
 * spring 注解加在实现类
 */
@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StockMapper stockMapper;

    @Autowired
    private StockOrderMapper orderMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;
    //方法用synchronized修饰,单机应用增加悲观锁
    //注意,与@Transactional一起使用是,不会生效,如要要使用的话在调用该方法的地方使用synchronized代码块
    //原因:Transactional事务是在锁之前开始的,事务范围广,当一个线程锁释放了,但是事务还没提交,当下个线程过来是,一起提交上一次事务
    //一般不建议使用,线程会单个使用,降低效率,并且不要在业务代码增加synchronized
    @Override
    public  int kill(Integer id) {

        //加入redis缓存限时抢购,即使获得令牌如果不在活动时间内也是无法抢购
        //校验redis中秒杀商品是否结束
        if (!redisTemplate.hasKey("kill" id)) {
           throw new RuntimeException("当前商品的抢购活动已经结束啦!");

        }
        //校验库存
        StockDO stockDO = checkStock(id);
        //扣减库存
        updateStock(stockDO);
        //创建订单

        return createOrder(stockDO);
  /*      //根据id校验库存
        StockDO stockDO = stockMapper.checkStock(id);

        if(stockDO.getSale().equals(stockDO.getCount())){
            throw new RuntimeException("库存不足。。。");
        }else {
            //扣减库存
            stockDO.setSale(stockDO.getSale()   1);
            stockMapper.update(stockDO);
            //创建订单
            StockOrderDO stockOrderDO = new StockOrderDO();
            stockOrderDO.setSid(id).setName(stockDO.getName()).setCreateTime(new Date());
            orderMapper.insert(stockOrderDO);
            return stockOrderDO.getId();
        }*/

    }

    private StockDO checkStock(Integer id){
        StockDO stockDO = stockMapper.checkStock(id);

        if(stockDO.getSale().equals(stockDO.getCount())) {
            throw new RuntimeException("库存不足。。。");
        }
        return  stockDO;
    }

    private void updateStock(StockDO stockDO){

        //使用乐观锁
        //在sql完成销量 1 和版本号的 1,并且根据商品id和版本号同时查询更新商品
        //使用版本号,数据库不支持并发写
        int updateRows = stockMapper.update(stockDO);
        if(updateRows == 0){
            throw new RuntimeException("抢购失败,请重试!!!");
        }
    }

    private Integer createOrder(StockDO stockDO){
        StockOrderDO stockOrderDO = new StockOrderDO();
        stockOrderDO.setSid(stockDO.getId())
                .setName(stockDO.getName())
                .setCreateTime(new Date());
        orderMapper.insert(stockOrderDO);
        return stockOrderDO.getId();
    }
}

二.使用synchronized关键字悲观锁

使用synchronized关键字悲观锁,防止超卖,使用悲观锁的话,对资源浪费比较大,每一次只允许一个线程访问,降低效率,其他的只能等待,显示是不合理的。不过如果真的要是用synchronized,不要在业务代码使用,必须在调用业务代码的地方使用同同步代码块,原因如下:

业务代码使用了@Transactional注解,一起使用是不会生效,因为Transactional事务是在锁之前开始的,事务范围广,当一个线程锁释放了,但是事务还没提交,当下个线程过来是,一起提交上一次事务。

代码语言:javascript复制
   @GetMapping("/kill")
    public  String kill(Integer id){
        System.out.println("秒杀商品id = " id);

        try {
            //根据秒杀商品id,去调秒杀业务
            //调用处增加synchronized悲观锁
            //先加锁,然后在开启事物,可以保证安全性。
             synchronized(this){
            int orderId = orderService.kill(id);
            return "秒杀成功,订单id:"   orderId;
              }

        }catch (Exception e){
            e.printStackTrace();
            return e.getMessage();
        }


    }

三、数据库层面加锁

数据库层面version版本号,乐观锁防止超卖,利用数据库不支持并发写,每一次只允许一个线程操作。数据增加version字段,每次修改都更加version去修改,并且数据增加销售量。

代码语言:javascript复制
  <update id="update">
         update stock
         set sale = sale   1,
         version = version  1
         where id = #{id} and version =#{version}

    </update>

使用测试发现利用乐观锁进行控制商品秒杀,同样的并发量,明显效率比较高,时间也是缩短一半左右。

四、Google guava 加锁

Google guava RateLimiter令牌桶算法接口限流,在利用乐观锁的实现超卖的前提下进行限流,因为是接口限流所以是在前端调用的时候进行限制。

这边插入一下限流算法的概念:

代码语言:javascript复制
限流:是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或者宕机

接口限流:在面临高并发的抢购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力,
    大量的请求抢购成功时需要调用下单接口,过多的请求达到数据库时会对系统的稳定性造成影响

常用限流算法:令牌桶算法、漏斗算法(用的少),Google开源项目Guava中的RateLimiter使用的就是令牌桶控制的算法。
        在实际开发高并发系统时有三把利器:缓存、降级、限流
    缓存:缓存的目的是提升系统访问量速度和增大系统处理容量
    降级:当服务压力剧增的情况下,根据当前业务情况及流量对一些服务和页面的降级(springcloud的hystrix),
        以此释放服务器资源以保证核心任务的正常运行
    限流:目的是通过对并发访问/请求进行限速,
        或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制的速率则可以拒绝服务、排队或者等待、降级处理

    漏斗算法(简单粗暴):漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),
            当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率
    令牌桶算法:令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,
            系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),
            如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
            设置超时时间,超过时间自己抛弃

详细算法介绍,可以参考:https://cloud.tencent.com/developer/article/2316325

代码实现:

代码语言:javascript复制
  //创建令牌桶实例
    private RateLimiter rateLimter = RateLimiter.create(10);


    /**
     * 秒杀方法
     * @param id
     * @return
     */
    @GetMapping("/killToken")
    public  String killToken(Integer id){
        //System.out.println("秒杀商品id = " id);
        //秒杀前,执行限流操作,获得到了令牌才执行抢购
        //@return {@code true} if the permit was acquired, {@code false} otherwise
        //如果超过等待时间
        if (!rateLimter.tryAcquire(1,TimeUnit.SECONDS)){
            log.error("当前请求被限流,直接抛弃,抢购失败!!,当前活动过于火爆,请重试");
            return "抢购失败!!,当前活动过于火爆,请重试";
        }
        try {
            //根据秒杀商品id,去调秒杀业务
            //调用处增加synchronized悲观锁
            //先加锁,然后在开启事物,可以保证安全性。
           // synchronized(this){
                int orderId = orderService.kill(id);
                log.info("秒杀商品id = " id);
                return "秒杀成功,订单id:"   orderId;
          //  }

        }catch (Exception e){
            e.printStackTrace();
            return e.getMessage();
        }


    }

五、Redis缓存

4.Redis缓存抢购时间,主要是为了缓解数据压力,利用缓存在调用数据库前,判断是否秒杀活动结束了,并且秒杀的话存在的时间也不是很长,如果存在才进行数据库操作,所以即使获得的秒杀资格但是活动结束的话也是抢购失败,主要是在业务层进行控制。即数据存一个秒杀key,设置秒杀时间比如:set kill1 EX 180 表示秒杀key存在180秒

代码语言:javascript复制
  //加入redis缓存限时抢购,即使获得令牌如果不在活动时间内也是无法抢购
        //校验redis中秒杀商品是否结束
        if (!redisTemplate.hasKey("kill" id)) {
           throw new RuntimeException("当前商品的抢购活动已经结束啦!");

        }

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

0 人点赞