Spring事务和Sql的Lock wait timeout exceeded错误有关吗?

碰到这个问题一般的现象都是程序会阻塞一段时间,然后报错:Lock wait timeout exceeded。我水平不足,用了一下午才确定了原因,把这次的事情写清楚,即使和大家的情况不一致,如果能给与些许的提示,也是好的。

为什么会得到这个错误,都怪我太好奇,想去测试下spring事务的其中一种传播机制。

spring事务传播机制默认是Request,这个传播机制,使得

事务 = 当前事务 == null ? new 事务 : 当前事务。

这是最为常见的事务方式,也因之是同一个事务,所以面对任何一个异常,都会回滚。

比如下面这个例子:

@Service
public class GradeService {
	@Autowired
	private GradeMapper gradeMapper;
	
	//先查,再改
	@Transactional
	public void update(int name) {
		System.out.println("===========================================================");
		Grade grade = gradeMapper.selectById(1);
		gradeMapper.updateNameById(1, grade.getGname() + 1);
	}
}
@Service
public class UserService {
	@Autowired
	private UserMapper userMapper;
	@Autowired
	private GradeService gradeService;
	
	//先查,再改,调用gradeService
	@Transactional
	public  void update(int i) {
		System.out.println("线程:"+Thread.currentThread().getName());
		
		User user = userMapper.selectById(1);
		userMapper.updateById(1, user.getName()+1);
		gradeService.update(i);
		
	}
}

这两个service的update方法都有事务,但是userservice调用了gradeservice。所以gradeService方法的事务就是Userservice上的事务。

但接下来,我修改了gradeService上的事务为:

@Transactional(propagation= Propagation.REQUIRES_NEW)

这个就代表着强制new 一个新事务,那么此时访问userservice会先开启一个事务,然后查询修改user,接着把user事务挂起,开启grade事务,再执行grade的一系列方法。

那么我执行了一下,正常访问,然后我再分别在两个service里写个除零异常,观察回滚结果,结论是:

外层事务异常对内层没有影响,内层异常对外层有影响,因为异常由内向上抛嘛。

而且由于代理模式,外层是在回滚之后才捕获异常的,所以外层捕获内层依然会回滚。

如果我到此为止了,那么这篇博客或许到此为止,题目就成为了spring的REQUIRES_NEW事务 。但我手贱,并发执行了userservice,开启了10个线程去执行。

然后,我就发现我的程序卡死了,它走到一半时就卡地不能动了。本身作为一个测试程序吧,即使我开了10个线程,那也应该立即执行完成,但是IDE显示的那个运行红点一直在亮着,它根本没有停止。

接下来就是关掉再次运行查找原因:

我一开始认为是线程有问题,查看了线程状态,绝大部分都是Running状态,只有两个是wait状态。

Run的栈如下:好像它此时正在和数据库进行连接。

java.net.SocketInputStream.socketRead0(Native Method)
java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
java.net.SocketInputStream.read(SocketInputStream.java:171)
java.net.SocketInputStream.read(SocketInputStream.java:141)
com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:100)
com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:143)
com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:173)
   - 已锁定 com.mysql.jdbc.util.ReadAheadInputStream@79c45d48
com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2954)

 Wait的状态如下,好像它是在getConnect时在挂起。

名称: Thread-4
状态: java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@763393a1上的WAITING
总阻止数: 11, 总等待数: 3

堆栈跟踪: 
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:2094)
com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1620)
com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1395)
com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1375)
com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1365)
com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:109)
org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:262)

我推测是wait的原因导致了我整个程序不动,去追下DruidDataSource的源码吧,这个源码是很不友好的,因为他没有注释,看的我一脸懵逼,最终debug到takeLast方法这里:

        for (;;) {
                ...
                ...
            
               emptySignal(); 

               ...
               ...
               notEmpty.signal(); 
        }

发现线程就是在这里停止不动的notEmpty.signal();看了半天,好像这两句是唤醒其他线程,让自己线程进行等待 。娘的,这什么跟什么?怎么让自己睡反而让别人醒?而且外部还有个无限循环,看的头大。

正在头大之时,程序有了反应,死死的它突然蹦出一片异常:

这是个sql异常,难道是连接数据库有问题?这个异常还挺少见啊,百度翻译一下,

超过锁定等待超时;尝试重新启动事务

 

呵呵~,这翻译~

对着翻译和英语我推测出了意思,是现在加锁加的时间太长了,不加了,请重新开启事务。

太复杂了,还是面向百度编程吧。百度一下,结果搜到的都是这样的:

1.Spring事务修改为@Transactional(propagation= Propagation.SUPPORTS)

这个解决方案是比较坑的,这个事务传播机制那么垃圾,我会使用吗?而且还没有给出一个根本原因。

2.查询数据库information_schema.INNODB_TRX这个事务数据库,把所谓的Lockwait状态的事务给kill掉,如下所示:

这又是一个很奇怪的解决,难道程序真正跑起来,我还得坐在数据库旁边一直firstblood,double kill 吗?

对以上两种思路我不敢苟同,便查看报错信息,看着看着我发现了不对劲的地方,我的报错信息告诉我以下奇异的事情:

 1.起初我开启了十条线程,其中8条访问进入了userservice,但它们都卡在即将要进入gradeService的地方,都没有进入。

2.另外两条线程连userservice都没有进去。

3.过一会儿这8条线程会全部报错Lock wait timeout exceeded

4.8线程报错的同时,另外两条线程走进了userservice和gradeservice,执行成功了。

如图(报异常的同时另外线程开始正常运行):

 

这,真奇了怪了~~~这什么跟什么?简直摸不着头脑

不过,我还有最后一个想法,最后一个推测,或许是我用的依赖太垃圾了吧,我的连接池用的是阿里的德鲁伊,这玩意一百年没有更新过了,我把它换掉,或许会更好,于是我换掉了依赖,换成了c3p0

焚香沐浴,换掉依赖,祷告苍天~运行!!!!

果然没报错~~顺利运行~~

 

本该贺喜万岁,只是tmd,如果没有我再次手贱,那么这个博客的标题可能就变成了"一次依赖错误引发的思考",我当时,是的,我至今还清晰地记得,我准备好了庆祝的威士忌,但我突然觉得,或许将线程开到了20个更能征服这个bug.

结果,它出现了和上面德鲁伊一样的情景。

部分卡死,报错,剩余线程正常运行。

我无法直面失败,我运行一次又一次,我期望失败只是个个例,但事实是它一直那样。我漫无目的地点击启动,发呆地看着打印的一行行debug英文和异常栈,以至于几天之后,当我写这篇博客的时候,我依然记得我那时的心跳是多么的无规则而又冲动。

我一直不相信命运,更觉得小说里主人公的好运不可能降临世间,但接下来,却发生了一件神迹,就像杨过在绝情谷吃了情花,百无医治,最终天竺神僧在情花下发现了一个断肠草的小芽克制了情花毒。

我在茫茫日志了发现了这两句:

acquire test -- pool is already maxed out. [managed: 15; max: 15]
awaitAvailable(): com.mchange.v2.c3p0.impl.NewPooledConnection@de524fb

意思是说:连接池最大是15,已经使用了15个,剩下的在等待获取。

这,,,,,难道???????莫非?

我有一个大胆想法,不过为了确认,我还需要验证一番。

 

我赶紧将线程池最大值调大,调成50,运行,成功!!

那再将最大值调成200,然后将同时运行的并发线程调成150,运行,成功!

那么这样做也应该没有问题,于是我再切换回德鲁伊,调整线程池最大值,运行,成功!!!!

果然如此啊~

 

以下还原案件现场:

德鲁伊默认最大连接池为8

我开启了10条线程,其中8条线程启动事务进入了userservice方法。它们共获取了8条连接,连接池已用完。

另外两条线程获取不到连接,无法进入userservice方法,一直wait等待连接。

8条进入userservice方法的线程要进入gradeservice,但是gradeservice的事务级别是Rquest_new,要新开事务新开连接,于是它们把原来的事务挂起,但是连接池却用完了,它们没办法,也要等待卡着了。

数据库事务最大延迟时间是40秒左右,过了40秒,数据库看那8个事务挂起还不提交,干脆不理你了,把事务回滚报了异常。

8条线程报了异常,归还了连接。

另外两条线程终于获得了连接,足够执行完剩下的方法了。

c3p0默认最大连接数是15.开10条并发测试没问题,20条就会有问题。

德鲁伊源码里就是:排队等待获取连接,谁先排进来的先唤醒谁,唤醒别人,然后自己沉睡,但还有个条件是要有空闲连接啊。

嗯,其实都是连接用完引起的,怪不得网上教程让使用@Transactional(propagation= Propagation.SUPPORTS)和kill,因为它们最终都是作用在连接上了啊。

 

 

 

 

 

 

 

 

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值