一次生产问题:MySQL连接耗尽和死锁

连接耗尽

一次外部系统后台多线程调用我的服务时,发生了2次问题,第一次是MySQL连接池耗尽,

Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection;

数据库是MySQL,连接池使用HikariPool,相关配置:
spring.datasource.hikari.minimun-idle=10
spring.datasource.hikari.maximun-pool-size=20
spring.datasource.hikari.connection-timeoiut=3000

并发的请求大约耗时500ms,一共4台实例,按照乐观估计,每秒可以处理2*20*4=160个请求,现在外部请求也就10qps,不至于这么脆弱,刚开始hikari连接池特性问题,查阅各项参数指标,想通过提高连接数来解决问题,后来发现集团建议配置值也是这样,只好本地进行压测,将

spring.datasource.hikari.minimun-idle=1
spring.datasource.hikari.maximun-pool-size=1

先来复现一下报错信息,用Jmete压测后很快出现无法获取连接的异常,于是将配置参数还原至生产配置,进行压测,发现几乎没有异常,全部都正常处理,这样看来不是连接配置问题,开始怀疑是Spring在接口结束后没有释放连接,断点后发现在接口结束时,Spring有拦截器释放连接,这里没有问题。于是继续查看生产日志,突然发现耗时很久的接口,然后顺着将所有耗时很久的接口都找出来,发现出现异常的时候,有一个接口跟着耗时达到几十秒,这个出现后,连接耗尽就解释的通了,这些耗时的接口占着连接不释放,新来的接口3S拿不到连接就报错了。

于是查看这个接口耗时问题,发现这个接口是最近优化过的,由于要串行调用2个外部接口x和y,每个接口要200ms左右,优化方案是x调完后,将y交给线程池调用,下面继续处理逻辑,直到需要用到y的结果时在completableFuture.get()阻塞获取结果,这样就可以节省一段时间,这样优化以后,发现在并发情况下,耗时更久,原因是线程池里还有其他任务在排队,于是这个接口就有可能等待很久,这样的反向优化很可怕,解决方案就是新建了另一个fast线程池,里面线程数量略微增加,且只提交需要立即处理的任务,这样优化后,果然没有连接耗尽的报错了。

死锁

经过优化后,并发可以正常处理了,但是高并发下又暴露了另一个问题:
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

具体报错位置是更新一个字段,类似于
update t1 set a = 1 where code = 'k'
看了半天这条语句,都不像是能产生死锁的语句啊,如果思维局限在一条记录上,确实不会产生死锁,后来查看代码,发现有一个循环,会更新t1中多条数据,且顺序不定,这就为死锁发生构造了条件,如多个线程并发下,T1要循环更新1,3,5,T2要循环更新3,1,5,那么就有可能发生死锁,T1更新完1,准备获取3的行锁,T2如果刚更新完3,准备获取1的行锁,那么他们就会锁死。于是就修改了逻辑,发现99%情况不需要更新,而之前逻辑是无脑直接更新,于是加了判断逻辑,查询出结果后,判断需要更新的场景再更新。这2次生产问题是一起发生的,只有在高并发下才会出现,原因各有不同。

总结:

1、线程池要隔离,特别是业务逻辑中异步处理任务较多时,要考虑异步是否需要快速处理完

2、表的修改要在需要修改的时候再修改,不要无脑修改,不要怕麻烦多查询一次
 

你可能感兴趣的:(编程经验,mysql,数据库)