记一次锁和事物导致的并发问题

需求背景

对系统内的物品做一个SN码追踪,我这边的设计是入库和出库操作都会将物品的SN码和相关单据信息记录到一张表中,由于存在小零件的物品,一包几百个都是同一个SN码,所以这里牵扯到一个数量。表结构的核心字段如下

字段类型
SN码varchar
操作单号varchar
操作数量int
库内余数int

并发问题

由于属于日志表,因此这张表的操作只能插入。字段库内余数需要先查询最近的一次操作记录,拿到上次的库内余数,然后与本次操作数量做计算,得到新的库内余数,再进行插入。
这里就会产生一个并发问题,这里直接使用了Lock来保证串行执行,伪代码未下:

@Transactional
void m1(parm){
try{
	//前置处理
	lock.lock()
	//业务代码
	// 1. 查询最近一次记录的  库内余数
	// 2. 计算本次操作后的  库内余数
	// 3. 插入记录
}finally{
	lock.unlock()
}
}

想法很好,并发时获取不到锁就阻塞,然后拿到锁后再进行操作。
单元测试,10个线程,并发插入1000条记录,测试失败!

问题分析

数据有误,说明在计算新的库内余数时,部分线程没有拿到最新的数据。因此经过分析后,我注意到了@Transactional,由于ERP系统用户少,并发不高,但数据比较重要,所以我们的业务层基本都是跑在事物下的,而本次的并发问题,罪魁祸首就是这个事物。

在线程进入方法后,一旦与数据库进行交互,会正式开启一个事物,之后获取锁,获取失败就会进行阻塞。当线程醒过来时,会继续之前的事物进行执行。而由于事物的隔离性,此时当前线程拿到的数据都是它开启事物时的数据,但这个数据其实并不一定是最新的!因此错误就产生了!

解决方案

我这里是直接采用了TransactionTemplate进行手动事物提交,当然也可以将这部分代码提出来,然后进行方法调用(注意同对象方法内调用不走代理事物失效的问题)。总之保证事物开启必须要在获取锁之后再开启即可。

void m1(parm){
try{
	//前置处理
	lock.lock()
	//业务代码
	//  手动开启事物 
	// 1. 查询最近一次记录的  库内余数
	// 2. 计算本次操作后的  库内余数
	// 3. 插入记录
}finally{
	lock.unlock()
}
}

再次测试,成功!

一波未平一波又起

经过上面的方法,我在自己的Controller层调用时没问题了,但情况是我这个服务不仅我自己调用,别的服务也会在Service层(含事物)调用我的服务,而在他们的Service层进行第一次数据库访问时,事物便开启了,从而导致后面的数据都是事物开启时间的数据,再次引发上面所述的问题。

最终解决方案

问题的核心就是在我们获取锁后进行数据库查询时,必须要拿到最新的数据,因此我新开了一个事物,如下:


void m1(parm){
try{
	//前置处理
	lock.lock()
	context.getBean(MyServiceImpl.class).doInsert(args) 
}finally{
	lock.unlock()
}
}
// 事物传播新开一个事物
@Transactional(propagation=Propagation.REQUIRES_NEW)
void doInsert(args){
// 1. 查询最近一次记录的  库内余数
	// 2. 计算本次操作后的  库内余数
	// 3. 插入记录
}

最终结果正常。

锁的优化

后期优化将锁换成了redis的锁,jdk的方法锁开销还是蛮大的,我们的数据只会计算同一个批次的余数,因此使用redis的锁对批次进行锁,可以有效提高并发量(虽然用不着,但学习还是需要的O(∩_∩)O)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值