前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >并发扣减库存方案一

并发扣减库存方案一

作者头像
叔牙
发布2020-11-19 14:33:04
发布2020-11-19 14:33:04
1.3K00
代码可运行
举报
运行总次数:0
代码可运行
代码语言:javascript
代码运行次数:0
复制
在开始分析库存扣减方案之前,首先有几个概念需要明确,因为本篇分析就是在此思想的基础上得出的解决方案.
那就是CAS和幂等,下边逐个做简要解释:
1.CAS
代码语言:javascript
代码运行次数:0
复制
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;}}

代码语言:javascript
代码运行次数:0
复制
此方法的作用是返回当前值,并对该原子变量进行++操作;分析一下代码,①首先一个永真循环,②每次循环拿到
该原子变量的"内存值"暂存到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);}

2.幂等
代码语言:javascript
代码运行次数:0
复制
幂等是一个抽象的概念。举个例子说,我们java后台写的一个接口或者方法,调用一次和调用N次,如果从理论上
我们得到的结果是一样的,那么这个操作就是幂等的(查询,设置),而插入和删除是典型的非幂等操作,因为不可重现;
再举个例子,我们系统中引入消息中间件的时候,会存在消息幂等的概念,消息中间件接收到发送方的消息后
存储此消息并做唯一标识,不管其有没有收到响应或者有没有重发,中间件都不会存储两条一模一样的消息;
消息被消费方消费后,不管中间件有没有收到响应,消费方应该要做唯一标识存储消费的消息,而不会导致
重复消费同一条消息(A像中间件发消息,如果中间件没有收到消息或者收到消息后,给的响应A没有收到,
那么A将重发消息;B消费中间件存储的消息,
如果给的响应中间件没有收到,消息将会重复被B消费)接下来开始分析库存扣减并发问题的解决方案,解决并发问题有很多方式,比如说借助redis原生的单线程阻塞操作,
zk的节点操作,以及其他封装的分布式锁操作;此处借助CAS理念和幂等使用mysql自带的表锁和行级锁实现并发操作
背景
代码语言:javascript
代码运行次数:0
复制
常用的扣减操作是service层操作数据库执行update Stock set stock = stock - ? where id = ?
存在问题
代码语言:javascript
代码运行次数:0
复制
单线程一切都运行正常,但是多线程情况下出现数据不一致问题,两个线程在同一个stock基础上进行不同的扣减,
导致后者覆盖前者
案例分析
代码语言:javascript
代码运行次数:0
复制
两个线程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,都是在此数字上扣减,
导致其中一个结果被覆盖,拿到错误的扣减结果,
还有一种情况是,设计往往有容错机制,例如“重试”,如果通过扣减接口来修改库存,在重试时,可能会
得到错误的数据,导致重复扣减;
重试导致错误的根本原因,是因为“扣减”操作是一个非幂等的操作,不能够重复执行,改成设置操作则不会
有这个问题
解决方案
代码语言:javascript
代码运行次数:0
复制
参考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是其期望的值,但是版本号
已经变更了两次,从而更新失败
以下是代码和并发测试
①创建库存表
代码语言:javascript
代码运行次数:0
复制
②编写接口及实现
代码语言:javascript
代码运行次数:0
复制
③单元测试
代码语言:javascript
代码运行次数:0
复制
代码语言:javascript
代码运行次数:0
复制
开了20个线程测试多次没有出现库存被扣负的情况,欢迎各位大神拿砖来拍......
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-07-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.CAS
  • 2.幂等
  • 背景
  • 存在问题
  • 案例分析
  • 解决方案
  • 以下是代码和并发测试
    • ①创建库存表
    • ②编写接口及实现
    • ③单元测试
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档