网站建设哪些字体没有版权阅读网站建设
web/
2025/10/3 5:21:13/
文章来源:
网站建设哪些字体没有版权,阅读网站建设,拼多多无货源电商怎么做,潍坊网站建设 世纪环球16楼1.概述
接着之前我们对Spring AOP以及基于AOP实现事务控制的上文#xff0c;今天我们来看看平时在项目业务开发中使用声明式事务Transactional的失效场景#xff0c;并分析其失效原因#xff0c;从而帮助开发人员尽量避免踩坑。
我们知道 Spring 声明式事务功能提供了极其…1.概述
接着之前我们对Spring AOP以及基于AOP实现事务控制的上文今天我们来看看平时在项目业务开发中使用声明式事务Transactional的失效场景并分析其失效原因从而帮助开发人员尽量避免踩坑。
我们知道 Spring 声明式事务功能提供了极其方便的事务配置方式配合 Spring Boot 的自动配置大多数 Spring Boot 项目只需要在方法上标记 Transactional 注解即可一键开启方法的事务性配置。当然后端开发人员对数据库事务这个概念并不陌生也知道如果整体考虑多个数据库操作要么成功要么失败时需要通过数据库事务来实现多个操作的一致性和原子性。如下所示 OverrideTransactional(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调用BB需要事务如果A有事务B就加入A的事务中如果A没有事务B就自己创建一个事务REQUIRED_NEWA调用BB需要新事务如果A有事务就挂起B自己创建一个新的事务SUPPORTSA调用BB有无事务无所谓A有事务就加入到A事务中A无事务B就以非事务方式执行NOT_SUPPORTSA调用BB以无事务方式执行A如有事务则挂起NEVERA调用BB以无事务方式执行A如有事务则抛出异常MANDATORYA调用BB要加入A的事务中如果A无事务就抛出异常NESTEDA调用BB创建一个新事务A有事务就作为嵌套事务存在A没事务就以创建的新事务执行
**isolation **事务的隔离级别默认值为 Isolation.DEFAULT。指定事务的隔离级别事务并发存在三大问题脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现常用的是READ_COMMITTED 和REPEATABLE_READ
isolation属性解释DEFAULT默认隔离级别取决于当前数据库隔离级别例如MySQL默认隔离级别是REPEATABLE_READREAD_UNCOMMITTEDA事务可以读取到B事务尚未提交的事务记录不能解决任何并发问题安全性最低性能最高READ_COMMITTEDA事务只能读取到其他事务已经提交的记录不能读取到未提交的记录。可以解决脏读问题但是不能解决不可重复读和幻读REPEATABLE_READA事务多次从数据库读取某条记录结果一致可以解决不可重复读不可以解决幻读SERIALIZABLE串行化可以解决任何并发问题安全性最高但是性能最低
**timeout **事务的超时时间默认值为 -1。如果超过该时间限制但事务还没有完成则自动回滚事务。
**readOnly**指定事务是否为只读事务默认值为 false为了忽略那些不需要事务的方法比如读取数据可以设置 read-only 为 true。
**rollbackFor**用于指定能够触发事务回滚的异常类型可以指定多个异常类型。
**noRollbackFor**抛出指定的异常类型不回滚事务也可以指定多个异常类型。 项目推荐基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装解决业务开发时常见的非功能性需求防止重复造轮子方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化做到可插拔。严格控制包依赖和统一版本管理做到最少化依赖。注重代码规范和注释非常适合个人学习和企业使用 Github地址https://github.com/plasticene/plasticene-boot-starter-parent Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent 微信公众号Shepherd进阶笔记 交流探讨qunShepherd_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, ListLong roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}ListUserRole 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, ListLong roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}ListUserRole 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 设置错误导致异常不满足回滚条件
直接看代码 Transactionalpublic void addUser(UserParam param) {User user PtcBeanUtils.copy(param, User.class);.......// 添加用户角色this.addUserRole(user.getId(), param.getRoleIds());log.info(执行结束了);}public void addUserRole(Long userId, ListLong roleIds) throws Exception {if (CollectionUtils.isEmpty(roleIds)) {return;}ListUserRole 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);}修正方法设置rollbackForTransactional(rollbackFor Exception.class)
3.4 Transactional 应用在非 public 修饰的方法上 Transactional(rollbackFor Exception.class)private void addUserRole(Long userId, ListLong roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}ListUserRole 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()方法 Nullableprotected TransactionAttribute computeTransactionAttribute(Method method, Nullable Class? targetClass) {// Dont 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, ListLong roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}ListUserRole 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-156682] com.plasticene.fast.service.impl.UserServiceImpl addUser : 发生异常咯
[1689568520410750976] [ERROR] [2023-08-10 17:25:02.097] [http-nio-18888-exec-156682] com.plasticene.boot.web.core.global.GlobalExceptionHandler exceptionHandler : 【系统异常】
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-onlyat org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)可以看到发生异常咯是我们在addUser()中捕获到输出的但是紧接着下一行发现有报出一个异常UnexpectedRollbackException。
原因是主方法添加用户的逻辑和子方法添加用户角色的逻辑是同一个事务子逻辑标记了事务需要回滚主逻辑自然也不能提交了。
修正方式其实要想新增用户角色失败不影响添加用户只需要让新增用户角色单独开启一个新事务即可。 Transactional(rollbackFor Exception.class, propagation Propagation.REQUIRES_NEW)public void addUserRole(Long userId, ListLong roleIds) {ListUserRole 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注解在开发时确实很方便但是稍有不慎使用不当就会导致事务失效数据不一致、甚至是系统数据库性能问题。所以上面满满的干货总结都是出自日常工作中碰到的有效帮你避坑。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/86031.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!