Spring @Transactional长事务事故
在Spring
中进行事务管理非常简单,只需要在方法上加上注解@Transactional
,Spring
就可以自动帮我们进行事务的开启、提交、回滚操作。甚至很多人心里已经将Spring
事务与@Transactional
划上了等号,只要有数据库相关操作就直接给方法加上@Transactional
注解。
一、简介
长事务顾名思义就是运行时间比较长,长时间未提交的事务,也可以称之为大事务。
长事务产生的原因:
- 操作的数据比较多
- 大量的锁竞争
- 事务中有其他非DB的耗时操作
长事务引发的常见危害有:
- 数据库连接池被占满,应用无法获取连接资源;
- 锁定太多的数据,造成大量的阻塞和锁超时;
- 数据库回滚时间长;
- 在主从架构中会导致主从延时变大。
- undo log膨胀
二、案例
spring @Transactional导致的生产事故。
/**
* 保存报销单并创建工作流
*/
@Transactional(rollbackFor = Exception.class)
public void save(RequestBillDTO requestBillDTO) {
//调用流程HTTP接口创建工作流
workflowUtil.createFlow("BILL",requestBillDTO);
//转换DTO对象
RequestBill requestBill = JkMappingUtils.convert(requestBillDTO, RequestBill.class);
requestBillDao.save(requestBill);
//保存明细表
requestDetailDao.save(requestBill.getDetail())
}
代码非常简单也很“优雅”,先通过http
接口调用工作流引擎创建审批流,然后保存报销单,而为了保证操作的事务,在整个方法上加上了@Transactional
注解(仔细想想,这样真的能保证事务吗?)。
报销系统开始出现了故障:数据库监控平台一直收到告警短信,数据库连接不足,出现大量死锁;日志显示调用流程引擎接口出现大量超时;同时一直提示CannotGetJdbcConnectionException
,数据库连接池连接占满。
在发生故障后,尝试过杀掉死锁进程,也进行过暴力重启,只是不到10分钟故障再次出现。
2.1 事故原因分析
通过对日志的分析我们很容易就可以定位到故障原因就是保存报销单的save()
方法,而罪魁祸首就是那个@Transactional
注解。
我们知道@Transactional
注解,是使用AOP
实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
当Spring
遇到该注解时,会自动从数据库连接池中获取connection
,并开启事务然后绑定到ThreadLocal
上,对于@Transactional
注解包裹的整个方法都是使用同一个connection
连接。如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection
的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。
在一个事务中执行RPC
操作导致数据库连接池撑爆属于是典型的长事务问题,类似的操作还有在事务中进行大量数据查询,业务规则处理等...
三、如何避免长事务
解决长事务的宗旨就是对事务方法进行拆分,尽量让事务变小,变快,减小事务的颗粒度。
- 在一个事务里面,避免一次处理太多数据;
- 在一个事务里面,尽量避免不必要的查询;
- 在一个事务里面,避免耗时太多的操作,造成事务超时。一些非
DB
的操作,比如rpc
调用,消息队列的操作尽量放到事务之外操作; - 在
InnoDB
事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放; - 通过
SETMAX_EXECUTION_TIME
命令,来控制每个语句查询的最长时间,避免单个语句意外查询太长时间; - 监控
information_schema.Innodb_trx
表,设置长事务阈值,超过就报警/或者kill
; - 在业务功能测试阶段要求输出所有的
general_log
,分析日志行为提前发现问题; - 设置
innodb_undo_tablespaces
值,将undo log
分离到独立的表空间。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。
既然提到了事务的颗粒度,我们就先回顾一下Spring
进行事务管理的方式。
3.1 声明式事务
首先我们要知道,通过在方法上使用@Transactional
注解进行事务管理的操作叫声明式事务。
使用声明式事务的优点
很明显,就是使用很简单,可以自动帮我们进行事务的开启、提交以及回滚等操作。使用这种方式,程序员只需要关注业务逻辑就可以了。
声明式事务有一个最大的缺点
就是事务的颗粒度是整个方法,无法进行精细化控制。
3.2 编程式事务
与声明式事务对应的就是编程式事务。
基于底层的API
,开发者在代码中手动的管理事务的开启、提交、回滚等操作。在spring
项目中可以使用TransactionTemplate
类的对象,手动控制事务。
@Autowired
private TransactionTemplate transactionTemplate;
//...
public void save(RequestBill requestBill) {
transactionTemplate.execute(transactionStatus -> {
requestBillDao.save(requestBill);
//保存明细表
requestDetailDao.save(requestBill.getDetail());
return Boolean.TRUE;
});
}
使用编程式事务最大的好处就是可以精细化控制事务范围。所以避免长事务最简单的方法就是不要使用声明式事务@Transactional
,而是使用编程式事务手动控制事务范围。
有的同学会说,@Transactional
使用这么简单,有没有办法既可以使用@Transactional
,又能避免产生长事务?那就需要对方法进行拆分,将不需要事务管理的逻辑与事务操作分开:
@Service
public class OrderService {
public void createOrder(OrderCreateDTO createDTO) {
query();
validate();
saveData(createDTO);
}
//事务操作
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO) {
orderDao.insert(createDTO);
}
}
query()
与validate()
不需要事务,我们将其与事务方法saveData()
拆开。
当然,这种拆分会命中使用@Transactional
注解时事务不生效的经典场景。
@Transactional
注解的声明式事务是通过spring aop
起作用的,而spring aop
需要生成代理对象,直接在同一个类中方法调用使用的还是原始对象,事务不生效。
正确的拆分方法应该使用下面两种:
- 可以将方法放入另一个类,如新增
manager层
,通过spring
注入,这样符合了在对象之间调用的条件。
@Service
public class OrderService {
@Autowired
private OrderManager orderManager;
public void createOrder(OrderCreateDTO createDTO){
query();
validate();
orderManager.saveData(createDTO);
}
}
@Service
public class OrderManager {
@Autowired
private OrderDao orderDao;
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO){
orderDao.saveData(createDTO);
}
}
- 启动类添加
@EnableAspectJAutoProxy(exposeProxy = true)
,方法内使用AopContext.currentProxy()
获得代理类,使用事务。
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {
}
public void createOrder(OrderCreateDTO createDTO) {
OrderService orderService = (OrderService)AopContext.currentProxy();
orderService.saveData(createDTO);
}
四、如何查询大事务
注:本文的sql的操作都是基于mysql5.7版本
以查询执行时间超过10秒的事务为例:
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(), trx_started)) > 10
查询事务相关语句
# 查询所有正在运行的事务及运行时间
select
t.*,
to_seconds(now()) - to_seconds(t.trx_started) idle_time
from
INFORMATION_SCHEMA.INNODB_TRX t;
# 查询事务详细信息及执行的SQL
select
now(),
(UNIX_TIMESTAMP(now()) - UNIX_TIMESTAMP(a.trx_started)) diff_sec,
b.id,
b.user,
b.host,
b.db,
d.SQL_TEXT
from
information_schema.innodb_trx a
inner join information_schema.PROCESSLIST b on a.TRX_MYSQL_THREAD_ID = b.id and b.command = 'Sleep'
inner join performance_schema.threads c on b.id = c.PROCESSLIST_ID
inner join performance_schema.events_statements_current d on d.THREAD_ID = c.THREAD_ID;
# 查询事务执行过的所有历史SQL记录
select
ps.id 'PROCESS ID',
ps.USER,
ps.HOST,
esh.EVENT_ID,
trx.trx_started,
esh.event_name 'EVENT NAME',
esh.sql_text 'SQL',
ps.time
from
PERFORMANCE_SCHEMA.events_statements_history esh
join PERFORMANCE_SCHEMA.threads th on esh.thread_id = th.thread_id
join information_schema.PROCESSLIST ps on ps.id = th.processlist_id
left join information_schema.innodb_trx trx on trx.trx_mysql_thread_id = ps.id
where
trx.trx_id is not null
and ps.USER != 'SYSTEM_USER'
order by
esh.EVENT_ID;
# 简单查询事务锁
select * from sys.innodb_lock_waits;
# 查询事务锁详细信息
select
tmp.*,
c.SQL_Text blocking_sql_text,
p.HOST blocking_host
from
(
select
r.trx_state wating_trx_state,
r.trx_id waiting_trx_id,
r.trx_mysql_thread_Id waiting_thread,
r.trx_query waiting_query,
b.trx_state blocking_trx_state,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
from
information_schema.innodb_lock_waits w
inner join information_schema.innodb_trx b on b.trx_id = w.blocking_trx_id
inner join information_schema.innodb_trx r on r.trx_id = w.requesting_trx_id
) tmp,
information_schema.PROCESSLIST p,
PERFORMANCE_SCHEMA.events_statements_current c,
PERFORMANCE_SCHEMA.threads t
where
tmp.blocking_thread = p.id
and t.thread_id = c.THREAD_ID
and t.PROCESSLIST_ID = p.id;
五、附录:编程式事务工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
@Service
public class TransactionManagerUtil {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 开启事务
public TransactionStatus begin() {
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(
new DefaultTransactionAttribute());
return transaction;
}
// 提交事务
public void commit(TransactionStatus transactionStatus) {
dataSourceTransactionManager.commit(transactionStatus);
}
// 回滚事务
public void rollback(TransactionStatus transactionStatus) {
dataSourceTransactionManager.rollback(transactionStatus);
}
}
使用
//编程式事务
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
@Resource
private TransactionDefinition transactionDefinition;
//手动开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager
.getTransaction(transactionDefinition);
try {
//TODO 业务代码
//手动提交事务
dataSourceTransactionManager.commit(transactionStatus);
} catch (Exception e) {
//手动回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
e.printStackTrace();
}
六、总结
使用@Transactional
注解在开发时确实很方便,但是稍微不注意就可能出现长事务问题。所以对于复杂业务逻辑,我这里更建议你使用编程式事务来管理事务,当然,如果你非要使用@Transactional
,可以根据上文提到的两种方案进行方法拆分。