之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复。今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道一万都不如满满的干货来的实在,干货都下面了!
前提:分布式系统,高并发场景
商品A只有100库存,现在有1000或者更多的用户购买。如何保证库存在高并发的场景下是安全的。
预期结果:1.不超卖 2.不少卖 3.下单响应快 4.用户体验好
下单思路:
(退单有单独的库存流水,申请退单插入流水,退单完成删除流水+还库存)
分析:
所以综上所述:
选择方案三比较合理。
重复下单问题
//key , 等待获取锁的时间 ,锁的时间redis.lock("shop-oms-submit" + token, 1L, 10L);
redis的key用token + 设备编号 一个用户多个设备可以同时下单。
//key , 等待获取锁的时间 ,锁的时间redis.lock("shop-oms-submit" + token + deviceType, 1L, 10L);
(2)防止恶意用户,恶意攻击 : 一分钟调用下单超过50次 ,加入临时黑名单 ,10分钟后才可继续操作,一小时允许一次跨时段弱校验。使用reids的list结构,过期时间一小时
/*** @param token* @return true 可下单*/public boolean judgeUserToken(String token) {//获取用户下单次数 1分钟50次String blackUser = "shop-oms-submit-black-" + token;if ((blackUser) != null) {return false;}String keyCount = "shop-oms-submit-count-" + token;Long nowSecond = w().toEpochSecond(ZoneOffset.of("+8"));//每一小时清一次key 过期时间1小时Long count = redis.rpush(keyCount, String.valueOf(nowSecond), 60 * 60);if (count < 50) {return true;}//获取第50次的时间List<String> secondString = redis.lrange(keyCount, count - 50, count - 49);Long oldSecond = Long.(0));//now > oldSecond + 60 用户可下单boolean result = nowSecondpareTo(oldSecond + 60) > 0;if (!result) {//触发限制,加入黑名单,过期时间10分钟redis.set(blackUser, String.valueOf(nowSecond), 10 * 60);}return result;}
多用户抢购时,如何做到并发安全减库存?
sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;
分析:
高并发场景下,假设库存只有 1件 ,两个请求同时进来,抢购该商品.
数据库层面会限制只有一个用户扣库存成功。在并发量不是很大的情况下可以这么做。但是如果是秒杀,抢购,瞬时流量很高的话,压力会都到数据库,可能拖垮数据库。
/*** 缺点并发不高,同时只能一个用户抢占操作,用户体验不好!** @param orderSkuAo*/public boolean subtractStock(OrderSkuAo orderSkuAo) {String lockKey = "shop-product-stock-subtract" + OrderCode();(lockKey)){return false;}try {lock.lock(lockKey, 1L, 10L);//处理逻辑}catch (Exception e){("e=",e);}finally {lock.unLock(lockKey);}return true;}
分析:
利用Redis 分布式锁,强制控制同一个商品处理请求串行化,缺点并发不高 ,处理比较慢,不适合抢购,高并发场景。用户体验差,但是减轻了数据库的压力。
/*** 扣库存操作,秒杀的处理方案* @param orderCode* @param skuCode* @param num* @return*/public boolean subtractStock(String orderCode,String skuCode, Integer num) {String key = "shop-product-stock" + skuCode;Object value = (key);if (value == null) {//前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品return false;}//先检查 库存是否充足Integer stock = (Integer) value;if (stock < num) {LogUtil.info("库存不足");return false;} //不可在这里直接操作数据库减库存,否则导致数据不安全//因为此时可能有其他线程已经将redis的key修改了//redis 减少库存,然后才能操作数据库Long newStock = redis.increment(key, -num.longValue());//库存充足if (newStock >= 0) {LogUtil.info("成功抢购");//TODO 真正扣库存操作 可用MQ 进行 redis 和 mysql 的数据同步,减少响应时间} else {//库存不足,需要增加刚刚减去的库存redis.increment(key, num.longValue());LogUtil.info("库存不足,并发");return false;}return true;}
分析:
利用Redis increment 的原子操作,保证库存安全,利用MQ保证高并发响应时间。但是事需要把库存的信息保存到Redis,并保证Redis 和 Mysql 数据同步。缺点是redis宕机后不能下单。
increment 是个原子操作。
方案三满足秒杀、高并发抢购等热点商品的处理,真正减扣库存和下单可以异步执行。在并发情况不高,平常商品或者正常购买流程,可以采用方案一数据库乐观锁的处理,或者对方案三进行重新设计,设计成支持单订单多商品即可,但复杂性提高,同时redis和mysql数据一致性需要定期检查。
订单时效问题
超过订单有效时间,订单取消,可利用MQ或其他方案回退库存。
设置定时检查
Spring task 的cron表达式定时任务
MQ消息延时队列
TCC 模型:Try/Confirm/Cancel:不使用强一致性的处理方案,最终一致性即可,下单减库存,成功后生成订单数据,如果此时由于超时导致库存扣成功但是返回失败,则通过定时任务检查进行数据恢复,如果本条数据执行次数超过某个限制,人工回滚。还库存也是这样。
幂等性:分布式高并发系统如何保证对外接口的幂等性,记录库存流水是实现库存回滚,支持幂等性的一个解决方案,订单号+skuCode为唯一主键(该表修改频次高,少建索引)
乐观锁:where stock + num>0
消息队列:实现分布式事务 和 异步处理(提升响应速度)
redis:限制请求频次,高并发解决方案,提升响应速度
分布式锁:防止重复提交,防止高并发,强制串行化
分布式事务:最终一致性,同步处理(Dubbo)/异步处理(MQ)修改 + 补偿机制
本文发布于:2024-01-28 00:45:37,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/17063739373630.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |