前言
在上周的开发中, 遇到了事务相关的问题, 测试环境正常但部署到正式环境就抛出异常, 又连续加班几天解决了此问题, 现对该问题作出复盘并回顾之前的知识点. 如有错误, 欢迎指正.
什么是事务
数据库的事务是一种机制, 一个操作序列, 包含了数据库操作命令. 事务把所有的命令做为一个整体一起向系统提交或撤销操作请求, 即这一组命令要么成功, 要么失败.
事务的 4 个特性 (ACID):
原子性
事务是一个完整的操作. 事务内的各元素是不可分割的. 事务中的元素必须作为一个整体提交或回滚. 如果事务中的任何元素失败, 整个事务将失败.
一致性
在事务开始前, 数据必须处于一致状态; 在事务结束后, 数据的状态也必须保持一致. 通过事务对数据所做的修改不能损坏数据.
隔离性
事务的执行不受其他事务的干扰, 事务执行的中间结果对其他事务必须是透明的.
持久性
对于提交的事务, 系统必须保证事务对数据库的改变不被丢失, 即使数据库发生故障.
如何实现事务
在目前的业务开发过程中, 都是以 Spring 框架为主. Spring 支持两种方式的事务管理编程式事务和声明式事务
编程式事务
所谓编程式事务就是手动在代码中完成事务的提交, 发生异常时的回滚.
在实现类中注入 PlatformTransactionManager
- @Autowired
- private PlatformTransactionManager transactionManager;
- @Override
- public Map<String, Object> saveResource(MultipartFile file) {
- TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
- try {
- // 相关业务
- // 手动提交
- transactionManager.commit(status);
- } catch (Exception e) {
- log.error("Exception:{}", ExceptionUtil.stacktraceToString(e));
- // 发生异常时进行回滚
- transactionManager.rollback(status);
- }
- }
声明式事务
所谓声明式事务, 就是使用 @Transactional 注解开启事务, 该注解可以放在类上和方法上, 放在类上时, 该类所有的 public 方法都会开启事务; 放在方法上时, 表示当前方法支持事务
- @Transactional
- @Override
- public Map<String, Object> saveResource(MultipartFile file) {
- // 相关业务
- }
@Transactional 注解
标有该注解的方法通过 AOP 完成事务的回滚与提交
先在 DispatcherServlet 中找到对于的 Controller, 再通过 CgLib 添加拦截器, 在类 ReflectiveMethodInvocation 中, 有多种拦截器的实现, 例如: 参数验证, AOP 前置拦截, 后置拦截, 抛出异常拦截, 环绕拦截等, 还有一个是 TransactionInterceptor 事务拦截器
调用 invoke() 方法
事务的大致执行逻辑
- protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {
- ...
- PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
- // 切点
- final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
- if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
- // Standard transaction demarcation with getTransaction and commit/rollback calls.
- // 创建事务
- TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
- // 环绕切点执行业务逻辑
- Object retVal;
- try {
- // This is an around advice: Invoke the next interceptor in the chain.
- // This will normally result in a target object being invoked.
- retVal = invocation.proceedWithInvocation();
- }
- catch (Throwable ex) {
- // target invocation exception
- // 执行过程中发生异常, 执行回滚
- completeTransactionAfterThrowing(txInfo, ex);
- throw ex;
- }
- finally {
- cleanupTransactionInfo(txInfo);
- }
- if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
- // Set rollback-only in case of Vavr failure matching our rollback rules...
- TransactionStatus status = txInfo.getTransactionStatus();
- if (status != null && txAttr != null) {
- retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
- }
- }
- // 正常执行, 提交事务
- commitTransactionAfterReturning(txInfo);
- return retVal;
- }
- ...
- }
completeTransactionAfterThrowing() 方法
- protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
- // 判断事务状态不为空的情况下
- if (txInfo != null && txInfo.getTransactionStatus() != null) {
- // 输出 debug 日志
- if (logger.isTraceEnabled()) {
- logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
- "] after exception:" + ex);
- }
- // 在指定异常下回滚
- if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
- try {
- // 开始回滚
- txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
- }
- catch (TransactionSystemException ex2) {
- logger.error("Application exception overridden by rollback exception", ex);
- ex2.initApplicationException(ex);
- throw ex2;
- }
- catch (RuntimeException | Error ex2) {
- logger.error("Application exception overridden by rollback exception", ex);
- throw ex2;
- }
- }
- // 不是指定的异常, 任然提交
- else {
- // We don't roll back on this exception.
- // Will still roll back if TransactionStatus.isRollbackOnly() is true.
- try {
- txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
- }
- catch (TransactionSystemException ex2) {
- logger.error("Application exception overridden by commit exception", ex);
- ex2.initApplicationException(ex);
- throw ex2;
- }
- catch (RuntimeException | Error ex2) {
- logger.error("Application exception overridden by commit exception", ex);
- throw ex2;
- }
- }
- }
- }
commitTransactionAfterReturning() 方法
- // 结束执行, 且没有抛出异常, 就执行提交
- protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
- if (txInfo != null && txInfo.getTransactionStatus() != null) {
- if (logger.isTraceEnabled()) {
- logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
- }
- txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
- }
- }
常用属性配置
propagation
配置事务的传播特性, 默认为: required
传播性 | 描述 |
---|---|
required | 在有事务的状态下执行,如果没有就创建新的事务 |
required_news | 创建新的事务,如果当前有事务就将当前事务挂起 |
supports | 如果有事务就在当前事务下运行,没有事务就在无事务状态下运行 |
not_supported | 在无事务状态下运行,如果有事务,将当前事务挂起 |
mandatory | 必须存在事务,若无事务,抛出异常 IllegalTransactionStateException |
never | 不支持事务,有事务就抛出异常 |
nested | 当前有事务就在当事务里面再起一个事务 |
isolation
配置事务个隔离级别, 默认为当前数据库的默认隔离级别 (MySQL 为 REPEATABLE-READ)
查看数据的隔离级别
隔离性 | 描述 |
---|---|
READ UNCOMMITTED(未提交度) | 读取未提交内容,所有事务可看到其他未提交事务的结果,很少实际使用(会出现脏读) |
READ COMMITTED(提交读) | 一个事务只能读取到另一个事务已提交事务的修改过的数据,并且其他事务每次对数据进行修改并提交后,该事务都能查询到最新值 |
REPEATABLE READ(可重复读) | 一个是事务读取其实事务已经提交的修改数据,第一次读取某条记录时,即时其他事务修改了该记录并提交时,之后再读取这条记录时,仍然是第一次读取的值,而不是每次读取不同的数据 |
SERIALIZABLE(串行化) | 事务串行化执行,不会出现踩踏,避免了脏读、幻读和不可重复度,但效率低下 |
timeout
配置事务超时时间, 默认为:-1
readOnly
当方法内部只需要查询数据时, 配置为 true
rollbackFor
配置在那些异常情况下需要回滚数据, 默认情况下只回滚 RuntimeException 和 Error, 开发中最好配置为 Exception
开发过程中遇到的问题
大事务导致事务超时
上周有一个功能, 包含 FTP 文件上传及数据库操作, 不知道是不是网络的原因, FTP 把文件传输到对方服务器的时间特别长, 导致后续的写库操作无法执行. 后面是通过将功能拆分, 不在事务内部执行文件上传, 移到外层就行.
事务失效
非 public 方法失效
在源码内部对于非 public 方式执行返回 null, 不支持对非 public 的事务支持
rollbackFor 配置为默认值
将 rollbackFor 配置为默认值后, 抛出非检查性异常时, 事务无法回滚
在标有注解的方法汇总在 cache 中捕获了异常, 没有基础抛出
- Resource resource = new Resource();
- resource.setCreateUser("admin");
- try {
- resourceMapper.insertUseGeneratedKeys(resource);
- } catch (Exception e) {
- // 在日志中输出堆栈信息, 并抛出异常
- log.error("Exception:{}", ExceptionUtil.stacktraceToString(e));
- throw new RuntimeException("系统错误");
- }
使用了不支持事务的引擎
在 MySQL 中支持事务的引擎是 innodb
参考文章
JavaGuide (gitee.io)
一口气说出 6 种,@Transactional 注解的失效场景 (juejin.cn)
数据库事务的概念和特性 (biancheng.NET) http://c.biancheng.net/view/7289.html
MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.1 Transaction Isolation Levels
阅读原文
来源: https://segmentfault.com/a/1190000040129676