免费空间领取网站,wordpress怎么加积分,asp网站建设参考文献,html做的小网站1、概述
我们知道 Spring 声明式事务功能提供了极其方便的事务配置方式#xff0c;配合 Spring Boot 的自动配置#xff0c;大多数 Spring Boot 项目只需要在方法上标记 Transactional注解#xff0c;即可一键开启方法的事务性配置。当然后端开发人员对数据库事务这个概念并…1、概述
我们知道 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条条框框还蛮多的可谓是细节点拉满如果不注意也不小心就会掉进坑里今天就让我们一起来了解使用细节把坑填平咯
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_READ READ_UNCOMMITTED A事务可以读取到B事务尚未提交的事务记录不能解决任何并发问题安全性最低性能最高 READ_COMMITTED A事务只能读取到其他事务已经提交的记录不能读取到未提交的记录。可以解决脏读问题但是不能解决不可重复读和幻读 REPEATABLE_READ A事务多次从数据库读取某条记录结果一致可以解决不可重复读不可以解决幻读 SERIALIZABLE 串行化可以解决任何并发问题安全性最高但是性能最低 timeout 事务的超时时间默认值为 -1。如果超过该时间限制但事务还没有完成则自动回滚事务。
readOnly 指定事务是否为只读事务默认值为 false为了忽略那些不需要事务的方法比如读取数据可以设置 read-only 为 true。
rollbackFor 用于指定能够触发事务回滚的异常类型可以指定多个异常类型。
noRollbackFor 抛出指定的异常类型不回滚事务也可以指定多个异常类型。 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是原生对象并不是代理自然就没有事务控制了。
解决方法
1、将addUser()方法开启事务即加上Transactional(rollbackFor Exception.class)这里本就该开启。里面的方式自动延用主方法的事务配置
2、将this换成代理的userService, 可以自己注入自己Resource private UserService userService当然也可以不用注入直接在Spring容器中获取userService这个bean
3、通过开启aop的代理来获取当前service对象然后来获取对象思路和第二种方式类似
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;}} 可以看到只有捕获到异常时才进行回滚操作如果我们在业务方法已经捕获掉异常这里就捕获不到了自然就不会回滚了。
解决方法
就是对异常捕获尽量做到局部针对操作不要笼统把整个方法的代码逻辑都包括进行这样异常就抛出去了。如果是实在要对代码块进行catch就用手动回滚的方式去恢复数据也可以理解为TCC模式
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)。这一点同样很通用就如果下面的方法抛出的是exception的子类用这个来接收统一处理也是可以的。
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也会提示爆红。提示private的问题。
原因是因为Spring通过CGLIB动态代理来增强生产代理对象CGLIB 通过继承方式实现代理类private 方法在子类不可见自然也就无法进行事务增强。会调用到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute()方法
解决方法改成public 3.5 Transactional 注解传播属性 propagation 设置错误
如上面我们新增的用户的同时要添加用户角色但是假如我们希望即使添加角色错误了还可以正常新增用户。 会发现都是同时成功或是同时失败。
原因是主方法添加用户的逻辑和子方法添加用户角色的逻辑是同一个事务子逻辑标记了事务需要回滚主逻辑自然也不能提交了。
解决方法添加一个新的事务即可。这个场景在mybatisplus的DB切换多数据源的事务时候也会出现如果有遇到过
Transactional(rollbackFor Exception.class, propagation Propagation.REQUIRES_NEW)
3.6 Transactional长事务导致生产事故
很多开发都觉得Spring的声明式事务使用非常简单即Transactional所以从来不注重细节。当 Spring 遇到该注解时会自动从数据库连接池中获取 connection并开启事务然后绑定到 ThreadLocal 上对于Transactional注解包裹的整个方法都是使用同一个connection连接。
如果我们出现了耗时的操作比如第三方接口调用、业务逻辑复杂、大批量数据处理等就会导致我们我们占用这个connection的时间会很长数据库连接一直被占用不释放。一旦类似操作过多就会导致数据库连接池耗尽。这就是典型的长事务问题
长事务引发的常见危害有
数据库连接池被占满应用无法获取连接资源容易引发数据库死锁数据库回滚时间长在主从架构中会导致主从延时变大。
服务系统开始出现故障数据库监控平台一直收到告警短信数据库连接不足出现大量死锁日志显示调用流程引擎接口出现大量超时同时一直提示CannotGetJdbcConnectionException数据库连接池连接占满。
要想解决这个问题其实也不难只需要对方法进行拆分将不需要事务管理的逻辑与事务操作分开这样就可以有效控制事务的时长从而避免长事务。当然对一个方法逻辑拆分成多个子方法很有可能造成上面叙述的事务不生效的情况
Spring的声明式事务使用Transactional注解在开发时确实很方便但是稍有不慎使用不当就会导致事务失效数据不一致、甚至是系统数据库性能问题。所以上面满满的干货总结都是出自日常工作中碰到的有效帮你避坑。