秒杀业务
说一个典型的场景:我们在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();
可以了这次抢干净了: