Redis修炼 (9.redis 解决秒杀业务中 全局唯一ID 和 乐观锁解决秒杀下单功能)

秒杀业务

说一个典型的场景:我们在app里面抢优惠券。

每个店铺都会发自己的优惠券,

当用户抢购的时候 生产订单就会保存在订单表中,订单表如果使用数据库自增ID会有一个问题:

订单表是一个数据量可能非常大的表 受单表数量限制太大,如果将来要分表 这些id在不同的数据表里面自增 会会出现问题。

全局唯一id

在某些业务场景下 我们需要全局id 全局id应该符合这些条件
![在这里插入图片描述](https://img-blog.csdnimg.cn/b30475dd59be4a0aa111dcca4cd09eca.png

在这里插入图片描述
来一个例子比如下面这个 全局id的结构:

我们使用long型 8个字节 64位 第一位符号为 后面31为存时间戳 后面32位存我们真正的订单号

在这里插入图片描述

    //这是2022年开始的时间
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate =stringRedisTemplate;
    }


    //keyPrefix为业务前缀
    public long nextId(String keyPrefix){

        //生成时间戳

        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        //生成序列号

       String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
       long count = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);

       //这里使用位运算 来拼接 lang//拼接并返回

        return timestamp << COUNT_BITS | count;
    }

这里面新手知道原理就行了,
需要掌握的就是这个redis 自增。
incr key 这个命令 每执行一次 就增长一个数字 他是全局增加的 ,依次来生成唯一的id

这里就是全局唯一id的生成 策略 使用 redis自增

全局唯一id 还有一种有名的算法 叫雪花算法。


秒杀下单

解决了全局唯一ID 的问题。 我们就可以聊一下 具体的秒杀下单的业务。

什么样的东西需要秒杀? 指的是 一段时间内 数量有限制的 商品。

比如代金券——一段时间内有效 | 数量只有一部分 抢完就没了。

比如在app 上有一张秒杀券, 这时候用户抢了这张.

这时候应该首先 判断一下 券的使用时间是不是正确/ 券的数量是不是足够。

符合条件话 就生成订单 秒杀券-1

    public Result seckillVoucher(Long voucherId) {
        //查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        //判断是否开始和结束

        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
            return Result.fail("秒杀尚未开始");
        }

        if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
            return Result.fail("秒杀已经结束");
        }
        //判断是否库存充足

        if(seckillVoucher.getStock()<1){
            return Result.fail("秒杀券库存不足");
        }

        //扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id",voucherId)
                .update();

        if(!success){
            return Result.fail("扣减失败 库存不足");
        }

        //创建订单

        VoucherOrder voucherOrder = new VoucherOrder();

        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);

        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);

        voucherOrder.setVoucherId(voucherId);

        save(voucherOrder);

        //返回订单id
        return Result.ok(orderId );
     }

高并发情况

但是这是低并发的情况下,但是在高并发的情况下 问题就有了。
我们使用jmeter 对刚才的功能 进行 压力测试:
在这里插入图片描述

具体的 怎么用 jmeter压力测试:

发现数据库里面出现了:库存 = -9 !
在这里插入图片描述
这里老手肯定知道原因,但是新手对并发编程不太了解 这里会懵逼:

因为这里会出现 两个线程 一起操作同一个数据 两个操作出现并发安全问题
在这里插入图片描述


这时候怎么办呢??

加锁

我们使用乐观锁来解决这个问题

乐观锁常见的思想种:

  • 版本号法(也就cas法): 给数据定义一个版本号字段, 查询的时候 把版本号也查出来 ,然后更新的时候对比以下前后版本 来判断是不是 有其他线程更改过。这个版本号 你可以新建一个字段 也可以用现有的字段 比如我们就用库存来做

我们尝试这样简单改了一下 每次查的时候 检测一下 刚开始的库存值是不是相同:

    //扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id",voucherId)
                .eq("stock",seckillVoucher.getStock()) //加一行 stock = 刚开始查到的stock 确定没有被其他线程修改过 才进行更新
                .update();

这里我们再用jmeter 压力测试一下:我们发现!

靠 只卖出20个? 刚才是-9 现在是80
在这里插入图片描述
这里其实很好理解
你想
200个人来抢100个票
你的乐观锁 每次要改库存的时候 查一下是不是和刚开始查的库存数量一样
不一样就说明已经有线程更改过了 那就放弃。

问题在于 1个线程更改的同时 可能有99个线程同时都 查一下是不是和刚开始查的库存数量一样 然后发现不一样, 这时候这99个都gg。

从代码上讲 线程安全是保住了 但是从业务上讲 等于该枪的没抢到。


改进 刚才是 stock = 刚开始查到的 stock
我们改成 stock>0

        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id",voucherId)
                .gt("stock",0) //加一行 stock > 0 刚开始查到的stock 确定没有被其他线程修改过 才进行更新
                .update();

可以了这次抢干净了:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值