在电商系统中买商品过程,先加入购物车,然后选中商品,点击结算,即会进入待支付状态,后续支付。过程需要检验库存是否足够,保证库存不被超卖。
场景一:买家需要购买数量可以多件场景二:秒杀活动,到时间点只能购买一件
主要环节:购物车->结清->支付
本文讲述结清时,扣库存环节,分布式系统产生订单环节后续文章再详细分析。
备注:挺推荐使用https://www.processon.com/在线来做流程图的
用分布式锁,是为了防刷、防止同一个用户同一秒里面把购物车里的商品进行多次结算,防止前端代码出问题触发两次。利用Jedis客户端编写分布式锁
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
lockKey是redis的Key,为用户id 商品id 商品数量组成,这样同一秒中只能有一次处理逻辑。requestId是redis的value,实际是当前线程id,表示有一条线程占用。
大家要注意这种分布式锁写法,是同时设定超时时间的。有些分布式锁的文章可能是比较旧版的redis不支持同时设置超时时间,他就一条语句先设置key value,另一条语句后设置超时时间。所以大家留意一下。
安全扣减库存方案有很多说法,列一下几个方案和我推荐的方案。
有的文章会用redis分布式锁来做保证扣库存数量准确的环节,让点击结算时,后端逻辑会查询库存和扣库存的update语句同时只有一条线程能够执行,以商品id为分布式锁的key,锁一个商品。但是这样,其他购买相同商品的用户将会进行等待。
也有文章会说借鉴ConcurrenthashMap,分段锁的机制,把100个商品,分在3个段上,key为分段名字,value为库存数量。用户下单时对用户id进行%3计算,看落在哪个redis的key上,就去取哪个。
如key1=product-01,value1=33;key2=product-02,value2=33;key3=product-03,value3=33;
其实会有几个问题:
缺点:
redis队列的lpush、rpop都是只能每次进出一个,对于购买多个数量的情况下不适用,只适用于秒杀情况购买一个的场景、或者抢红包的场景,所以觉得不是很通用。
备注:这个抢红包场景以后再分享。
*样例场景:*
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库存");
}
}
updateProduct方法中执行的sql如下:
update Product set count = count - #{购买数量} where id = #{id} and count - #{购买数量} >= 0;
虽然redis已经防止了超卖,但是数据库层面,为了也要防止超卖,以防redis崩溃时无法使用或者不需要redis处理时,则用乐观锁,因为不一定全部商品都用redis。
利用sql每条单条语句都是有事务的,所以两条sql同时执行,也就只会有其中一条sql先执行成功,另外一条后执行,也如上文提及到的场景一样。
分开两个系统处理库存和订单时,这个时候可以用LCN框架做分布式事务,但是因为是http请求的,也是有概率会订单系统的网络超时,导致未返回结果。
其实也可以使用最终一致性的方式,数据表记录一条交互流水记录,更新库存成功后,更新这个交互流水记录的库存操作字段为已处理,订单处理字段为处理中,然后发送mq,等待订单创建生成回调。也要做定时任务做主动查询订单系统的结果,以防没有结果回来。
掘金:地藏Kelvin
简书:地藏Kelvin
CSDN:地藏Kelvin
我的Gitee: 地藏Kelvin https://gitee.com/dizang-kelvin