当前位置:   article > 正文

商城系统学习总结(1)——订单与库存在高并发场景下案例解析_订单的并发场景

订单的并发场景

一、 问题

一件商品只有100个库存,现在有1000或者更多的用户来购买,每个用户计划同时购买1个到几个不等商品。如何保证库存在高并发的场景下是安全的?

(1)不多发

(2)不少发

二、 下单的步骤

(1)下单

(2)下单同时预占库存

(3)支付

(4)支付成功真正减扣库存

(5)取消订单

(6)回退预占库存

三、 什么时候进行预占库存?

(1)方案一: 加入购物车的时候去预占库存

(2)方案二: 下单的时候去预占库存

(3)方案三: 支付的时候去预占库存

四、 分析

(1)方案一 :加入购物车并不代表用户一定会购买,如果这个时候开始预占库存,会导致想购买的无法加入购物车。而不想购买的人一直占用库存。显然这种做法是不可取的。

(2)方案二 :商品加入购物车后,选择下单,这个时候去预占库存。用户选择去支付说明了,用户购买欲望是比 方案一 要强烈的。订单也有一个时效,例如半个小时。超过半个小时后,系统自动取消订单,回退预占库存。

(3)方案三 :下单成功去支付的时候去预占库存。只有100个用户能支付成功,900个用户支付失败。用户体验不好,就像你走了一条光明大道,一路通畅,突然被告知此处不通行。而且支付流程也是一个比较复杂的流程,如果和减库存放在一起,将会变的更复杂。

所以综上所述: 选择方案二比较合理。

五、 重复下单问题

(1)用户点击过快,重复提交两次

(2)网络延时,用户刷新或者点击下单重复提交

(3)网络框架重复请求,某些网络框架,在延时比较高的情况下会自动重复请求

(4)用户恶意行为

六、 解决办法

1、在UI拦截,点击后按钮置灰,不能继续点击,防止用户,连续点击造成的重复下单。

下单前获取一个下单的唯一token,下单的时候需要这个token。后台系统校验这个 token是否有效,才继续进行下单操作。

  1. /**
  2. * 先生成 token 保存到 Redis
  3. * token 作为 key , 并设置过期时间 时间长度 根据任务需求
  4. * value 为数字 自增判断 是否使用过 *
  5. * @param user
  6. * @return
  7. */
  8. public String createToken(User user) {
  9. String key = "placeOrder:token:" + user.getId();
  10. String token = UUID.randomUUID().toString();
  11. //保存到Redis
  12. redisService.set(key + token, 0, 1000L);
  13. return token;
  14. }
  15. /**
  16. * 校验下单的token是否有效
  17. * @param user
  18. * @param token
  19. * @return
  20. */
  21. public Boolean checkToken(User user, String token) {
  22. String key = "placeOrder:token:" + user.getId();
  23. if (null != redisService.get(key + token)) {
  24. long times = redisService.increment(key + token, 1);
  25. if (times == 1) {
  26. //利用increment 原子性 判断是否 该token 是否使用
  27. return true;
  28. } else {
  29. // 已经使用过了
  30. }
  31. //删除
  32. redisService.remove(key + token);
  33. }
  34. return false;
  35. }

2、如何安全的减扣库存?

同一个用户或者多个用户同时抢购一个商品的时候,我们如何做到并发安全减扣库存?

(1)数据库操作商品库存

  1. publicinterfaceProductDaoextendsJpaRepository<Product,Integer>{
  2. /**
  3. * @param pid 商品ID
  4. * @param num 购买数量
  5. * @return
  6. */
  7. @Transactional
  8. @Modifying
  9. @Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1")
  10. intreduceStock1(Integerpid, Integernum);
  11. /**
  12. * @param pid 商品ID
  13. * @param num 购买数量
  14. * @return
  15. */
  16. @Transactional
  17. @Modifying
  18. @Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1 and availableNum - ?2 >= 0")
  19. intreduceStock2(Integerpid, Integernum);
  20. }

(2)下单

  1. /**
  2. * 下单操作1
  3. * @param req
  4. */
  5. private int place(PlaceOrderReq req) {
  6. User user = userDao.findOne(req.getUserId());
  7. Product product = productDao.findOne(req.getProductId());
  8. //下单数量
  9. Integer num = req.getNum();
  10. //可用库存
  11. Integer availableNum = product.getAvailableNum();
  12. //可用预定
  13. if (availableNum >= num) {
  14. //减库存
  15. int count = productDao.reduceStock1(product.getId(), num);
  16. if (count == 1) {
  17. //生成订单
  18. createOrders(user, product, num);
  19. } else {
  20. logger.info("库存不足 3");
  21. }
  22. return 1;
  23. } else {
  24. logger.info("库存不足 4");
  25. return -1;
  26. }
  27. }
  28. /**
  29. * 下单操作2
  30. * @param req
  31. */
  32. private int place2(PlaceOrderReq req) {
  33. User user = userDao.findOne(req.getUserId());
  34. Product product = productDao.findOne(req.getProductId());
  35. //下单数量
  36. Integer num = req.getNum();
  37. //可用库存
  38. Integer availableNum = product.getAvailableNum();
  39. //可用预定
  40. if (availableNum >= num) {
  41. //减库存
  42. int count = productDao.reduceStock2(product.getId(), num);
  43. if (count == 1) {
  44. //生成订单
  45. createOrders(user, product, num);
  46. } else {
  47. logger.info("库存不足 3");
  48. }
  49. return 1;
  50. } else {
  51. logger.info("库存不足 4");
  52. return -1;
  53. }
  54. }

方法1: 不考虑库存安全的写法

  1. /**
  2. * 方法 1
  3. * 减可用
  4. * 加预占
  5. * 库存数据不安全
  6. * @param req
  7. */
  8. @Override
  9. @Transactional
  10. public void placeOrder(PlaceOrderReq req) {
  11. place1(req);
  12. }

分析: 在高并的场景下,假设库存只有 2 件 ,两个请求同时进来,抢购改商品,购买数量都是 2. A请求 此时去获取库存,发现库存刚好足够,执行扣库存下单操作。 在 A 请求为完成的时候(事务未提交),B请求 此时也去获取库存,发现库存还有2. 此时也去执行扣库存,下单操作。库存剩 2 ,但是卖出了 4 。最终数据库库存数量将变为 -2 ,所以库存是不安全的。

方法2 :这个操作可以保证库存数据是安全的

  1. /**
  2. * 方法 2
  3. * 减可用
  4. * 加预占
  5. * 库存数据不安全
  6. * @param req
  7. */
  8. @Override
  9. @Transactional
  10. public void placeOrder(PlaceOrderReq req) {
  11. place2(req);
  12. }

分析: 在方法1 的基础上 ,更新库存的语句,增加了可用库存数量 大于 0, availableNum - num >= 0 ;实质是使用了数据库的乐观锁来控制库存安全,在并发量不是很大的情况下可以这么做。但是如果是秒杀,抢购,瞬时流量很高的话,压力会都到数据库,可能拖垮数据库。

方法3 :该方法也可以保证库存数量安全

  1. /**
  2. * 方法 3
  3. * 采用Redis锁同一时间只能一个请求修改同一个商品的数量
  4. * 缺点并发不高,同时只能一个用户抢占操作,用户体验不好!
  5. * @param req
  6. */
  7. @Override
  8. public void placeOrder2(PlaceOrderReq req) {
  9. String lockKey = "placeOrder:" + req.getProductId();
  10. Boolean isLock = redisService.lock(lockKey);
  11. if (!isLock) {
  12. logger.info("系统繁忙稍后再试!");
  13. return 2;
  14. }
  15. //place2(req); place1(req);
  16. //这两个方法都可以
  17. redisService.unLock(lockKey);
  18. }

分析: 利用Redis 分布式锁, 强制控制 同一个商品,同时只能一个请求处理下单。 其他请求返回 ‘系统繁忙稍后再试!’; 强制把处理请求串行化,缺点并发不高 ,处理比较慢,不适合抢购等方案 。 用户体验也不好,明明看到库存是充足的,就是强不到。 相比方案2减轻了数据库的压力。

方法4 :可以保证库存安全,满足高并发处理,但是相对复杂一点

  1. /**
  2. * 方法 4
  3. * 商品的数量等其他信息先保存到Redis
  4. * 检查库存与减少库存不是原子性,以increment > 0为准
  5. * @param req
  6. */
  7. @Override
  8. public void placeOrder3(PlaceOrderReq req) {
  9. String key = "product:" + req.getProductId();
  10. // 先检查 库存是否充足
  11. Integer num = (Integer) redisService.get(key);
  12. if (num < req.getNum()) {
  13. logger.info("库存不足 1");
  14. } else{
  15. //不可在这里下单减库存,否则导致数据不安全, 情况类似 方法1;
  16. }
  17. //减少库存
  18. long value = redisService.increment(key, -req.getNum().longValue());
  19. //库存充足
  20. if (value >= 0) {
  21. logger.info("成功抢购 ! ");
  22. //TODO 真正减 扣 库存 等操作 下单等操作 ,这些操作可用通过 MQ 或 其他方式
  23. place2(req);
  24. } else {
  25. //库存不足,需要增加刚刚减去的库存
  26. redisService.increment(key, req.getNum().longValue());
  27. logger.info("库存不足 2 ");
  28. }
  29. }

分析 : 利用Redis increment 的原子操作,保证库存安全。 事先需要把库存的数量等其他信息保存到Redis,并保证更新库存的时候,更新Redis。 进来的时候 先 get 库存数量是否充足,再执行 increment。以 increment > 0 为准。 检查库存 与 减少库存 不是原子性的。 检查库存的时候技术库存充足也不可下单;否则造成库存不安全,原来类似 方法1. increment 是个原子操作,已这个为准。redisService.increment(key, -req.getNum().longValue()) >= 0 说明库存充足,可以下单。redisService.increment(key, -req.getNum().longValue()) < 0 的时候 不能下单,此时库存不足。并且需要 回加刚刚减去的库存数量,否则会导致刚才减扣的数量 一直买不出去。数据库与缓存的库存不一致。此方法可以满足高并抢购等一些方案,真正减扣库存和下单可以异步执行。订单时效问题,订单取消等为保证商家利益,同时把商品卖给有需要的人,订单下单成功后,往往会有个有效时间。超过这个时间,订单取消,库存回滚。订单取消后,可利用MQ 回退库存等。

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/502721
推荐阅读
相关标签
  

闽ICP备14008679号