• 售前

  • 售后

热门帖子
入门百科

京东一面:Redis 怎样实现库存扣减操纵?怎样防止商品被超卖?

[复制链接]
AriesHun 显示全部楼层 发表于 2022-8-3 10:01:35 |阅读模式 打印 上一主题 下一主题

在一样平常开发中有很多地方都有类似扣减库存的操纵,好比电商体系中的商品库存,抽奖体系中的奖品库存等。
办理方案


  • 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。
  • 还是使用数据库,但是将库存分层多份存到多条记录内里,扣减库存的时间路由一下,如许子增大了并发量,但是还是制止不了大量的去访问数据库来更新库存。
  • 将库存放到redis使用redis的incrby特性来扣减库存。
分析

在上面的第一种和第二种方式都是基于数据来扣减库存。
基于数据库单库存

第一种方式在全部哀求都会在这里期待锁,获取锁有去扣减库存。在并发量不高的环境下可以使用,但是一旦并发量大了就会有大量哀求壅闭在这里,导致哀求超时,进而整个体系雪崩;而且会频仍的去访问数据库,大量占用数据库资源,以是在并发高的环境下这种方式不实用。
基于数据库多库存

第二种方式着实是第一种方式的优化版本,在肯定水平上进步了并发量,但是在还是会大量的对数据库做更新操纵大量占用数据库资源。
基于数据库来实现扣减库存还存在的一些标题:
用数据库扣减库存的方式,扣减库存的操纵必须在一条语句中实行,不能先selec在update,如许在并发下会出现超扣的环境。如:
  1. update number set x=x-1 where x > 0
复制代码
MySQL自身对于高并发的处置惩罚性能就会出现标题,一样平常来说,MySQL的处置惩罚性能会随着并发thread上升而上升,但是到了肯定的并发度之后会出现显着的拐点,之后一起降落,终极乃至会比单thread的性能还要差。
当减库存和高并发遇到一起的时间,由于操纵的库存数目在同一行,就会出现争抢InnoDB行锁的标题,导致出现相互期待乃至死锁,从而大大低落MySQL的处置惩罚性能,终极导致前端页面出现超时非常。
基于redis

针对上述标题的标题我们就有了第三种方案,将库存放到缓存,使用redis的incrby特性来扣减库存,办理了超扣和性能标题。
但是一旦缓存丢失必要思量规复方案。好比抽奖体系扣奖品库存的时间,初始库存=总的库存数-已经发放的嘉奖数,但是如果是异步发奖,必要比及MQ消息消耗完了才华重启redis初始化库存,否则也存在库存不同等的标题。
基于redis实现扣减库存的详细实现



  • 我们使用redis的lua脚原来实现扣减库存
  • 由于是分布式环境下以是还必要一个分布式锁来控制只能有一个服务去初始化库存
  • 必要提供一个回调函数,在初始化库存的时间去调用这个函数获取初始化库存
初始化库存回调函数(IStockCallback )

  1. /**
  2.  * 获取库存回调
  3.  * @author yuhao.wang
  4.  */
  5. public interface IStockCallback {
  6.     /**
  7.      * 获取库存
  8.      * @return
  9.      */
  10.     int getStock();
  11. }
复制代码
扣减库存服务(StockService)

  1. /**
  2.  * 扣库存
  3.  *
  4.  * @author yuhao.wang
  5.  */
  6. @Service
  7. public class StockService {
  8.     Logger logger = LoggerFactory.getLogger(StockService.class);
  9.     /**
  10.      * 不限库存
  11.      */
  12.     public static final long UNINITIALIZED_STOCK = -3L;
  13.     /**
  14.      * Redis 客户端
  15.      */
  16.     @Autowired
  17.     private RedisTemplate<String, Object> redisTemplate;
  18.     /**
  19.      * 执行扣库存的脚本
  20.      */
  21.     public static final String STOCK_LUA;
  22.     static {
  23.         /**
  24.          *
  25.          * @desc 扣减库存Lua脚本
  26.          * 库存(stock)-1:表示不限库存
  27.          * 库存(stock)0:表示没有库存
  28.          * 库存(stock)大于0:表示剩余库存
  29.          *
  30.          * @params 库存key
  31.          * @return
  32.          *   -3:库存未初始化
  33.          *   -2:库存不足
  34.          *   -1:不限库存
  35.          *   大于等于0:剩余库存(扣减之后剩余的库存)
  36.          *      redis缓存的库存(value)是-1表示不限库存,直接返回1
  37.          */
  38.         StringBuilder sb = new StringBuilder();
  39.         sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
  40.         sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
  41.         sb.append("    local num = tonumber(ARGV[1]);");
  42.         sb.append("    if (stock == -1) then");
  43.         sb.append("        return -1;");
  44.         sb.append("    end;");
  45.         sb.append("    if (stock >= num) then");
  46.         sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
  47.         sb.append("    end;");
  48.         sb.append("    return -2;");
  49.         sb.append("end;");
  50.         sb.append("return -3;");
  51.         STOCK_LUA = sb.toString();
  52.     }
  53.     /**
  54.      * @param key           库存key
  55.      * @param expire        库存有效时间,单位秒
  56.      * @param num           扣减数量
  57.      * @param stockCallback 初始化库存回调函数
  58.      * @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存
  59.      */
  60.     public long stock(String key, long expire, int num, IStockCallback stockCallback) {
  61.         long stock = stock(key, num);
  62.         // 初始化库存
  63.         if (stock == UNINITIALIZED_STOCK) {
  64.             RedisLock redisLock = new RedisLock(redisTemplate, key);
  65.             try {
  66.                 // 获取锁
  67.                 if (redisLock.tryLock()) {
  68.                     // 双重验证,避免并发时重复回源到数据库
  69.                     stock = stock(key, num);
  70.                     if (stock == UNINITIALIZED_STOCK) {
  71.                         // 获取初始化库存
  72.                         final int initStock = stockCallback.getStock();
  73.                         // 将库存设置到redis
  74.                         redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
  75.                         // 调一次扣库存的操作
  76.                         stock = stock(key, num);
  77.                     }
  78.                 }
  79.             } catch (Exception e) {
  80.                 logger.error(e.getMessage(), e);
  81.             } finally {
  82.                 redisLock.unlock();
  83.             }
  84.         }
  85.         return stock;
  86.     }
  87.     /**
  88.      * 加库存(还原库存)
  89.      *
  90.      * @param key    库存key
  91.      * @param num    库存数量
  92.      * @return
  93.      */
  94.     public long addStock(String key, int num) {
  95.         return addStock(key, null, num);
  96.     }
  97.     /**
  98.      * 加库存
  99.      *
  100.      * @param key    库存key
  101.      * @param expire 过期时间(秒)
  102.      * @param num    库存数量
  103.      * @return
  104.      */
  105.     public long addStock(String key, Long expire, int num) {
  106.         boolean hasKey = redisTemplate.hasKey(key);
  107.         // 判断key是否存在,存在就直接更新
  108.         if (hasKey) {
  109.             return redisTemplate.opsForValue().increment(key, num);
  110.         }
  111.         Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null");
  112.         RedisLock redisLock = new RedisLock(redisTemplate, key);
  113.         try {
  114.             if (redisLock.tryLock()) {
  115.                 // 获取到锁后再次判断一下是否有key
  116.                 hasKey = redisTemplate.hasKey(key);
  117.                 if (!hasKey) {
  118.                     // 初始化库存
  119.                     redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);
  120.                 }
  121.             }
  122.         } catch (Exception e) {
  123.             logger.error(e.getMessage(), e);
  124.         } finally {
  125.             redisLock.unlock();
  126.         }
  127.         return num;
  128.     }
  129.     /**
  130.      * 获取库存
  131.      *
  132.      * @param key 库存key
  133.      * @return -1:不限库存; 大于等于0:剩余库存
  134.      */
  135.     public int getStock(String key) {
  136.         Integer stock = (Integer) redisTemplate.opsForValue().get(key);
  137.         return stock == null ? -1 : stock;
  138.     }
  139.     /**
  140.      * 扣库存
  141.      *
  142.      * @param key 库存key
  143.      * @param num 扣减库存数量
  144.      * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
  145.      */
  146.     private Long stock(String key, int num) {
  147.         // 脚本里的KEYS参数
  148.         List<String> keys = new ArrayList<>();
  149.         keys.add(key);
  150.         // 脚本里的ARGV参数
  151.         List<String> args = new ArrayList<>();
  152.         args.add(Integer.toString(num));
  153.         long result = redisTemplate.execute(new RedisCallback<Long>() {
  154.             @Override
  155.             public Long doInRedis(RedisConnection connection) throws DataAccessException {
  156.                 Object nativeConnection = connection.getNativeConnection();
  157.                 // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
  158.                 // 集群模式
  159.                 if (nativeConnection instanceof JedisCluster) {
  160.                     return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
  161.                 }
  162.                 // 单机模式
  163.                 else if (nativeConnection instanceof Jedis) {
  164.                     return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
  165.                 }
  166.                 return UNINITIALIZED_STOCK;
  167.             }
  168.         });
  169.         return result;
  170.     }
  171. }
复制代码
调用

  1. /**
  2.  * @author yuhao.wang
  3.  */
  4. @RestController
  5. public class StockController {
  6.     @Autowired
  7.     private StockService stockService;
  8.     @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
  9.     public Object stock() {
  10.         // 商品ID
  11.         long commodityId = 1;
  12.         // 库存ID
  13.         String redisKey = "redis_key:stock:" + commodityId;
  14.         long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId));
  15.         return stock >= 0;
  16.     }
  17.     /**
  18.      * 获取初始的库存
  19.      *
  20.      * @return
  21.      */
  22.     private int initStock(long commodityId) {
  23.         // TODO 这里做一些初始化库存的操作
  24.         return 1000;
  25.     }
  26.     @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
  27.     public Object getStock() {
  28.         // 商品ID
  29.         long commodityId = 1;
  30.         // 库存ID
  31.         String redisKey = "redis_key:stock:" + commodityId;
  32.         return stockService.getStock(redisKey);
  33.     }
  34.     @RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
  35.     public Object addStock() {
  36.         // 商品ID
  37.         long commodityId = 2;
  38.         // 库存ID
  39.         String redisKey = "redis_key:stock:" + commodityId;
  40.         return stockService.addStock(redisKey, 2);
  41.     }
  42. }
复制代码
 

 
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作