临沂网站推广,网站建站加盟,红色礼品网站模板,网站备案 图片大小文章目录 一、Spring 事务是什么二、Spring 中事务的实现方法2.1 Spring 编程式事务#xff08;手动#xff09;2.1.1 编程式事务的使用演示2.1.2 编程式事务存在的问题 2.2 Spring 声明式事务#xff08;自动#xff09;2.2.1 Transactional 作用范围2.2.2 Transactional … 文章目录 一、Spring 事务是什么二、Spring 中事务的实现方法2.1 Spring 编程式事务手动2.1.1 编程式事务的使用演示2.1.2 编程式事务存在的问题 2.2 Spring 声明式事务自动2.2.1 Transactional 作用范围2.2.2 Transactional 参数说明2.2.3 Transactional 捕获异常时回滚失效问题2.4.4 Transactional 工作原理 2.3 Spring 事务失效场景 三、事务的隔离级别3.1 事务的特性回顾3.2 MySQL 的事务隔离级别3.3 Spring 事务的隔离级别 四、Spring 事务的传播机制4.1 为什么需要事务传播机制4.2 事务传播机制的分类4.3 Spring 事务传播机制使用案例 一、Spring 事务是什么
在 Spring 框架中事务Transaction是一种用于管理数据库操作的机制旨在确保数据的一致性、可靠性和完整性。事务可以将一组数据库操作如插入、更新、删除等视为一个单独的执行单元要么全部成功地执行要么全部回滚。这样可以确保数据库在任何时候都保持一致的状态即使在发生故障或错误时也能保持数据的完整性。
Spring 框架通过提供事务管理功能使开发者能够更轻松地管理事务的边界。Spring 主要提供了两种主要的事务管理方式 编程式事务管理通过编写代码显式地管理事务的开始、提交和回滚操作。这种方式提供了更大的灵活性但也需要更多的代码维护。 声明式事务管理通过在配置中声明事务的行为由 Spring 框架自动处理事务的边界减少了开发者的工作量并提高了代码的可维护性。
二、Spring 中事务的实现方法
2.1 Spring 编程式事务手动
2.1.1 编程式事务的使用演示
在 Spring 中编程式事务管理是一种手动控制事务边界的方式与 MySQL 操作事务的方法类似它涉及三个重要的操作步骤 开启事务获取事务首先需要通过获取事务管理器例如 DataSourceTransactionManager来获取一个事务从而开始一个新的事务。事务管理器是用于管理事务的核心组件。 提交事务一旦一组数据库操作成功执行并且希望将这些更改永久保存到数据库中就可以调用事务对象的提交方法。这将使得事务中的所有操作都被应用到数据库。 回滚事务如果在事务处理过程中发生错误或某种条件不满足就可以调用事务对象的回滚方法从而撤销事务中的所有操作回到事务开始前的状态。
在 Spring Boot 中可以利用内置的事务管理器 DataSourceTransactionManager 来获取事务提交或回滚事务。此外TransactionDefinition 是用来定义事务的属性的当获取事务时需要将 TransactionDefinition 传递进DataSourceTransactionManager以获取一个事务状态 TransactionStatus。
例如下面的代码演示了编程式事务
RestController
RequestMapping(/user)
public class UserController {// 编程式事务Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;Autowiredprivate TransactionDefinition transactionDefinition;Autowiredprivate UserService userService;RequestMapping(/del)public int delById(RequestParam(id) Integer id) {if (id null || id 0) return 0;// 1. 开启事务TransactionStatus transactionStatus null;int res 0;try {transactionStatus dataSourceTransactionManager.getTransaction(transactionDefinition);// 2. 业务操作 —— 删除用户res userService.delById(id);System.out.println(删除: res);// 3. 提交、回滚事务// 提交事务dataSourceTransactionManager.commit(transactionStatus);} catch (Exception e) {e.printStackTrace();// 回滚事务if (transactionStatus ! null) {dataSourceTransactionManager.rollback(transactionStatus);}}return res;}
}
这段代码展示了如何通过编程式事务管理在Spring Boot中处理用户删除操作。编程式事务允许我们在代码中明确地控制事务的边界以及在需要时手动提交或回滚事务。
2.1.2 编程式事务存在的问题
通过上面的示例代码可以发现编程式事务虽然提供了更大的灵活性但也存在一些问题和挑战 代码冗余和可读性差 编程式事务需要在代码中显式地添加事务管理的逻辑导致代码变得冗余且难以维护。每次需要使用事务的地方都需要重复编写事务开启、提交和回滚的代码降低了代码的可读性。 事务边界控制复杂 开发者需要手动管理事务的边界确保事务的开始、提交和回滚都在正确的位置。这可能会导致遗漏事务管理的代码从而影响数据的一致性。 事务传播和嵌套问题 在涉及多个方法调用的场景中手动控制事务的传播和嵌套关系可能变得复杂。需要开发者确保事务在各个方法间正确传播同时处理好嵌套事务的问题。 异常处理繁琐 编程式事务需要在异常处理时手动进行回滚操作如果异常处理不当事务可能无法正确回滚导致数据不一致。 可维护性差 随着项目的发展业务逻辑可能会变得更加复杂可能需要频繁地修改事务管理的代码。这会增加代码维护的难度可能导致错误的引入。 不利于横向扩展 编程式事务难以支持横向扩展因为事务管理的代码紧耦合在业务逻辑中扩展时可能需要修改大量代码。
相比之下声明式事务管理通过在方法上添加注解或在配置文件中进行声明使事务管理与业务逻辑分离提供了更好的代码组织和可维护性。声明式事务可以在切面中自动处理事务的开始、提交和回滚从而减轻了开发者的工作负担。
所以大多数情况下建议使用声明式事务管理来处理事务特别是在简化事务逻辑和提高代码可读性方面更加有效。
2.2 Spring 声明式事务自动
声明式事务的实现非常简单只需要在需要的方法上添加 Transactional 注解就可以轻松实现无需手动开启或提交事务。 当进入被注解的方法时Spring 会自动开启一个事务。方法执行完成后如果没有抛出未捕获的异常事务会自动提交保证数据的一致性。然而如果方法在执行过程中发生了未经处理的异常事务会自动回滚以确保数据库的完整性和一致性。 这种方式大大简化了事务管理的编码减少了手动处理事务的繁琐操作提高了代码的可读性和可维护性。例如下面的代码实现
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;// 声明式事务RequestMapping(/del)Transactionalpublic int delById(Integer id) {if (id null || id 0) return 0;int result userService.delById(id);return result;}
}在这个示例中delById 方法使用了 Transactional 注解表示该方法需要受到声明式事务的管理。在这个方法内部首先检查了传入的 id如果为负数则直接返回结果。然后调用了 userService.delById(id) 方法删除了指定用户。在方法结束时事务会自动提交。
同时如果在执行过程中发生了未处理的异常事务将会自动回滚以保持数据库的一致性。这种方式简化了事务管理提高了代码的可读性和可维护性。
2.2.1 Transactional 作用范围
Transactional 注解可以被用来修饰方法或类 当修饰方法时需要注意它只能应用到 public 访问修饰符的方法上否则注解不会生效。通常推荐在方法级别使用 Transactional。 当修饰类时表示该注解对于类中所有的 public 方法都会生效。如果在类级别添加了 Transactional那么该类中所有的公共方法都将自动应用事务管理。
一般来说推荐将 Transactional 注解应用在方法级别以便更精确地控制事务的范围从而避免不必要的事务开销。如果类中的所有方法都需要事务管理那么将注解应用在类级别是一个更方便的选择。
2.2.2 Transactional 参数说明
通过查看 Transactional 的源码可以发现它支持多个参数用来配置事务的行为。
以下是对其中参数说明
参数名称类型默认值描述valueString“”事务管理器的名称与 transactionManager 等效。transactionManagerString“”事务管理器的名称与 value 等效。labelString[]空数组事务标签暂无具体用途。propagationPropagationPropagation.REQUIRED事务的传播行为默认为 REQUIRED。isolationIsolationIsolation.DEFAULT事务的隔离级别默认为数据库默认隔离级别。timeoutint-1事务的超时时间单位为秒。-1 表示没有超时限制。timeoutStringString“”事务的超时时间的字符串表示与 timeout 等效。readOnlybooleanfalse是否将事务设置为只读默认为 false。rollbackForClass? extends Throwable[]空数组触发回滚的异常类型。rollbackForClassNameString[]空数组触发回滚的异常类型的类名字符串。noRollbackForClass? extends Throwable[]空数组不触发回滚的异常类型。noRollbackForClassNameString[]空数组不触发回滚的异常类型的类名字符串。
这些参数提供了对事务行为的灵活配置可以根据具体业务需求来调整事务的传播、隔离、超时和回滚策略等。
2.2.3 Transactional 捕获异常时回滚失效问题
针对于上述的实例代码现在代码中间模拟实现一个异常观察会出现什么情况
RequestMapping(/del)
Transactional
public int delById(Integer id) {if (id null || id 0) return 0;int result userService.delById(id);System.out.println(result);try {int num 10 / 0;} catch (Exception e) {// 如果直接处理异常则不会回滚e.printStackTrace();}return result;
}通过浏览器访问发现服务器成功捕获了异常 但是事务却没有回滚对应的用户数据还是被删除了 其原因在于
在异常处理中直接捕获了异常并进行了处理从而导致事务回滚失效。默认情况下Transactional 注解会在方法内抛出 RuntimeException 及其子类异常时触发事务回滚。然而当自己在 catch 块内捕获异常并处理时Spring 无法感知到异常从而无法触发事务回滚。
解决方法
对于这个问题的解决方法大致可以分为两种
将捕获的异常再次抛出
e.printStackTrace();
throw e;这种方法通过重新抛出异常使得 Spring 能够捕获异常并触发事务回滚。在异常发生后事务将被回滚确保之前的数据库操作不会生效从而保持数据的一致性。
使用 TransactionAspectSupport 手动回滚事务
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();这种方法利用了 Spring 提供的 TransactionAspectSupport 类来手动设置事务回滚状态。在捕获异常后通过调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()可以将当前事务设置为回滚状态从而达到回滚事务的效果。这种方法更加灵活可以在需要的时候手动控制事务的回滚。
无论选择哪种方法都可以在异常发生时触发事务回滚保障数据的完整性和一致性。选择哪种方法取决于具体的代码逻辑和需求。
2.4.4 Transactional 工作原理
Transactional 注解的工作原理基于 Spring AOP面向切面编程和事务管理器。它利用了 Spring 框架的代理机制来实现事务管理。
当一个被 Transactional 注解修饰的方法被调用时Spring 会创建一个代理对象来包装这个方法。代理对象会在方法执行之前和之后添加事务管理的逻辑以确保事务的开始、提交和回滚。这个过程是通过 AOP 技术实现的。 具体来说以下是 Transactional 注解的工作流程 事务代理的创建 Spring 在运行时会为每个被 Transactional 注解修饰的类创建一个代理对象。这个代理对象会包含事务管理的逻辑。 方法调用 当调用一个被 Transactional 注解修饰的方法时实际上是通过代理对象来调用。 事务切面的触发 在代理对象中事务切面会在方法执行前后被触发。在方法执行前切面会开启一个事务在方法执行后切面会根据方法的执行情况决定是提交事务还是回滚事务。 事务管理器的使用 切面会通过事务管理器来控制事务。事务管理器负责实际的事务管理操作如开启、提交和回滚事务。 事务控制 如果方法正常执行完毕切面会通知事务管理器提交事务。如果方法在执行过程中抛出异常切面会通知事务管理器回滚事务。
总体来说Transactional 注解的工作原理是通过代理和切面来实现事务管理将事务的控制与业务逻辑分离使代码更加模块化和可维护。这也是声明式事务管理的核心机制之一。
2.3 Spring 事务失效场景
在某些情况下Spring 中的事务可能会失效导致事务不生效或不按预期执行。以下是一些可能导致事务失效的场景 非 public 修饰的方法 默认情况下Transactional 注解只对 public 访问修饰符的方法起作用。如果你在非 public 方法上添加了 Transactional 注解事务可能不会生效。 timeout 超时 如果事务执行的时间超过了设置的 timeout 值事务可能会被强制回滚。这可能会导致事务不按预期执行特别是当事务需要执行较长时间的操作时。 代码中有 try/catch 如果在方法内部捕获并处理了异常Spring 将无法感知到异常从而无法触发事务回滚。这可能导致事务在异常发生时不会回滚。 调用类内部带有 Transactional 的方法 当一个类内部的方法被调用时它的 Transactional 注解可能不会生效。这是因为 Spring 默认使用基于代理的事务管理直接在类内部调用方法不会经过代理从而事务管理可能不会生效。
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;public int del(Integer id){return delById(id);}// 声明式事务RequestMapping(/del)Transactionalpublic int delById(Integer id) {if (id null || id 0) return 0;int result userService.delById(id);return result;}
}数据库不支持事务 如果你的数据库不支持事务例如使用了某些特殊的数据库引擎事务可能无法正常工作。在这种情况下应该确保使用支持事务的数据库引擎。
三、事务的隔离级别
3.1 事务的特性回顾
在数据库中事务具有以下四个重要的特性通常被称为 ACID 特性 原子性Atomicity 事务被视为一个不可分割的操作单元要么全部执行成功要么全部失败回滚。 一致性Consistency 事务使数据库从一个一致的状态转变到另一个一致的状态保证数据的完整性和一致性。 隔离性Isolation 并发执行的事务之间应该互不影响每个事务都感觉自己在独立地操作数据。 持久性Durability 一旦事务提交其对数据库的修改就应该是永久性的即使发生系统崩溃也不应该丢失。
3.2 MySQL 的事务隔离级别
MySQL 支持以下四个事务隔离级别用于控制多个事务之间的相互影响程度 读未提交Read Uncommitted 允许一个事务读取另一个事务尚未提交的数据。这是最低的隔离级别可能会导致脏读、不可重复读和幻读的问题。 读已提交Read Committed 允许一个事务只能读取另一个事务已经提交的数据。这可以避免脏读但可能会出现不可重复读和幻读的问题。 可重复读Repeatable Read 保证在同一个事务中多次读取同样记录的结果是一致的即使其他事务对该记录进行了修改。这可以避免脏读和不可重复读但可能出现幻读。 串行化Serializable 最高的隔离级别确保每个事务都完全独立运行避免了脏读、不可重复读和幻读问题但可能影响并发性能。
以下是事务四个隔离级别对应的脏读、不可重复读、幻读情况
隔离级别脏读不可重复读幻读读未提交√√√读已提交×√√可重复读××√串行化×××
√ 表示可能出现该问题。× 表示该问题不会出现。
3.3 Spring 事务的隔离级别
Spring 通过 Transactional 注解中的 isolation 参数来支持不同的事务隔离级别。Isolation的源码如下 可以使用这些枚举值来设置隔离级别
Isolation.DEFAULT使用数据库的默认隔离级别。Isolation.READ_UNCOMMITTED读未提交。Isolation.READ_COMMITTED读已提交。Isolation.REPEATABLE_READ可重复读。Isolation.SERIALIZABLE串行化。
例如指定 Spring 事务的隔离级别为 DEFAULT
RequestMapping(/del)
Transactional(isolation Isolation.DEFAULT)
public int delById(Integer id) {if (id null || id 0) return 0;int result userService.delById(id);return result;
}通过选择合适的事务隔离级别可以在并发环境中控制事务之间的相互影响程度从而避免数据不一致的问题。不同的隔离级别在性能和数据一致性方面有不同的权衡开发人员需要根据具体的业务需求来选择合适的隔离级别。
四、Spring 事务的传播机制
4.1 为什么需要事务传播机制
在复杂的应用场景中一个事务操作可能会调用多个方法或服务。这些方法可能需要独立地进行事务管理但又需要协同工作以保持数据的一致性和完整性。这时就需要引入事务传播机制。
事务传播机制定义了多个事务方法之间如何协同工作如何共享同一个事务以及在嵌套事务中如何进行隔离和提交。通过事务传播机制可以确保多个事务方法在执行时能够按照一定的规则进行协调避免数据不一致的问题。
4.2 事务传播机制的分类
Spring 定义了七种事务传播行为用于控制多个事务方法之间的交互。这些传播行为可以在 Transactional 注解中的 propagation 参数中进行设置。以下是这些传播行为 REQUIRED默认 如果当前存在事务就加入到当前事务中如果没有事务就创建一个新的事务。这是最常用的传播行为。 SUPPORTS 如果当前存在事务就加入到当前事务中如果没有事务就以非事务方式执行。 MANDATORY 如果当前存在事务就加入到当前事务中如果没有事务就抛出异常。 REQUIRES_NEW 无论当前是否存在事务都创建一个新的事务。如果当前存在事务则将当前事务挂起。 NOT_SUPPORTED 以非事务方式执行如果当前存在事务就将当前事务挂起。 NEVER 以非事务方式执行如果当前存在事务就抛出异常。 NESTED 如果当前存在事务就在一个嵌套的事务中执行如果没有事务就与 REQUIRED 一样。
以上 7 种传播行为可以根据是否支持当前事务分为以下 3 类
4.3 Spring 事务传播机制使用案例
REQUIRED 和 NESTED 传播机制的事务演示
控制层 Controller 的 UserController
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;RequestMapping(/add) // /add?usernamelisipassword123456Transactional(propagation Propagation.NESTED)// Transactional(propagation Propagation.REQUIRED)//Transactional(propagation Propagation.REQUIRES_NEW)public int add(RequestParam(username) String username, RequestParam(password) String password) {if (null username || null password || .equals(username) || .equals(password)) {return 0;}int result 0;// 用户添加操作UserInfo user new UserInfo();user.setUsername(username);user.setPassword(password);result userService.add(user);try {int num 10 / 0; // 加入事务外部事务回滚内部事务也会回滚} catch (Exception e) {e.printStackTrace();TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return result;}
}
服务层Service的UserService
Service
public class UserService {Autowiredprivate UserMapper userMapper;Autowiredprivate LogService logService;public int delById(Integer id){return userMapper.delById(id);}Transactional(propagation Propagation.NESTED)// Transactional(propagation Propagation.REQUIRED)//Transactional(propagation Propagation.REQUIRES_NEW)public int add(UserInfo user){// 添加用户信息int addUserResult userMapper.add(user);System.out.println(添加用户结果 addUserResult);//添加日志信息Log log new Log();log.setMessage(添加用户信息);logService.add(log);return addUserResult;}
}
服务层Service的LogService
Service
public class LogService {Autowiredprivate LogMapper logMapper;Transactional(propagation Propagation.NESTED)// Transactional(propagation Propagation.REQUIRED)//Transactional(propagation Propagation.REQUIRES_NEW)public int add(Log log){int result logMapper.add(log);System.out.println(添加日志结果 result);// 模拟异常情况try {int num 10 / 0;} catch (Exception e) {// 加入事务内部事务回滚外部事务也会回滚并且会抛异常// 嵌套事务内部事务回滚不影响外部事务e.printStackTrace();TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return result;}
}
在事务传播机制中REQUIRED 和 NESTED 是两种不同的传播行为它们在事务的嵌套、回滚以及对外部事务的影响等方面有所不同。通过上面代码的演示可以得出 REQUIRED 和 NESTED 之间的主要区别如下 嵌套性质 REQUIRED内部方法与外部方法共享同一个事务内部方法的事务操作是外部方法事务的一部分。NESTED内部方法创建一个嵌套事务它是外部事务的子事务具有独立的事务状态内部事务的回滚不会影响外部事务。 回滚行为 REQUIRED如果内部方法抛出异常或设置回滚会导致整个外部事务回滚包括内部方法和外部方法的操作。NESTED如果内部方法抛出异常或设置回滚只会回滚内部事务而外部事务仍然可以继续执行。 影响外部事务 REQUIRED内部方法的事务操作会影响外部事务的状态内部方法回滚会导致外部事务回滚。NESTED内部方法的事务操作不会影响外部事务的状态内部方法回滚不会影响外部事务的提交或回滚。 支持性 REQUIRED较为常用适用于将多个方法的操作作为一个整体进行事务管理的情况。NESTED在某些数据库中不支持需要数据库支持保存点Savepoint的功能。
总的来说REQUIRED 适用于需要将多个方法的操作作为一个整体事务管理的情况而 NESTED 适用于需要在内部方法中创建嵌套事务的情况保持内部事务的独立性不影响外部事务。选择使用哪种传播行为取决于业务需求和数据库的支持情况。