这个月一直在做项目的的善后工作(20号离职),在当前这个项目上于ERP和PDM系统集成对接中,出现了许多小问题,让我感觉值得一提的是,我们OA系统的领料单流程在归档后需要对ERP物料进行扣减,但是其中物料扣减发生了错误,这是由于多人同时读写数据造成(实在不敢称之为并发)。下面我将分享一下整个过程以及解决方案。
在于外部系统对接的一开始,我们就约定从ERP的数据源获取物料的库存显示到表单中,用户根据实际领料来填写,到提交归档节点的时候,我们会再取一次最新的库存(因为流程到归档时间线可能会有点长,期间库存有可能已经被修改了),然后执行扣减回写到ERP数据源。
一开始觉得并没有问题,因为使用该流程的员工就是几十人,但是不幸的事情发生了:扣减不一致,员工A对于X物料扣减10,员工B对于X物料扣减5,X物料初始为500,正常来说剩余485。但是最终却成为495。虽然说这是一件概率小的时候,但是自己还是不应该将原因归咎于概率,所以需要彻底避免这个问题。
一开始想使用Redis锁来实现(Redis setNX 实现分布式锁大家可以搜一搜相关文章,之前个人使用Redis分布式锁的时候是使用SpringBoot RedisTemplate去实现),但是想想这个业务完全用不上这样来搞,所以想到了乐观锁(觉得悲观锁有点麻烦,相关概念我就不描述了)。
乐观锁使用版本号作为条件:即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
那么我们来看具体的逻辑:使用for循环新增50条线程模拟并发访问(也可以使用jmeter进行接口访问测试)
代码语言:javascript复制 for (int i = 0; i < 50; i ) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
transactionalTest.setNum(finalI);
}
}).start();
}
如果我们不考虑并发,那么Service层这样写
代码语言:javascript复制 public void setNum(int num) {
TestVO testVO = testDao.getNum();
int data = testDao.updateNum(num testVO.getNum(), testVO.getVersion());
}
下面是执行前的数据
id | num | version |
---|---|---|
1 | 0 | 0 |
执行sql:
代码语言:javascript复制UPDATE pri_admin set num = #{num},version=version 1 where id = 1
id | num | version |
---|---|---|
1 | 400 | 50 |
0 1 2 .... 49应该为1225,但是这里只有400可见并发修改数据出现了问题。
现在我们使用乐观锁实现数据的累加。
我们sql修改如下:比较上面的SQL多了一个and条件去匹配version
代码语言:javascript复制UPDATE pri_admin set num = #{num},version=version 1 where id = 1 AND version=#{version}
Service层修改如下,我们首先获取version和num,然后更新的时候匹配version,如果不匹配的话那么data就为0,此时进行递归进行更新,直到更新失败,我们也可以将线程存储到集合中,记录递归的访问次数,当次数达到一定数目后作为递归结束条件。
代码语言:javascript复制 public void setNum(int num) {
TestVO testVO = testDao.getNum();
int data = testDao.updateNum(num testVO.getNum(), testVO.getVersion());
if (data == 0) {
System.out.println(Thread.currentThread().getName() ":尝试继续更新");
setNum(num);
} else {
System.out.println("成功");
}
}
我们再来看一下执行结果(先将数据库num置0):
id | num | version |
---|---|---|
1 | 1225 | 100 |
最近在整理面试题的题库,大家可以到公众号Java面试-Java面试-笔试面试进行查看,目前只整理了四篇(觉得整理起来好累),大家也可以到小程序中进行面试题查看。