利用Redis increment 的原子操作,保证库存数安全
- 先查询redis中是否有库存信息,如果没有就去数据库查,这样就可以减少访问数据库的次数。
获取到后把数值填入redis,以商品id为key,数量为value。
注意要设置序列化方式为StringRedisSerializer,不然不能把value做加减操作。
还需要设置redis对应这个key的超时时间,以防所有商品库存数据都在redis中。 - 比较下单数量的大小,如果够就做后续逻辑。
- 执行redis客户端的increment,参数为负数,则做减法。因为redis是单线程处理,并且因为increment让key对应的value 减少后返回的是修改后的值。
有的人会不做第一步查询直接减,其实这样不太好,因为当库存为1时,很多做减3,或者减30情况,其实都是不够,这样就白减。 - 扣减数据库的库存,这个时候就不需要再select查询,直接乐观锁update,把库存字段值减1 。
- 做完扣库存就在订单系统做下单。
样例场景:
- 假设两个用户在第一步查询得到库存等于10,A用户走到第二步扣10件,同时一秒内B用户走到第二部扣3件。
- 因为redis单线程处理,若A用户线程先执行redis语句,那么现在库存等于0,B就只能失败,就不会出更新数据库了。
public void order(OrderReq req) {
String key = "product:" + req.getProductId();
// 第一步:先检查 库存是否充足
Integer num = (Integer) redisTemplate.get(key);
if (num == null){
// 去查数据库的数据
// 并且把数据库的库存set进redis,注意使用NX参数表示只有当没有redis中没有这个key的时候才set库存数量到redis
//注意要设置序列化方式为StringRedisSerializer,不然不能把value做加减操作
// 同时设置超时时间,因为不能让redis存着所有商品的库存数,以免占用内存。
if (count >=0) {
//设置有效期十分钟
redisTemplate.expire(key, 60*10+随机数防止雪崩, TimeUnit.SECONDS);
}
// 减少经常访问数据库,因为磁盘比内存访问速度要慢
}
if (num < req.getNum()) {
logger.info("库存不足");
}
// 第二步:减少库存
long value = redisTemplate.increment(key, -req.getNum().longValue());
// 库存充足
if (value >= 0) {
logger.info("成功购买");
// update 数据库中商品库存和订单系统下单,单的状态未待支付
// 分开两个系统处理时,可以用LCN做分布式事务,但是也是有概率会订单系统的网络超时
// 也可以使用最终一致性的方式,更新库存成功后,发送mq,等待订单创建生成回调。
boolean res= updateProduct(req);
if (res)
createOrder(req);
} else {
// 减了后小小于0 ,如两个人同时买这个商品,导致A人第一步时看到还有10个库存,但是B人买9个先处理完逻辑,
// 导致B人的线程10-9=1, A人的线程1-10=-9,则现在需要增加刚刚减去的库存,让别人可以买1个
redisTemplate.increment(key, req.getNum().longValue());
logger.info("恢复redis库存");
}
}
update使用乐观锁
updateProduct方法中执行的sql如下:
update Product set count = count - #{购买数量} where id = #{id} and count - #{购买数量} >= 0;
虽然redis已经防止了超卖,但是数据库层面,为了也要防止超卖,以防redis崩溃时无法使用或者不需要redis处理时,则用乐观锁,因为不一定全部商品都用redis。
利用sql每条单条语句都是有事务的,所以两条sql同时执行,也就只会有其中一条sql先执行成功,另外一条后执行,也如上文提及到的场景一样。
简单说一下分布式事务:
分开两个系统处理库存和订单时,这个时候可以用LCN框架做分布式事务,但是因为是http请求的,也是有概率会订单系统的网络超时,导致未返回结果。
其实也可以使用最终一致性的方式,数据表记录一条交互流水记录,更新库存成功后,更新这个交互流水记录的库存操作字段为已处理,订单处理字段为处理中,然后发送mq,等待订单创建生成回调。也要做定时任务做主动查询订单系统的结果,以防没有结果回来。
方案优势
- 不需要频繁访问数据库商品库存还有多少
- 不阻塞其他用户
- 安全扣减库存量
- 内存访问库存数量,减少数据库交互
高并发额外优化
- 用户访问下单是,前端ui可以让用户触发结算后,把按钮置灰色,防止重复触发。
- 可以按照库存数量来选定是否要用redis,因为如果库存数量少,或者说最近下单次数少的商品,就不用放redis,因为少人看和买的情况下,不必放redis导致占用内存。
- 如果到时间点抢购时,可以使用mq队列形式,用户触发购买商品后,进入队列,让用户的页面一直在转圈圈,等轮到他买的时候再进入结算页面,结算页面的后续流程和本文一致。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。