浅谈库存扣减和锁

您所在的位置:网站首页 分布式库存扣减 浅谈库存扣减和锁

浅谈库存扣减和锁

#浅谈库存扣减和锁| 来源: 网络整理| 查看: 265

先说场景:

物品W现在库存剩余1个,  用户P1,P2同时购买.则只有1人能购买成功.(前提是不允许超卖)

秒杀也是类似的情况, 只有1件商品,N个用户同时抢购,只有1人能抢到..

这里不谈秒杀设计,不谈使用队列等使请求串行化,就谈下怎么用锁来保证数据正确.

 

常见的实现方案有以下几种:

1.代码同步, 例如使用 synchronized ,lock 等同步方法

2.不查询,直接更新  update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0

3.使用CAS, update table set surplus = aa where id = xx and version = y

4.使用数据库锁, select xx for update

5.使用分布式锁(zookeeper,redis等)

 

下面就针对这几种方案来分析下;

1.代码同步, 例如使用 synchronized ,lock 等同步方法

面试的时候,我经常会问这个问题,很大一部分人都会回答用这个方案来实现.

伪代码如下:

 

[java] view plain copy print? public synchronized void buy(String productName, Integer buyQuantity) {       // 其他校验...       // 校验剩余数量       Product product  = 从数据库查询出记录;       if (product.getSurplus  buyQuantity) { 影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查询的剩余数量 ; } else { return "库存不足"; } } // 记录日志... // 其他业务... }

看到重新查询几个字,小伙伴们应该就又想到事务隔离级别问题了.

 

没错,所以上面代码中的getByDB方法,必须单独事务(注意同一个bean内单独事务不生效哦),而且数据库的事务隔离级别必须是RC,

否则上面的代码就会是死循环了.

 

上面的方案,可能会出现一个CAS中经典问题. ABA的问题.

ABA是指:

线程T1 查询,库存剩余  100

线程T2 查询,库存剩余  100

线程T1 执行subupdate t set surplus = 90 where id = x and surplus = 100;

线程T3 查询, 库存剩余 90

线程T3 执行add  update t set surplus = 100 where id = x and surplus = 90;

线程T2 执行subupdate t set surplus = 90 where id = x and surplus = 100;

这里线程T2执行的时候,库存的100已经不是查询到的100了,但是对于这个业务是不影响的.

一般的设计中CAS会使用version来控制.

 

[html] view plain copy print? update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;   update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;

 

这样,每次更新version在原基础上+1,就可以了.

使用CAS要注意几点,

1)失败重试次数,是否需要限制

2)失败重试对用户是透明的

 

4.使用数据库锁, select xx for update

方案3种的cas,是乐观锁的实现, 而select for udpate 则是悲观锁. 在查询数据的时候,就将数据锁住.

伪代码如下:

 

[java] view plain copy print? public void buy(String productName, Integer buyQuantity) {       // 其他校验...       Product product = select * from table where name = productName for update;       if (查询的剩余数量 > buyQuantity) {           影响行数 = update table set surplus = (surplus - buyQuantity) where name = productName ;       } else {           return "库存不足";       }              // 记录日志...       // 其他业务...   }   public void buy(String productName, Integer buyQuantity) { // 其他校验... Product product = select * from table where name = productName for update; if (查询的剩余数量 > buyQuantity) { 影响行数 = update table set surplus = (surplus - buyQuantity) where name = productName ; } else { return "库存不足"; } // 记录日志... // 其他业务... }

 

 

线程T1 进行sub , 查询库存剩余 100

线程T2 进行sub , 这时候,线程T1事务还未提交,线程T2阻塞,直到线程T1事务提交或回滚才能查询出结果.

所以线程T2查询出的一定是最新的数据.相当于事务串行化了,就解决了数据一致性问题.

对于select for update,需要注意的有2点.

1) 统一入口:所有库存操作都需要统一使用 select for update ,这样才会阻塞, 如果另外一个方法还是普通的select, 是不会被阻塞的

2) 加锁顺序:如果有多个锁,那么加锁顺序要一致,否则会出现死锁.

 

 

5.使用分布式锁(zookeeper,redis等)

使用分布式锁,原理和方案1种的synchronized是一样的.只不过synchronized的flag只有jvm进程内可见,而分布式锁的flag则是全局可见.方案4种的select for update 的flag 也是全局可见.

分布式锁的实现方案有很多:基于redis,基于zookeeper,基于数据库等等.前面一篇博客写了基于redis的简易实现

基于redis setnx的简易分布式锁

 

需要注意,使用分布式锁和synchronized锁有同样的问题,就是锁和事务的顺序,这个在方案1里面已经讲过.不再重复.

 

做个简单总结:

方案1: synchronized等jvm内部锁不适合用来保证数据库数据一致性,不能跨jvm

方案2: 不具备通用性,不能记录操作前后日志

方案3: 推荐使用.但是如果数据竞争激烈,则自动重试次数会急剧上升,需要注意.

方案4: 推荐使用.最简单的方案,但是如果事务过大,会有性能问题.操作不当,会有死锁问题

方案5: 和方案1类似,只是能跨jvm



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3