场景:
a. 电商中 商品下单后扣减库存
b. 七夕等节日抽奖活动中 奖品库存扣减
c. 项目或者活动场次预订或者下单扣减场次库存
d. 秒杀场景中 库存扣减以及避免超卖
e. 淘宝付费扣减库存等
f. 锦鲤活动促销的 扣减库存的场景。
如何设计扣减库存呢?
-
如果您是单机单体应用?
则可以采用如下手段
1.1 代码同步, 例如使用 synchronized ,lock 等同步方法
这个方案不可行,主要是因为以下几点:
1).synchronized 作用范围是单个jvm实例, 如果做了集群,分布式等,就没用了,因为无法跨jvm.
2).synchronized是作用在对象实例上的,如果不是单例,则多个实例间不会同步(这个一般用spring管理bean,默认就是单例)
3).单个jvm时,synchronized也不能保证多个数据库事务的隔离性. 这与代码中的事务传播级别,数据库的事务隔离级别,加锁时机等相关.
3-1).先说隔离级别,常用的是 Read Committed 和 Repeatable Read ,另外2种不常用就不说了
3-1-1)RR(Repeatable Read)级别.mysql默认的是RR,事务开启后,不会读取到其他事务提交的数据。
该方案:可以在用户数较小并且并发交易量不大的情况下使用。
1.2.乐观锁 一般的设计中CAS会使用version来控制.
update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;
这样,每次更新version在原基础上+1,就可以了.
使用CAS要注意几点,
1)失败重试次数,是否需要限制
2)失败重试对用户是透明的
1.3.悲观锁 使用数据库锁, select xx for update
主要控制的是事务串行化,以解决了数据一致性问题.
利用数据库行锁先进性锁定这条记录
SELECT col1... from product where ID =1 for update;
再进行操作
UPDATE product set product_stock = product_stock- 1;
利用排它锁将并行转化为串行操作
(啥是并行?同时做几件事情,比如explain sql ,再比如批量处理sql 都属于并行操作。使用多CPU和磁盘来快速响应查询(queries)是并行技术的目的。但是,多个用户同时执行并行操作,CPU、内存及磁盘等资源会被快速耗尽。串行 按照一定的顺序规则进行 执行。,主要保证事务的串行化。)
该方案可以在用户数和交易数比较小的公司可以采用,具体用户体验性能较差,还会出现
a. 并发量剧增带来的用户体验性能低下
b. 串行设计不合理同样导致死锁问题的发生
c. 一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源
d. 可能出现超卖的情况
举个例子:
mysql默认是可重复读,甲乙两个请求均下单了,事务均未提交,甲提交修改库存1->0, 但是乙读到还是1,此时提交修改库存1->0. 这就属于超卖现象。上述说的串行化,并不是将事务隔离级别设置为串行化,而是使用排它锁串行化,此时并发量上来,将会出现超卖现象。
2. 如果您是分布式架构或者高并发环境
那么建议您采用如下手段:
流程图:
查询库存-->查看redis中是否包含
-->包含 则在redis中扣减库存
-->不包含则 插入数据库进行扣减 并且同步至redis缓存中
2.1 手段一:分布式锁机制
基于redis
利用redis 实现分布式锁,
- 使用setnx命令(在key不存在时,创建并设置value 返回1,key存在时,会反回0)来获取锁
- 考虑宕机情况,增加超时失效释放锁机制
- 非超时机制中,增加释放锁的线程码,确保锁只能有锁的线程进行释放。
- Long TIMEOUT_SECOUND = 120000L;
- String featureCode = "machine01";
- Jedis client = jedisPool.getResource();
- while(client.setnx("lock",featureCode+":"+String.valueOf(System.currentTimeMillis())) == 0){
- Long lockTime = Long.valueOf(client.get("lock").substring(9));
- if (lockTime!=null && System.currentTimeMillis() > lockTime+TIMEOUT_SECOUND) {
- client.del("lock");
- }
- Thread.sleep(10000);
- }
- // to do sth
- if (featureCode.equals(client.get("lock").substring(0, 8))) {
- client.del("lock");
- }
2.2 手段二:将库存放到redis使用redis的incrby特性来扣减库存。
将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。
但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。
2.3 手段三:基于redis-lua脚本实现扣减库存的具体实现
- 我们使用redis的lua脚本来实现扣减库存
- 由于是分布式环境下所以还需要一个分布式锁来控制只能有一个服务去初始化库存
- 需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存
初始化库存回调函数(IStockCallback )
- 获取库存回调
- public interface IStockCallback {
- * 获取库
- int getStock();
- }
- 扣减库存服务(StockService)
- public class StockService {
- Logger logger = LoggerFactory.getLogger(StockService.class);
- * 库存不足
- public static final int LOW_STOCK = 0;
- * 不限库
- public static final long UNINITIALIZED_STOCK = -1L;
- * Redis 客户端
- @Autowired
- private RedisTemplate redisTemplate;
- * 执行扣库存的脚本
- public static final String STOCK_LUA;
- static {
- /**
- *
- * @desc 扣减库存Lua脚本
- * 库存(stock)-1:表示不限库存
- * 库存(stock)0:表示没有库
- * 库存(stock)大于0:表示剩余库存
- * @params 库存key
- * @return
- * 0:库存不足
- * -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(" if (stock == -1) then");
- sb.append(" return 1;");
- sb.append(" end;");
- sb.append(" if (stock > 0) then");
- sb.append(" redis.call('incrby', KEYS[1], -1);");
- sb.append(" return stock;");
- sb.append(" end;");
- sb.append(" return 0;");
- sb.append("end;");
- sb.append("return -1;");
- STOCK_LUA = sb.toString();
- }
- * @param key 库存key
- * @param expire 库存有效时间,单位秒
- * @param stockCallback 初始化库存回调函数
- * @return 0:库存不足; -1:库存未初始化; 大于0:扣减库存之前的剩余库存(扣减之前剩余的库存)
- public long stock(String key, long expire, IStockCallback stockCallback) {
- long stock = stock(key);
- // 初始化库存
- if (stock == UNINITIALIZED_STOCK) {
- RedisLock redisLock = new RedisLock(redisTemplate, key);
- try {
- // 获取锁
- if (redisLock.tryLock()) {
- // 双重验证,避免并发时重复回源到数据库
- stock = stock(key);
- if (stock == UNINITIALIZED_STOCK) {
- // 获取初始化库存
- final int initStock = stockCallback.getStock();
- // 将库存设置到redis
- redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
- // 调一次扣库存的操作
- stock = stock(key);
- }
- }
- } catch (Exception e) {
- logger.error(e.getMessage(), e);
- } finally {
- redisLock.unlock();
- }
- }
- return stock;
- }
* 获取
- * @param key 库存key
- * @return 0:库存不足; -1:库存未初始化; 大于0:剩余库存
- public int getStock(String key) {
- Integer stock = (Integer) redisTemplate.opsForValue().get(key);
- return stock == null ? -1 : stock;
- }
- * 扣库
- * @param key 库存key
- * @return 扣减之前剩余的库存【0:库存不足; -1:库存未初始化; 大于0:扣减库存之前的剩余库存】
- private Long stock(String key) {
- // 脚本里的KEYS参数
- List keys = new ArrayList<>();
- keys.add(key);
- // 脚本里的ARGV参数
- List args = new ArrayList<>();
- long result = redisTemplate.execute(new RedisCallback() {
- public 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;
- }
- }
-
- @RestController
- public class StockController {
- @Autowired
- private StockService stockService;
- @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
- public Object stock() {
- // 商品ID
- long commodityId = 1;
- // 库存ID
- String redisKey = "redis_key:stock:" + commodityId;
- long stock = stockService.stock(redisKey, 60 * 60, () -> initStock(commodityId));
- return stock > 0;
- }
- 获取初始的库存
- private int initStock(long commodityId) {
- // TODO 这里做一些初始化库存的操作
- return 1000;
- }
- @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
- public Object getStock() {
- // 商品ID
- long commodityId = 1;
- // 库存ID
String redisKey = "redis_key:stock:" + commodityId;
- return stockService.getStock(redisKey);
- }
- }
2.4 手段四-利用redis+异步调用机制处理
-->redis扣减库存-->记录扣减日志
异步调用(可以写一个workerHandler类 executor是多线程的,它的作用更像是一个规划中心。而Worker则只是个搬运工,它自己本身只有一个线程的。或者使用rocketmq的异步worker处理 )获取日志-->同步数据库。
bu:https://blog.csdn.net/qq315737546/article/details/76850173