事件回顾
在业务高峰期并发量较大时引起大量事务不提交现象,并且业务线程基本被block、不能对外提供服务
查询事务mysql事务: SELECT * from information_schema.INNODB_TRX it ;
dump java应用栈信息:jstack $pid
线上出问题期间没有dump到栈信息,直接回滚了代码版本,后续花费了大量时间进行压测验证,最后定位到类似以下代码出现问题
@Transaction(transactionManager = "datasource1")
public void testTrans() {
// test1Mapper 绑定datasource1
test1Mapper.insert(***);
test1Mapper.update(****);
// test2Mapper 绑定datasource2
test2Mapper.insert(***);
}
在上线之前review到这段代码第一反应 test2Mapper.insert(***); 出现错误不会回滚 test1Mapper中的sql操作,由于上线时间临近,改动代码+提测 可能来不及,即使不回滚对业务的影响不大,可后期修复数据
万万没想到的是,正式因为这个事务方法在业务高峰期引起了雪崩。。。。。。 怪自己太肤浅
直接原因
1. 在业务高峰期,mysql连接池比较繁忙
2. 进入事务方法执行test1Mapper中的两条sql使用连接1,因开启事务,连接1绑定到了事务上下文,在事务未结束之前不释放
3. test2Mapper在执行sql时,因绑定不同数据源则会重新获取连接2,若此时恰好连接池没有空闲连接则进入等待,当前线程被block,等待时间由checkoutTimeout(c3p0数据源)控制,线上设置30秒
4. 持有连接1,但获取不到连接2,恰好又是业务高峰期,进一步加剧了连接池的连接竞争
5. 很快其他非事务方法操作sql时也获取不到连接造成大量业务线程等待,引起雪崩
为什么上面代码会引起高峰期雪崩,需要从spring 事务机制、数据源、连接池来分析下原因
原因分析
数据源 与 连接池
连接池:应用与db连接缓存起来,进行复用,减小创建链接带来的开销
数据源:封装连接池中创建、销毁、获取、退还 连接, 等操作 提供统一接口
Druid数据源类:
public class DruidDataSource extends... {
// 连接数据
private volatile DruidConnectionHolder[] connections;
}
C3P0数据源相关类:
class BasicResourcePool implements ResourcePool
{
int target_pool_size;
// 存储连接
/* keys are all valid, managed resources, value is a PunchCard */
HashMap managed = new HashMap();
/* all valid, managed resources currently available for checkout */
LinkedList unused = new LinkedList();
/* resources which have been invalidated somehow, but which are */
/* still checked out and in use. */
HashSet excluded = new HashSet();
}
业务方法执行事务流程
1. 开启事务
2. 执行事务方法
3. 提交 或 回滚
4. 恢复事务设置
第4步是执行 set autocommit = 1, 没有事务时默认autocommit=1,第1步开启事务时 会执行set autocommit = 0;且会调用mysql执行该语句
spring @Transaction 分析
进入业务方法之前开启事务,创建事务对象,通过@Transaction注解中的数据源获取连接,并绑定到threadlocal中
每次从数据源中获取连接时若无空隙连接则当前线程被阻塞
,绑定连接后,在进入事务体方法执行sql时则会从threadlocal中获取已绑定连接执行sql
简化代码
public class DataSourceTransactionManager ....{
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTran