Spring事务使用最佳实践
Posted Andyzty
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring事务使用最佳实践相关的知识,希望对你有一定的参考价值。
目录
1 Spring事务最佳实践
1.1、Spring事务传播机制
Spring定义了七种传播行为:默认为:PROPAGATION_REQUIRED
传播行为 | 含义 |
---|---|
TransactionDefinition.PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中。这是最常见的选择。(默认传播行为) |
TransactionDefinition.PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
TransactionDefinition.PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
TransactionDefinition.PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。 |
TransactionDefinition.PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果当前存在事务,就把当前事务挂起。 |
TransactionDefinition.PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
TransactionDefinition.PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
传播行为是指:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。该问题只有一个函数不存在函数之间的调用,故排除传播行为的问题。
1.2、隔离规则
Spring的 isolation用于指定事务的隔离规则,默认值为DEFAULT。总共五种隔离规则,如下所示:
@isolation属性 | 事务属性-隔离规则 | 含义 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|---|
DEFAULT | TransactionDefinition.ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
|
|
|
READ_UNCOMMITTED | TransactionDefinition.ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的数据变更(最低的隔离级别) | 是 | 是 | 是 |
READ_COMMITTED | TransactionDefinition.ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据 | 否 | 是 | 是 |
REPEATABLE_READ | TransactionDefinition.ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改 | 否 | 否 | 否 |
SERIALIZABLE | TransactionDefinition.ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 | 否 | 否 | 否 |
mysql默认隔离级别,是可重复读(RR)
1.3、Spring事务实现方式
目前Spring有两种方式实现事务,分别是编程式和声明式两种,Spring事务的本质其实就是数据库对事务的使用,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的
1.3.1、编程式事务实现
编程式事务就是以代码编程的方式来控制事务的运行。举个例子
//dataSource配置略
//事务管理器
<bean id="tradeTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="tradeShardDataSource"/>
</bean>
//事务模板类
<bean id="tradeTransactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="tradeTransactionManager"/>
<property name="isolationLevelName" value="ISOLATION_READ_COMMITTED"/>
</bean>
通过手动代码就完成了以编程式事务对业务方法的管理。它的缺点也很明显,事务代码和业务代码糅杂在一起,破坏了业务代码条理性,而且也不利于维护和扩展,优点作用范围可控不容易出问题,适用于业务简单、少量事务的场景,而且事务作用范围能够控制在代码块级别
public boolean saveBscancOrderAndPayOrder(final BscancOrderPO bscancOrderPO, final PayOrderPO payOrderPO)
boolean result = false;
try
result = tradeTransactionTemplate.execute(new TransactionCallback<Boolean>()
@Override
public Boolean doInTransaction(TransactionStatus transactionStatus)
try
int row = bscancOrderWrapper.insert(bscancOrderPO);
if (row <= 0)
return false;
row = payOrderWrapper.insert(payOrderPO);
if (row <= 0)
transactionStatus.setRollbackOnly();//关联插入失败回滚
return false;
return true;
catch (Exception e)
transactionStatus.setRollbackOnly();//出现异常回滚
return false;
);
catch (Exception e)
LOGGER.error("saveBscancOrderAndPayOrder 获取事务异常订单插入失败, bscancOrderPO:, payOrderPO:", JsonUtil.toJson(bscancOrderPO), JsonUtil.toJson(payOrderPO), e);
return result;
1.3.2、声明式事务实现
利用Spring AOP对方法进行拦截。在方法开始之前创建或者加入一个事务,在方法执行完毕之后根据情况提交或回滚事务,声明式事务配置
//datasorce省略
//事务管理器
<bean id="mybatisTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>
//声明式事务配置
<tx:annotation-driven id="txAdvice" transaction-manager="mybatisTransactionManager"/>
//通过AOP定义事务作用范围
<aop:config>
<aop:pointcut id="serviceMethods"
expression="execution(* com.zzc.qrcode.order.core.dao.service..*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
@Transactional(rollbackFor = Exception.class)
public void saveBscancOrderAndPayOrder(final BscancOrderPO bscancOrderPO, final PayOrderPO payOrderPO)
bscancOrderWrapper.insert(bscancOrderPO);
payOrderWrapper.insert(payOrderPO);
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别
@Transactional 注解的属性信息
属性 | 描述 |
---|---|
name | 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器 |
propagation | 事务的传播行为,默认值为 REQUIRED |
isolation | 事务的隔离度,默认值采用 DEFAULT |
timeout | 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务,不配置默认是30秒 |
read-only | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true |
rollback-for | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔 |
no-rollback- for | 抛出 no-rollback-for 指定的异常类型,不回滚事务 |
1.3.3、Spring声明式事务使用注意事项
因配置不正确,导致方法上的事务没生效
-
@Transactional注解标记的方法是public的,Spring默认通过动态代理的方式实现AOP,对目标方法进行增强,private方法无法代理到,Spring自然也无法动态增强事务处理逻辑
-
@Transactional必须通过代理过的类从外部调用目标方法才能生效。即Spring注入的Bean进行调用的方法
/**
* @author zhangtianyou on 2021/4/23.
*/
public interface OrderService
void insertPayOrder();
void saveOrder();
import javax.transaction.Transactional;
/**
* @author zhangtianyou on 2021/4/23.
*/
@Service("orderService")
public class OrderServiceImpl implements OrderService
@Override
@Transactional
public void insertPayOrder()
//DB插入支付订单
//DB插入订单流水
@Override
public void saveOrder()
//dosomething
insertPayOrder();
//dosomething
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author zhangtianyou on 2021/4/23.
*/
public class TransactionTest
@Autowired
private OrderService orderService;
public void saveOrderTest()
//do something
orderService.saveOrder();
//do something
上面insertPayOrder()处理异常不会回滚,因为事务不生效,应该直接代理的saveOrder()方法无事务,而调用虽然调用了带有注解的内部方法insertPayOrder(),但因insertPayOrder()不是通过代理调用,顾不生效。
解决上述问题有两种办法,1.在orderService.saveOrder()加上事务@Transactional注解,但会增加事务的范围。2.将orderService.saveOrder()单独抽一层服务biz层中即orderServiceBiz.saveOrder(),在通过orderService.insertPayOrder调用
可以打开debug日志 logging.level.org.springframework.orm.jpa=DEBUG 观察
因异常处理不正确,导致事务虽然生效但出现异常时没回滚
Spring默认只会对标记@Transactional注解的方法出现了RuntimeException和Error的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望Spring针对其他异常也可以回滚,那么可以相应配置@Transactional注解的rollbackFor和noRollbackFor属性来覆盖其默认设置。
-
对事务内做了try..catch,SpringAOP无法感知事务
@Override @Transactional public void insertPayOrder() try //DB插入支付订单 //DB插入订单流水 catch (Exception e)
默认情况下,出现RuntimeException(非受检异常)或Error的时候,Spring才会回滚事务,如果要对其他业务异常也回滚,则需要配置 rollback-for
@Override
@Transactional(rollbackFor = Exception.class)
public void insertPayOrder() throws Exception
//DB插入支付订单
//DB插入订单流水
//do somethring
因事务传播方式配置不正确,导致事务不符合预期
如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是@Transactional注解的Propagation属性
2、事务问题治理
2.1、大事物的危害
2.1.1 事务问题原因分类
根据使用经验,将主要事物问题汇总如下:
事务问题分类 | 详细问题 | 备注 |
---|---|---|
无需事务 | 单条更新、插入、删除无需单独显试引入事务,因为对于这类SQL数据库本身开启事务处理 |
|
对于全部为查询操作,无需引入事务 |
| |
对于跨数据库使用事务(目前项目中未发现) |
| |
事务范围太大 | 在一个事务里面, 包含太多流程处理,增加事务处理时长 |
|
在一个事务里面,包含不必要且耗时的查询DB功能 |
| |
事务范围越大,出现死锁的可能性会越高 | ||
多个事务循环嵌套(和事物传播设置相关) |
| |
事务使用不当 | 事务类包含了耗时太多的操作,目前梳理出来的有:
|
|
使用了Spring声明式事务,但是方法内部使用try、catch导致事务无效 |
| |
@Transactional 注解使用不当,导致事务无效 |
|
2.1.2、大事物带来的潜在风险
-
并发情况下,数据库连接池容易被撑爆(如果配置连接池大小虽然不会撑爆、但会占用大量连接池资源)
-
锁定太多的数据,造成大量的阻塞和锁超时
-
执行时间长,容易造成主从延迟,而且造成大量锁等待
-
回滚所需要的时间比较长
-
undo log日志膨胀,不仅增加了存储的空间,而且可能降低查询的性能
-
zebra 事务是走主库,滥用事务有可能导致主库压力过大,数据库整体处理能力下降等
2.2、治理方案
目前从以下几个方面优化:
移除无效声事务
-
只有读取操作
-
只有一条单条的更新操作
-
多次更新操作不在同一个数据库
缩短事务作用范围
-
减少事务处理范围,将需要处理的事务SQL操作单数封装方法,事务内提出无效的流程和查询
事务内耗时操作
-
RPC调用/接口埋点/leaf调用/线程池任务/Spring事件
事务最佳实践
-
Spring声明式事务最佳实践,异常处理等
-
将事务使用方法专门抽象一层,避免分散
以上是关于Spring事务使用最佳实践的主要内容,如果未能解决你的问题,请参考以下文章
API设计与开发实践第2篇 Restful API 设计最佳实践的四个重要改进