2019独角兽企业重金招聘Python工程师标准>>>

在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。

解决方案

  1. 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。
  2. 还是使用数据库,但是将库存分层多份存到多条记录里面,扣减库存的时候路由一下,这样子增大了并发量,但是还是避免不了大量的去访问数据库来更新库存。
  3. 将库存放到redis使用redis的incrby特性来扣减库存。

正常的操作是:
扣库存->成功->下单->成功
扣库存->成功->下单->失败->回滚库存
扣库存->失败->下单失败

分析

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

基于数据库单库存

第一种方式在所有请求都会在这里等待锁,获取锁有去扣减库存。在并发量不高的情况下可以使用,但是一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源,所以在并发高的情况下这种方式不适用。

基于数据库多库存

第二种方式其实是第一种方式的优化版本,在一定程度上提高了并发量,但是在还是会大量的对数据库做更新操作大量占用数据库资源。

基于数据库来实现扣减库存还存在的一些问题:

  • 用数据库扣减库存的方式,扣减库存的操作必须在一条语句中执行,不能先selec在update,这样在并发下会出现超扣的情况。如:
update number set x=x-1 where x > 0
  • MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。

  • 当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。

基于redis

针对上述问题的问题我们就有了第三种方案,将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。

基于redis实现扣减库存的具体实现

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

初始化库存回调函数(IStockCallback )

/*** 获取库存回调* @author yuhao.wang*/
public interface IStockCallback {/*** 获取库存* @return*/int getStock();
}

扣减库存服务(StockService)

/*** 扣库存** @author yuhao.wang*/
@Service
public class StockService {Logger logger = LoggerFactory.getLogger(StockService.class);/*** 不限库存*/public static final long UNINITIALIZED_STOCK = -3L;/*** Redis 客户端*/@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 执行扣库存的脚本*/public static final String STOCK_LUA;static {/**** @desc 扣减库存Lua脚本* 库存(stock)-1:表示不限库存* 库存(stock)0:表示没有库存* 库存(stock)大于0:表示剩余库存** @params 库存key* @return*       -3:库存未初始化*      -2:库存不足*        -1:不限库存*        大于等于0:剩余库存(扣减之后剩余的库存)*        redis缓存的库存(value)是-1表示不限库存,直接返回1*/StringBuilder sb = new StringBuilder();sb.append("if (redis.call('exists', KEYS[1]) == 1) then");sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");sb.append("    local num = tonumber(ARGV[1]);");sb.append("    if (stock == -1) then");sb.append("        return -1;");sb.append("    end;");sb.append("    if (stock >= num) then");sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");sb.append("    end;");sb.append("    return -2;");sb.append("end;");sb.append("return -3;");STOCK_LUA = sb.toString();}/*** @param key           库存key* @param expire        库存有效时间,单位秒* @param num           扣减数量* @param stockCallback 初始化库存回调函数* @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存*/public long stock(String key, long expire, int num, IStockCallback stockCallback) {long stock = stock(key, num);// 初始化库存if (stock == UNINITIALIZED_STOCK) {RedisLock redisLock = new RedisLock(redisTemplate, key);try {// 获取锁if (redisLock.tryLock()) {// 双重验证,避免并发时重复回源到数据库stock = stock(key, num);if (stock == UNINITIALIZED_STOCK) {// 获取初始化库存final int initStock = stockCallback.getStock();// 将库存设置到redisredisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);// 调一次扣库存的操作stock = stock(key, num);}}} catch (Exception e) {logger.error(e.getMessage(), e);} finally {redisLock.unlock();}}return stock;}/*** 加库存(还原库存)** @param key    库存key* @param num    库存数量* @return*/public long addStock(String key, int num) {return addStock(key, null, num);}/*** 加库存** @param key    库存key* @param expire 过期时间(秒)* @param num    库存数量* @return*/public long addStock(String key, Long expire, int num) {boolean hasKey = redisTemplate.hasKey(key);// 判断key是否存在,存在就直接更新if (hasKey) {return redisTemplate.opsForValue().increment(key, num);}Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null");RedisLock redisLock = new RedisLock(redisTemplate, key);try {if (redisLock.tryLock()) {// 获取到锁后再次判断一下是否有keyhasKey = redisTemplate.hasKey(key);if (!hasKey) {// 初始化库存redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);}}} catch (Exception e) {logger.error(e.getMessage(), e);} finally {redisLock.unlock();}return num;}/*** 获取库存** @param key 库存key* @return -1:不限库存; 大于等于0:剩余库存*/public int getStock(String key) {Integer stock = (Integer) redisTemplate.opsForValue().get(key);return stock == null ? -1 : stock;}/*** 扣库存** @param key 库存key* @param num 扣减库存数量* @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】*/private Long stock(String key, int num) {// 脚本里的KEYS参数List<String> keys = new ArrayList<>();keys.add(key);// 脚本里的ARGV参数List<String> args = new ArrayList<>();args.add(Integer.toString(num));long result = redisTemplate.execute(new RedisCallback<Long>() {@Overridepublic Long doInRedis(RedisConnection connection) throws DataAccessException {Object nativeConnection = connection.getNativeConnection();// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行// 集群模式if (nativeConnection instanceof JedisCluster) {return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);}// 单机模式else if (nativeConnection instanceof Jedis) {return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);}return UNINITIALIZED_STOCK;}});return result;}}

调用

/*** @author yuhao.wang*/
@RestController
public class StockController {@Autowiredprivate StockService stockService;@RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object stock() {// 商品IDlong commodityId = 1;// 库存IDString redisKey = "redis_key:stock:" + commodityId;long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId));return stock >= 0;}/*** 获取初始的库存** @return*/private int initStock(long commodityId) {// TODO 这里做一些初始化库存的操作return 1000;}@RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object getStock() {// 商品IDlong commodityId = 1;// 库存IDString redisKey = "redis_key:stock:" + commodityId;return stockService.getStock(redisKey);}@RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object addStock() {// 商品IDlong commodityId = 2;// 库存IDString redisKey = "redis_key:stock:" + commodityId;return stockService.addStock(redisKey, 2);}
}

源码: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-stock-redis 工程

参考:

  • http://www.cnblogs.com/billyxp/p/3701124.html
  • http://blog.csdn.net/jiao_fuyou/article/details/15504777
  • https://www.jianshu.com/p/48c1a92fbf3a
  • https://www.zhihu.com/question/268937734

转载于:https://my.oschina.net/xiaominmin/blog/3060257

基于redis实现的扣减库存相关推荐

  1. 电商库存设计mysql redis_基于redis实现的扣减库存

    在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等. 解决方案 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段. 还是使用数据库, ...

  2. synchronized、Lock和 redis锁,基于redis实现的扣减库存锁(附代码)

    目录 锁的概念 公平锁 可中断锁 可重入锁 几种加锁方式 synchronized Lock Lock接口的6个方法: Lock的实现类 ReentrantLock 可重入锁 ReadWriteLoc ...

  3. 扣减库存,redis你值得拥有

    扯扯犊子 并发知识点总结: 1.秒杀,物理业务隔离,抽成单独的服务器 2.接口设计,防止洪流访问数据库. 3.加redis缓存. 4.缓存雪崩,一个请求,多个key同时失效,避免同时失效.多个请求,一 ...

  4. 浅析「扣减库存」的方案设计

    今天我们来探讨下扣减库存的方案. 生活中,我们总是用各种电商 APP 抢购商品,但是库存数是很少的,特别是秒杀场景,商品可能就一件,那如何保证不会出现超卖的情况呢? 一.扣减库存的三种方案 1.1 下 ...

  5. 电商扣减库存_电商系统秒杀架构设计

    作者:曹林华 https://blog.51cto.com/13527416/2085258 前言 最近在部门内部分享了原来在电商业务做秒杀活动的整体思路,大家对这次分享反馈还不错,所以我就简单整理了 ...

  6. 电商扣减库存_经验分享:电商库存体系设计笔记

    最近在做仓库库存管理相关的项目,清晰地了解了仓库是如何管理库存的,并且整清楚了各个系统的库存是如何交互的,整理了下分享给大家. 库存是什么 这里是百度百科给出的解释: "库存(invento ...

  7. java 电商锁库存实现_电商项目扣减库存方案

    阿里巴巴b2b电商算法实战电子商务 85.3元 包邮 (需用券) 去购买 > 各位小宝贝们,大家是不是在面试过程中经常被问到,你电商项目扣减库存时,到底是下单减库存呢?还是付款减库存? 那今天给 ...

  8. 电商扣减库存_电商仓库管理的难点与解决方案

    在我国,目前有很多小型电商企业,其仓库面积都在1000平以内,由于前期对营销的专注,对仓库的忽视,包括对设施设备的投入,导致了今天仓库工作效率低下,库存混乱,运作成本高等诸多问题,以下我们来细细诉说. ...

  9. 高并发下 如何安全、高效扣减库存? 有更好的方案?

    参考文档 干货分享:五分钟教你解决高并发场景下的订单和库存处理方案 聊聊高并发下库存加减那些事儿--"如何实现异步扣减库存" Redis如何实现高并发分布式锁? 如何利用Redis ...

最新文章

  1. 腾讯 AI Lab 开源业内最大规模多标签图像数据集
  2. 产品经理如何评估产品机会
  3. 【Flutter】Flutter 页面生命周期 ( 初始化期 | createState | initState | 更新期 | build | 销毁期 | dispose)
  4. 图解win7下ping命令使用
  5. 【学习笔记】分布式Tensorflow
  6. CAN总线-位时序、波特率、采样点
  7. VTK:Utilities之DetermineActorType
  8. 信奥中的数学:前缀和与差分、大整数开方技巧
  9. Android实现号码归属地查询
  10. Java设计模式学习总结(7)——结构型模式之适配器模式
  11. 中文命名之Hibernate 5演示 - 使用注解(annotation)而非xml定义映射
  12. 项目实现思路(不断更新)
  13. java美图秀秀,【美图秀秀和Java手机游戏模拟器哪个好用】美图秀秀和Java手机游戏模拟器对比-ZOL下载...
  14. 如何用计算机解锁苹果手机,教你怎么使用Apple Watch手表解锁苹果Mac电脑
  15. 怎么选择boost升压电路的电感?只要三个公式
  16. 客户端无法远程连接服务器的问题
  17. 混淆矩阵 matlab代码示例
  18. Java doc或docx转pdf文件预览
  19. 《5G应用“扬帆”行动计划(2021-2023年)》征求意见稿发布
  20. docker容器技术之虚拟化网络概述(四)

热门文章

  1. ORACLE将查询字段指定为某种类型
  2. 问题之sqlyou的使用
  3. vue路由匹配实现包容性_包容性设计:面向老年用户的数字平等
  4. 隐马尔可夫模型(HMM)及Viterbi算法
  5. WPF 实现 DataGrid/ListView 分页控件
  6. bootstrap评分插件 Bootstrap Star Rating Examples
  7. 微软2014编程之美初赛第一场——题目3 : 活动中心
  8. 8.2设备文件及磁盘分区
  9. Swift - 自定义单元格实现微信聊天界面
  10. MVC 扩展方法特点