1.概述
接着之前咱们对Spring AOP以及根据AOP完成业务操控的上文,今日咱们来看看平常在项目业务开发中运用声明式业务@Transactional
的失效场景,并剖析其失效原因,然后帮助开发人员尽量防止踩坑。
咱们知道 Spring 声明式业务功用供给了极其便利的业务装备办法,合作 Spring Boot 的自动装备,大多数 Spring Boot 项目只需求在办法上符号 @Transactional
注解,即可一键敞开办法的业务性装备。当然后端开发人员对数据库业务这个概念并不陌生,也知道假设整体考虑多个数据库操作要么成功要么失利时,需求经过数据库业务来完成多个操作的共同性和原子性。如下所示:
@Override
@Transactional(rollbackFor = Exception.class)
public void addUser(UserParam param) {
User user = PtcBeanUtils.copy(param, User.class);
userDAO.insert(user);
if (!CollectionUtils.isEmpty(param.getRoleIds())) {
userRoleService.addUserRole(user.getId(), param.getRoleIds());
}
}
新增用户的一起还增加了用户人物,这儿便是运用@Transactional
来操控业务确保共同性的。但大多数开发仅限于为办法符号 @Transactional
来敞开声明式业务,以为就能够无忧无虑了,不会去重视业务是否有用、出错后业务是否正确回滚,也不会考虑杂乱的业务代码中涉及多个子业务逻辑时,怎么正确处理业务。业务没有被正确处理,一般来说不会过于影响正常流程,也不简单在测试阶段被发现。但当体系越来越杂乱、压力越来越大之后,就会带来许多的数据不共同问题,随后便是许多的人工介入查看和修复数据。
正是由于声明式业务@Transactional
运用简略,所以许多开发人员不重视细节点,可是@Transactional
条条框框还蛮多的,可谓是细节点拉满,假设不注意也不小心就会掉进坑里,今日就让咱们一起来了解运用细节,把坑填平咯。
2.@Transactional
话不多说,先看看该注解界说
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
从上面看出@Transactional
既能够作用于类上,也能够作用于办法上,作用于类: 表示所有该类的**public
**办法都装备相同的业务特点信息。接下来再看看其特点:
propagation: 设置业务的传播行为,首要处理是A办法调用B办法时,业务的传播办法问题的,默许值为 Propagation.REQUIRED
,其他特点值信息如下:
业务传播行为 | 解说 |
---|---|
REQUIRED(默许值) | A调用B,B需求业务,假设A有业务B就参加A的业务中,假设A没有业务,B就自己创立一个业务 |
REQUIRED_NEW | A调用B,B需求新业务,假设A有业务就挂起,B自己创立一个新的业务 |
SUPPORTS | A调用B,B有无业务无所谓,A有业务就参加到A业务中,A无业务B就以非业务办法履行 |
NOT_SUPPORTS | A调用B,B以无业务办法履行,A如有业务则挂起 |
NEVER | A调用B,B以无业务办法履行,A如有业务则抛出反常 |
MANDATORY | A调用B,B要参加A的业务中,假设A无业务就抛出反常 |
NESTED | A调用B,B创立一个新业务,A有业务就作为嵌套业务存在,A没业务就以创立的新业务履行 |
isolation : 业务的阻隔等级,默许值为 Isolation.DEFAULT
。指定业务的阻隔等级,业务并发存在三大问题:脏读、不行重复读、幻读/虚读。能够经过设置业务的阻隔等级来确保并发问题的呈现,常用的是READ_COMMITTED 和REPEATABLE_READ
isolation特点 | 解说 |
---|---|
DEFAULT | 默许阻隔等级,取决于当时数据库阻隔等级,例如MySQL默许阻隔等级是REPEATABLE_READ |
READ_UNCOMMITTED | A业务能够读取到B业务尚未提交的业务记载,不能处理任何并发问题,安全性最低,功用最高 |
READ_COMMITTED | A业务只能读取到其他业务现已提交的记载,不能读取到未提交的记载。能够处理脏读问题,可是不能处理不行重复读和幻读 |
REPEATABLE_READ | A业务屡次从数据库读取某条记载结果共同,能够处理不行重复读,不能够处理幻读 |
SERIALIZABLE | 串行化,能够处理任何并发问题,安全性最高,可是功用最低 |
timeout : 业务的超时时刻,默许值为 -1。假设超过该时刻约束但业务还没有完结,则自动回滚业务。
readOnly: 指定业务是否为只读业务,默许值为 false;为了忽略那些不需求业务的办法,比方读取数据,能够设置 read-only 为 true。
rollbackFor: 用于指定能够触发业务回滚的反常类型,能够指定多个反常类型。
noRollbackFor: 抛出指定的反常类型,不回滚业务,也能够指定多个反常类型。
项目推荐:根据SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级体系架构底层结构封装,处理业务开发经常见的非功用性需求,防止重复造轮子,便利业务快速开发和企业技能栈结构一致办理。引进组件化的思维完成高内聚低耦合并且高度可装备化,做到可插拔。严格操控包依靠和一致版本办理,做到最少化依靠。重视代码标准和注释,十分适合个人学习和企业运用
Github地址:github.com/plasticene/…
Gitee地址:gitee.com/plasticene3…
微信大众号:Shepherd进阶笔记
交流探讨qun:Shepherd_126
3.@Transactional失效场景、原因及批改办法
3.1 同一个类中的办法经过this调用导致失效
public void addUser(UserParam param) {
User user = PtcBeanUtils.copy(param, User.class);
// 新增用户
userDAO.insert(user);
// 增加用户人物
this.addUserRole(user.getId(), param.getRoleIds());
log.info("履行结束了");
}
@Transactional(rollbackFor = Exception.class)
public void addUserRole(Long userId, List<Long> roleIds) {
if (CollectionUtils.isEmpty(roleIds)) {
return;
}
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoles.add(userRole);
});
userRoleDAO.insertBatch(userRoles);
throw new RuntimeException("产生反常咯");
}
履行#addUser()
会发现业务操控失效,产生反常业务并没有回滚,用户和人物绑定都刺进成功了。
这儿,我给出@Transactional
收效准则 1,有必要经过署理过的类从外部调用方针办法才干收效.
Spring 是经过 AOP 技能对办法进行增强完成业务操控的,要调用增强过的办法必然是调用署理后的目标,而这儿this是原生目标,并不是署理,自然就没有业务操控了。
批改办法:①:将this换成署理的userService
, 能够自己注入自己@Resource private UserService userService
,当然也能够不用注入,直接在Spring容器中获取userService
这个bean ②将#addUser()
办法敞开业务即加上@Transactional(rollbackFor = Exception.class)
,这儿本就该敞开,只是为了演示失效状况没加上,由于在#addUser()
里边有刺进用户的操作涉及到业务的所以本要敞开。当然假设#addUser()
只是做一些判别、逻辑处理不涉及到数据库业务操作,那么这样处理就显得有点不太适宜,并且简单导致另一种业务失效的状况,即由于没有正确处理反常,导致业务即便收效也不一定能回滚。
3.2 反常被catch“吃掉了”导致@Transactional失效
如下所示:
@Transactional(rollbackFor = Exception.class)
public void addUser(UserParam param) {
try {
User user = PtcBeanUtils.copy(param, User.class);
// 完结一些逻辑处理
.......
// 增加用户人物
this.addUserRole(user.getId(), param.getRoleIds());
log.info("履行结束了");
} catch (Exception e) {
log.error(e.getMessage());
}
}
@Transactional(rollbackFor = Exception.class)
public void addUserRole(Long userId, List<Long> roleIds) {
if (CollectionUtils.isEmpty(roleIds)) {
return;
}
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoles.add(userRole);
});
userRoleDAO.insertBatch(userRoles);
throw new RuntimeException("产生反常咯");
}
@Transactional
收效准则2:只要反常传播出了符号了 @Transactional 注解的办法,业务才干回滚。之前咱们总结过 根据AOP业务操控完成原理说过在 Spring的 TransactionAspectSupport
里有个 invokeWithinTransaction
办法,里边便是处理业务的逻辑。能够看到,只要捕获到反常才干进行后续业务处理:
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
......
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);
}
......
return result;
}
}
能够看到,只要捕获到反常时才进行回滚操作,假设咱们在业务办法现已捕获掉反常,这儿就捕获不到了,自然就不会回滚了。
批改办法:便是对反常捕获尽量做到局部针对操作,不要抽象把整个办法的代码逻辑都包括进行,这样反常就抛出去了。
3.3 @Transactional 特点 rollbackFor 设置过错,导致反常不满意回滚条件
直接看代码:
@Transactional
public void addUser(UserParam param) {
User user = PtcBeanUtils.copy(param, User.class);
.......
// 增加用户人物
this.addUserRole(user.getId(), param.getRoleIds());
log.info("履行结束了");
}
public void addUserRole(Long userId, List<Long> roleIds) throws Exception {
if (CollectionUtils.isEmpty(roleIds)) {
return;
}
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoles.add(userRole);
});
userRoleDAO.insertBatch(userRoles);
throw new Exception("产生反常咯");
}
这儿#addUser()
运用@transactional
,但没有设置rollbackFor
特点,且#addUserRole()
抛出的反常是exception
,不是RuntimeException
,这样业务也失效了,由于默许状况下,呈现 RuntimeException(非受检反常)或 Error 的时候,Spring才会回滚业务
从上面3.2小节的completeTransactionAfterThrowing(txInfo, ex);
进去完结回滚操作会判别反常类型是否满意规则,DefaultTransactionAttribute
类能看到如下代码块,能够发现相关依据,经过注释也能看到 Spring 这么做的原因,大约的意思是受检反常一般是业务反常,或许说是相似另一种办法的返回值,呈现这样的反常或许业务还能完结,所以不会自动回滚;而Error 或 RuntimeException 代表了非预期的结果,应该回滚:
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
批改办法:设置rollbackFor
:@Transactional(rollbackFor = Exception.class)
3.4 @Transactional 应用在非 public 润饰的办法上
@Transactional(rollbackFor = Exception.class)
private void addUserRole(Long userId, List<Long> roleIds) {
if (CollectionUtils.isEmpty(roleIds)) {
return;
}
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoles.add(userRole);
});
userRoleDAO.insertBatch(userRoles);
throw new RuntimeException("产生反常咯");
}
idea也会提示爆红:
Spring经过CGLIB动态署理来增强出产署理目标,CGLIB 经过继承办法完成署理类,private 办法在子类不行见,自然也就无法进行业务增强。s在根据AOP业务操控完成原理一文中也剖析过,会调用到AbstractFallbackTransactionAttributeSource
的computeTransactionAttribute()
办法
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
......
}
批改办法:自然是改成public
3.5 @Transactional 注解传播特点 propagation 设置过错
如上面咱们新增的用户的一起要增加用户人物,可是假设咱们期望即使增加人物过错了,还能够正常新增用户。
public void addUser(UserParam param) {
String username = param.getUsername();
checkUsernameUnique(username);
User user = PtcBeanUtils.copy(param, User.class);
// 增加用户
userDAO.insert(user);
// 增加用户人物
userRoleService.addUserRole(user.getId(), param.getRoleIds());
}
#userRoleService.addUserRole()
@Transactional(rollbackFor = Exception.class)
private void addUserRole(Long userId, List<Long> roleIds) {
if (CollectionUtils.isEmpty(roleIds)) {
return;
}
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoles.add(userRole);
});
userRoleDAO.insertBatch(userRoles);
throw new RuntimeException("产生反常咯");
}
你会发现只会一起刺进失利,无法完成上面所说的。这时候你或许会想到,既然addUserRole()
抛出了反常不能刺进用户人物,可是addUser()
不想受影响,正常增加用户,那么何不在addUser()
里边对userRoleService.addUserRole()
进行反常捕获,不就能够处理问题了吗?真是如此吗,就让咱们来验证一下:
@Transactional(rollbackFor = Exception.class)
public void addUser(UserParam param) {
User user = PtcBeanUtils.copy(param, User.class);
// 增加用户
userDAO.insert(user);
// 增加用户人物
try {
userRoleService.addUserRole(user.getId(), param.getRoleIds());
} catch (Exception e) {
log.error(e.getMessage());
}
}
履行会发现,用户相同没有增加成功,看日志报错:
[1689568520410750976] [ERROR] [2023-08-10 17:25:02.023] [http-nio-18888-exec-1@56682] com.plasticene.fast.service.impl.UserServiceImpl addUser : 产生反常咯
[1689568520410750976] [ERROR] [2023-08-10 17:25:02.097] [http-nio-18888-exec-1@56682] com.plasticene.boot.web.core.global.GlobalExceptionHandler exceptionHandler : 【体系反常】
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
能够看到产生反常咯
是咱们在addUser()
中捕获到输出的,可是紧接着下一行发现有报出一个反常UnexpectedRollbackException
。
原因是,主办法增加用户的逻辑和子办法增加用户人物的逻辑是同一个业务,子逻辑符号了业务需求回滚,主逻辑自然也不能提交了。
批改办法:其实要想新增用户人物失利不影响增加用户,只需求让新增用户人物独自敞开一个新业务即可。
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void addUserRole(Long userId, List<Long> roleIds) {
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoles.add(userRole);
});
userRoleDAO.insertBatch(userRoles);
throw new RuntimeException("产生反常啦!");
}
3.6 @Transactional长业务导致出产事故
许多开发都觉得Spring的声明式业务运用十分简略,即@Transactional
,所以从来不重视细节。当 Spring 遇到该注解时,会自动从数据库衔接池中获取 connection,并敞开业务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个办法都是运用同一个connection衔接。假设咱们呈现了耗时的操作,比方第三方接口调用、业务逻辑杂乱、大批量数据处理等就会导致咱们咱们占用这个connection的时刻会很长,数据库衔接一向被占用不释放。一旦相似操作过多,就会导致数据库衔接池耗尽。这便是典型的长业务问题
长业务引发的常见损害有:
- 数据库衔接池被占满,应用无法获取衔接资源;
- 简单引发数据库死锁;
- 数据库回滚时刻长;
- 在主从架构中会导致主从延时变大。
服务体系开端呈现故障:数据库监控平台一向收到告警短信,数据库衔接不足,呈现许多死锁;日志显示调用流程引擎接口呈现许多超时;一起一向提示CannotGetJdbcConnectionException
,数据库衔接池衔接占满。
要想处理这个问题其实也不难,只需求对办法进行拆分,将不需求业务办理的逻辑与业务操作分隔,这样就能够有用操控业务的时长然后防止长业务。当然对一个办法逻辑拆分红多个子办法很有或许形成上面叙说的业务不收效的状况,不过我相信你看到上面的总结必定没问题啦。
4.总结
Spring的声明式业务运用@Transactional
注解在开发时的确很便利,可是稍有不小心运用不当就会导致业务失效数据不共同、乃至是体系数据库功用问题。所以上面满满的干货总结都是出自日常工作中碰到的,有用帮你避坑。