在开始分析库存扣减方案之前,首先有几个概念需要明确,因为本篇分析就是在此思想的基础上得出的解决方案.
那就是CAS和幂等,下边逐个做简要解释:
CAS全称是Compare And Set,是java最底层的一种操作,jvm提供了unsafe类与物理机内存打交道,其原理
就是"比较赋值",重要的有点事比较和赋值有严格的顺序关系,并且比较成立才会赋值.
java并发包中的原子操作类和重入锁都使用的CAS,下面拿AtomicInteger中的一段代码举例分析:
12345678910111213 | /*** Atomically increments by one the current value.** @return the previous value*/public final int getAndIncrement() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return current;}} |
---|
此方法的作用是返回当前值,并对该原子变量进行++操作;分析一下代码,①首先一个永真循环,②每次循环拿到
该原子变量的"内存值"暂存到current中,next就是++后要赋给原子变量的值,
③接下来调用了一个compareAndSet方法(代码如下),并根据其返回值做处理;compareAndSet的作用就是拿
current值和内存中值作比较,如果和内存中值一致,就做赋值操作返回true,如果不一致返回false,
如果为false,继续for循环重复此操作,直到成功。很明显做for循环的目的就是防止从内存取值操作到赋值
这中间有其他线程乱入,导致current失效(已经和此刻内存中的原子变量值不一致)
123456789101112 | /*** Atomically sets the value to the given updated value* if the current value {@code ==} the expected value.** @param expect the expected value* @param update the new value* @return true if successful. False return indicates that* the actual value was not equal to the expected value.*/public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);} |
---|
幂等是一个抽象的概念。举个例子说,我们java后台写的一个接口或者方法,调用一次和调用N次,如果从理论上
我们得到的结果是一样的,那么这个操作就是幂等的(查询,设置),而插入和删除是典型的非幂等操作,因为不可重现;
再举个例子,我们系统中引入消息中间件的时候,会存在消息幂等的概念,消息中间件接收到发送方的消息后
存储此消息并做唯一标识,不管其有没有收到响应或者有没有重发,中间件都不会存储两条一模一样的消息;
消息被消费方消费后,不管中间件有没有收到响应,消费方应该要做唯一标识存储消费的消息,而不会导致
重复消费同一条消息(A像中间件发消息,如果中间件没有收到消息或者收到消息后,给的响应A没有收到,
那么A将重发消息;B消费中间件存储的消息,
如果给的响应中间件没有收到,消息将会重复被B消费)接下来开始分析库存扣减并发问题的解决方案,解决并发问题有很多方式,比如说借助redis原生的单线程阻塞操作,
zk的节点操作,以及其他封装的分布式锁操作;此处借助CAS理念和幂等使用mysql自带的表锁和行级锁实现并发操作
常用的扣减操作是service层操作数据库执行update Stock set stock = stock - ? where id = ?
单线程一切都运行正常,但是多线程情况下出现数据不一致问题,两个线程在同一个stock基础上进行不同的扣减,
导致后者覆盖前者
两个线程A和B同时查到库存为5,A执行操作update Stock set stock = stock - 2 where id = 1,B执行操作
update Stock set stock = stock - 3 where id = 1,
而操作的结果可能是2或者3,而不是我们期望的0;因为A B两个线程查询的时候stock=5,都是在此数字上扣减,
导致其中一个结果被覆盖,拿到错误的扣减结果,
还有一种情况是,设计往往有容错机制,例如“重试”,如果通过扣减接口来修改库存,在重试时,可能会
得到错误的数据,导致重复扣减;
重试导致错误的根本原因,是因为“扣减”操作是一个非幂等的操作,不能够重复执行,改成设置操作则不会
有这个问题
参考CAS思想,我们进行更新的时候带上期望数据库存在的旧值update Stock set stock = newValue
where id = ? and stock = oldValue,这种情况在并发场景下,
执行update的时候如果发现oldValue和之前查出来的值不一致,那么就放弃update,返回给调用方错误码(或者抛出异常);
但是这样还是存在一个问题,例如:当前线程
是A,查到库存是5,B线程把库存扣减到3,然后C线程又把线程新增到5,那么A执行上述更新操作的时候对这
两次变更时无法感知的,其实A持有的stock=5已经和现在两次修改
后的stock=5已经完全不止一个值(不是一个版本的值),这就是典型的CAS中的ABA问题,原子变量操作是存在
ABA问题的,后边出现了AtomicStampedReference类,每一次修改后
版本号变更,此场景中我们在库存表中加一个version字段,执行更新操作的时候update Stock stock = newValue,
version = version + 1 where id = ? and stock = oldValue and version = ?
每次修改后都会导致version变更,那么接着上边的例子,A更新的时候虽然发现stock是其期望的值,但是版本号
已经变更了两次,从而更新失败
开了20个线程测试多次没有出现库存被扣负的情况,欢迎各位大神拿砖来拍......
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!