前言
以前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是由于加班的缘由比较忙,就一直没来得及回复。今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让你们都能学习到,说一千道一万都不如满满的干货来的实在,干货都下面了!前端
介绍
前提:分布式系统,高并发场景
商品A只有100库存,如今有1000或者更多的用户购买。如何保证库存在高并发的场景下是安全的。
预期结果:1.不超卖 2.很多卖 3.下单响应快 4.用户体验好
mysql
下单思路:面试
- 下单时生成订单,减库存,同时记录库存流水,在这里须要先进行库存操做再生成订单数据,这样库存修改为功,响应超时的特殊状况也能够经过第四步定时校验库存流水来完成最终一致性。
- 支付成功删除库存流水,处理完成删除可让库存流水数据表数据量少,易于维护。
- 未支付取消订单,还库存+删除库存流水
- 定时校验库存流水,结合订单状态进行响应处理,保证最终一致性
(退单有单独的库存流水,申请退单插入流水,退单完成删除流水+还库存)redis
何时进行减库存
- 方案一:加购时减库存。
- 方案二:确认订单页减库存。
- 方案三:提交订单时减库存。
- 方案四:支付时减库存。
分析:sql
- 方案一:在这个时间内加入购物车并不表明用户必定会购买,若是这个时候处理库存,会致使想购买的用户显示无货。而不想购买的人一直占着库存。显然这种作法是不可取的。惟品会购物车锁库存,可是他们是另外一种作法,加入购物车后会有必定时效,超时会从购物车清除。
- 方案二:确认订单页用户有购买欲望,可是此时没有提交订单,减库存会增长很大的复杂性,并且确认订单页的功能是让用户确认信息,减库存不合理,但愿你们对该方案发表一下观点,本人暂时只想到这么多。
- 方案三:提交订单时减库存。用户选择提交订单,说明用户有强烈的购买欲望。生成订单会有一个支付时效,例如半个小时。超过半个小时后,系统自动取消订单,还库存。
- 方案四:支付时去减库存。好比:只有100个用户能够支付,900个用户不能支付。用户体验太差,同时生成了900个无效订单数据。
因此综上所述:
选择方案三比较合理。
数据库
重复下单问题缓存
- 用户点击过快,重复提交。
- 网络延时,用户重复提交。
- 网络延时高的状况下某些框架自动重试,致使重复请求。
- 用户恶意行为。
解决办法
-
前端拦截,点击后按钮置灰。安全
- 后台:
(1)redis 防重复点击,在下单前获取用户token,下单的时候后台系统校验这个 token是否有效,致使的问题是一个用户多个设备不能同时下单。
//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 (redis.get(blackUser) != null) { return false; } String keyCount = "shop-oms-submit-count-" + token; Long nowSecond = LocalDateTime.now().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.valueOf(secondString.get(0)); //now > oldSecond + 60 用户可下单 boolean result = nowSecond.compareTo(oldSecond + 60) > 0; if (!result) { //触发限制,加入黑名单,过时时间10分钟 redis.set(blackUser, String.valueOf(nowSecond), 10 * 60); } return result; }
如何安全的减库存
多用户抢购时,如何作到并发安全减库存?
- 方案1: 数据库操做商品库存采用乐观锁防止超卖:
sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;
分析:
高并发场景下,假设库存只有 1件 ,两个请求同时进来,抢购该商品.
数据库层面会限制只有一个用户扣库存成功。在并发量不是很大的状况下能够这么作。可是若是是秒杀,抢购,瞬时流量很高的话,压力会都到数据库,可能拖垮数据库。
- 方案2:利用Redis单线程 强制串行处理
/** * 缺点并发不高,同时只能一个用户抢占操做,用户体验很差! * * @param orderSkuAo */ public boolean subtractStock(OrderSkuAo orderSkuAo) { String lockKey = "shop-product-stock-subtract" + orderSkuAo.getOrderCode(); if(redis.get(lockKey)){ return false; } try { lock.lock(lockKey, 1L, 10L); //处理逻辑 }catch (Exception e){ LogUtil.error("e=",e); }finally { lock.unLock(lockKey); } return true; }
分析:
利用Redis 分布式锁,强制控制同一个商品处理请求串行化,缺点并发不高 ,处理比较慢,不适合抢购,高并发场景。用户体验差,可是减轻了数据库的压力。
- 方案3 :redis + mq + mysql 保证库存安全,知足高并发处理,但相对复杂。
/** * 扣库存操做,秒杀的处理方案 * @param orderCode * @param skuCode * @param num * @return */ public boolean subtractStock(String orderCode,String skuCode, Integer num) { String key = "shop-product-stock" + skuCode; Object value = redis.get(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)修改 + 补偿机制
写在最后的话
你们看完有什么不懂的能够在下方留言讨论,也能够私信问我通常看到后我都会回复的。也欢迎你们关注个人公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。最后以为文章对你有帮助的话记得点个赞哦,点点关注不迷路,天天都有新鲜的干货分享!