禁止wordpress网站上传图片时自动生成三张图片方法,google广告投放技巧,搜狗推广登录app,迁安做网站中的cms润强Spring 声明式事务 1.Spring 事务管理概述1.1 事务管理的重要性1.2 Spring事务管理的两种方式1.2.1 编程式事务管理1.2.2 声明式事务管理 1.3 为什么选择声明式事务管理 2. 声明式事务管理2.1 基本用法2.2 常用属性2.2.1 propagation#xff08;传播行为#xff09;2.2.2 iso… Spring 声明式事务 1.Spring 事务管理概述1.1 事务管理的重要性1.2 Spring事务管理的两种方式1.2.1 编程式事务管理1.2.2 声明式事务管理 1.3 为什么选择声明式事务管理 2. 声明式事务管理2.1 基本用法2.2 常用属性2.2.1 propagation传播行为2.2.2 isolation隔离级别2.2.3 readOnly只读事务2.2.4 timeout超时时间2.2.5 rollbackFor回滚异常 3. Spring事务失效4. 案例4.1 前期准备4.1.1 依赖引入4.1.2 数据库建表语句4.1.3 实体类以及相关代码 4.2 转账案例4.2.1 测试成功4.2.2 测试回滚4.2.3 测试受检异常4.2.4 测试 rollBackFor 属性4.2.5 测试隔离级别 Spring 提供了两个事务管理方式一种是编程式(很少用)一种是声明式事务。声明式事务管理将事务管理的代码从业务逻辑中分离出来使得代码更清晰、可维护。使得开发者可以通过配置而不是编写大量的代码来管理事务。
使用这里我们只介绍声明式事务
1.Spring 事务管理概述
1.1 事务管理的重要性
在应用程序中事务管理是确保数据操作的一致性、隔离性、持久性和原子性的关键机制。当多个数据库操作必须作为一个不可分割的单元执行时事务管理变得至关重要。对于复杂的业务逻辑事务能够确保在并发和异常情况下数据库始终保持一致性。
1.2 Spring事务管理的两种方式
Spring框架提供了两种主要的事务管理方式分别是编程式事务管理和声明式事务管理。
1.2.1 编程式事务管理
编程式事务管理要求开发者通过编写代码来管理事务的开始、提交和回滚。虽然具有灵活性但容易导致代码冗余和可读性差。
try {// 开始事务transactionManager.beginTransaction();// 执行业务逻辑// 提交事务transactionManager.commit();
} catch (Exception e) {// 发生异常回滚事务transactionManager.rollback();throw e;
}1.2.2 声明式事务管理
相比之下声明式事务管理通过配置文件或注解的方式实现事务控制将事务逻辑从业务代码中分离出来。这种方式更加简洁、可维护并提供更好的可读性。
Transactional
public void performBusinessLogic() {// 业务逻辑
}1.3 为什么选择声明式事务管理
选择声明式事务管理有以下优势
简洁性 通过注解或XML配置开发者无需编写冗长的事务管理代码使代码更加简洁清晰。可维护性 事务逻辑与业务逻辑分离易于维护和理解。可读性 使用注解或XML配置事务逻辑与业务逻辑在代码中更易于辨认提高代码的可读性。一致性 通过统一的配置方式整个应用程序可以保持一致的事务管理策略减少错误和不一致性。集成性 声明式事务更好地与Spring的其他特性如AOP集成提供更全面的解决方案。
综合而言声明式事务管理是Spring中推荐的事务管理方式它能够提高代码的可维护性、可读性并与其他Spring特性协同工作使得开发者能够更专注于业务逻辑的实现而不是事务的管理。
2. 声明式事务管理
声明式事务管理建立在AOP之上其本质是对方法前后进行拦截然后在目标方法开始之前创建或者加入一个事务执行完目标方法之后根据执行的情况提交或者回滚。
Spring 根据类或者方法上是否有transactional注解来判断是否开启事务。
2.1 基本用法
在类上使用
Transactional
public class TestService {// 类中所有方法都将使用默认的事务配置
}在方法上使用
public class TestService {Transactionalpublic void method1() {// 这个方法将使用默认的事务配置}Transactional(propagation Propagation.REQUIRED, isolation Isolation.READ_COMMITTED)public void method2() {// 这个方法将使用指定的事务配置}
}2.2 常用属性
2.2.1 propagation传播行为
Transactional 注解的 propagation 属性用于定义事务的传播行为它指定在方法被调用时当前方法的事务如何与现有的事务进行交互。
Transactional(propagation Propagation.*)
public void method1() {// ...
}定义事务的传播行为包括
REQUIRED默认如果当前存在事务则加入该事务如果不存在事务则新建一个事务。REQUIRES_NEW无论当前是否存在事务都会创建一个新的事务如果存在事务则将其挂起。SUPPORTS如果当前存在事务则加入该事务如果不存在事务则以非事务的方式执行。MANDATORY该传播行为要求当前方法必须在一个事务中执行否则将抛出异常。NOT_SUPPORTED以非事务的方式执行如果当前存在事务则将其挂起。NEVER以非事务的方式执行如果当前存在事务则抛出异常。NESTED如果当前存在事务则创建一个嵌套事务并在嵌套事务内执行。嵌套事务是外部事务的一部分但有独立的提交和回滚。
2.2.2 isolation隔离级别
Transactional 注解中的 isolation 属性用于指定事务的隔离级别。隔离级别定义了多个事务并发执行时彼此之间的可见性和影响的程度。
Transactional(isolation Isolation.*)
public void method1() {// ...
}Spring 支持以下五个隔离级别 DEFAULT默认使用底层数据库的默认隔离级别。通常为数据库的默认配置比如 MySQL 默认的是 REPEATABLE_READ而 Oracle 默认的是 READ_COMMITTED。 READ_UNCOMMITTED读未提交 允许一个事务读取另一个事务未提交的数据。这是最低的隔离级别可能导致脏读、不可重复读和幻读的问题。 READ_COMMITTED读已提交保证一个事务提交后才能被其他事务读取。这是大多数数据库的默认隔离级别可以避免脏读但仍可能存在不可重复读和幻读的问题。 REPEATABLE_READ可重复读 对相同字段的多次读取结果是一致的除非自己进行了数据更新。避免了不可重复读的问题但仍可能存在幻读的问题。 SERIALIZABLE串行化 最高的隔离级别确保每个事务都完全看不到其他事务的操作包括读取和写入。可以避免脏读、不可重复读和幻读的问题但也降低了并发性能。
2.2.3 readOnly只读事务
标识事务是否为只读可以提高事务的性能。
Transactional(readOnly true)
public void method1() {// ...
}2.2.4 timeout超时时间
指定事务的超时时间单位为秒。
Transactional(timeout 60)
public void method1() {// ...
}2.2.5 rollbackFor回滚异常
指定哪些异常触发事务回滚。Transactional 注解默认只对运行时异常进行事务回滚对检查时异常不回滚。
Transactional(rollbackFor {SQLException.class, MyCustomException.class})
public void method1() {// ...
}3. Spring事务失效
哪些情况下会导致Spring事务失效对应的原因是什么 1.方法内的自调用Spring事务是基于AOP的只要使用代理对象调用某个方法时Spring事务才能生效而在一个方法中调用使用this.xxxO调用方法时this并不是代理对象所以会导致事务失效。 解决办法1将需要在同一事务中执行的方法抽取到一个独立的Bean中通过依赖注入的方式调用该Bean。确保方法调用经过代理对象从而激活事务。 解决办法2在类内部通过依赖注入的方式将当前类注入到自己中然后通过注入的对象调用方法。这样确保调用经过代理对象从而使事务生效。 解决办法3使用AopContext.currentProxy()获取当前代理对象通过这个代理对象调用方法。结合EnableAspectJAutoProxy(exposeProxytrue)注解开启对当前代理对象的暴露确保事务能够正确地被激活。 2.方法是private的Spring事务会基于CGLIB来进行AOP而CGLIB会基于父子类来失效子类是代理类父类是被代理类如果父类中的某个方法是private的那么子类就没有办法重写它也就没有办法额外增加Spring事务的逻辑。 3.方法是final的原因和private是了样的也是由于子类不能重写父类中的final的方法 4.单独的线程调用方法当Mybatis或JdbcTemplate执行SQL时会从ThreadLocal中去获取数据库连接对象如果开启事务的线程和执行SQL的线程是同一个那么就能拿到数据库连接对象如果不是同一个线程那就拿到不到数据库连接对象这样Mybatis或JdbcTemplate就会自己去新建一个数据库连接用来执行SQL此数据库连接的autocommit为true那么执行完SQL就会提交后续再抛异常也就不能再回滚之前已经提交了的SQL了。 5.没加Configuration注解如果用SpringBoot基本没有这个问题但是如果用的Spring那么可能会有这个问题这个问题的原因其实也是由于Mybatis或JdbcTemplate会从ThreadLocal中去获取数据库连接但是ThreadLocal中存储的是一个MAPMAP的key为DataSource对象value为连接对象而如果我们没有在AppConfig上添加Configuration注解的话会导致MAP中存的DataSource对象和Mybatis和JdbcTemplate中的DataSource对象不相等从而也拿不到数据库连接导致自己去创建数据库连接了。 6.异常被吃掉如果Spring事务没有捕获到异常那么也就不会回滚了默认情况下Spring会捕获RuntimeException和Error。 7.类没有被Spring管理 8.数据库不支持事务
4. 案例
4.1 前期准备
4.1.1 依赖引入 dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.16/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency!-- 使用Plus 简化开发 --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.5.1/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion8.0.17/version/dependency/dependencies4.1.2 数据库建表语句
CREATE TABLE users (id int NOT NULL AUTO_INCREMENT,user_name varchar(18) NOT NULL,balance decimal(10, 2) NULL DEFAULT NULL,PRIMARY KEY (id) USING BTREE
)4.1.3 实体类以及相关代码
User
Data
public class User {private int id;private String userName;private Double balance;
}UserMapper
Mapper
public interface UserMapper extends BaseMapperUser {
}UserService
public interface UserService extends IServiceUser {
}UserServiceImpl
Service
public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService {
}application.yml
Spring:datasource:url: jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf-8driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 120125hzy.type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus: # MyBatis Plus配置configuration: map-underscore-to-camel-case: true # 驼峰下划线转换
logging: # 控制台打印 SQLlevel:com:example:mapper: debug4.2 转账案例
在Spring中你可以使用Transactional注解来实现声明式事务。这个注解可以应用于类级别或方法级别具体取决于你想要控制事务的粒度。
接口编写 /*** 测试转账*/String Transfer(Integer fromId, Integer toId, Double money);生成单元测试每次测试前两人余额都调整为 10000.00
SpringBootTest
class UserServiceImplTest {Autowiredprivate UserService userService;Testvoid transfer() {String ans userService.Transfer(1,2,600.0);assert ans.equals(转账成功);}
}4.2.1 测试成功 OverrideTransactionalpublic String Transfer(Integer fromId, Integer toId, Double money) {try {// 查询转出账户User fromUser getById(fromId);if (fromUser null) {throw new RuntimeException(转出账户不存在);}// 查询转入账户User toUser getById(toId);if (toUser null) {throw new RuntimeException(转入账户不存在);}// 检查余额是否足够if (fromUser.getBalance() money) {throw new RuntimeException(余额不足);}// 更新转出账户余额fromUser.setBalance(fromUser.getBalance() - money);updateById(fromUser);// 更新转入账户余额toUser.setBalance(toUser.getBalance() money);updateById(toUser);log.info(转账成功);return 转账成功;} catch (Exception e) {throw new RuntimeException(转账失败: e.getMessage());}}测试通过 数据库也成功修改 4.2.2 测试回滚 OverrideTransactionalpublic String Transfer(Integer fromId, Integer toId, Double money) {try {// 查询转出账户User fromUser getById(fromId);if (fromUser null) {throw new RuntimeException(转出账户不存在);}// 查询转入账户User toUser getById(toId);if (toUser null) {throw new RuntimeException(转入账户不存在);}// 检查余额是否足够if (fromUser.getBalance() money) {throw new RuntimeException(余额不足);}// 更新转出账户余额fromUser.setBalance(fromUser.getBalance() - money);updateById(fromUser);// 手动抛出异常测试事务回滚if (1 1) throw new RuntimeException(转账异常事务回滚);// 更新转入账户余额toUser.setBalance(toUser.getBalance() money);updateById(toUser);log.info(转账成功);return 转账成功;} catch (Exception e) {throw new RuntimeException(转账失败: e.getMessage());}}测试未通过 数据库也没有改变 4.2.3 测试受检异常 OverrideTransactionalpublic String Transfer(Integer fromId, Integer toId, Double money) throws SQLException {try {// 查询转出账户User fromUser getById(fromId);if (fromUser null) {throw new RuntimeException(转出账户不存在);}// 查询转入账户User toUser getById(toId);if (toUser null) {throw new RuntimeException(转入账户不存在);}// 检查余额是否足够if (fromUser.getBalance() money) {throw new RuntimeException(余额不足);}// 更新转出账户余额fromUser.setBalance(fromUser.getBalance() - money);updateById(fromUser);// 手动抛出异常测试事务回滚if (1 1) throw new RuntimeException(转账异常事务回滚);// 更新转入账户余额toUser.setBalance(toUser.getBalance() money);updateById(toUser);log.info(转账成功);return 转账成功;} catch (Exception e) {throw new SQLException(转账失败: e.getMessage());}}测试未通过 事务未回滚 4.2.4 测试 rollBackFor 属性
Transactional(rollbackFor SQLException.class)这次成功回滚了。 4.2.5 测试隔离级别
添加一个接口 /*** 测试付款*/String payment(Integer id,Double money);OverrideTransactional()public String payment(Integer id, Double money) {try {User user getById(id);if (user null) {throw new RuntimeException(支付账户不存在);}// 更新转出账户余额user.setBalance(user.getBalance()-money);updateById(user);Thread.sleep(5000);if (1 1) throw new RuntimeException(转账异常事务回滚);log.info(支付成功);}catch (RuntimeException e){throw new RuntimeException(支付失败);}catch (Exception e){}return 支付成功;}修改 Transfer()
OverrideTransactional(isolation Isolation.READ_UNCOMMITTED)public String Transfer(Integer fromId, Integer toId, Double money){try {// 查询转出账户User fromUser getById(fromId);if (fromUser null) {throw new RuntimeException(转出账户不存在);}log.info(id: {}的余额:{},fromUser.getId(),fromUser.getBalance());// 查询转入账户User toUser getById(toId);if (toUser null) {throw new RuntimeException(转入账户不存在);}// 检查余额是否足够if (fromUser.getBalance() money) {throw new RuntimeException(余额不足);}// 更新转出账户余额fromUser.setBalance(fromUser.getBalance() - money);updateById(fromUser);// 更新转入账户余额toUser.setBalance(toUser.getBalance() money);updateById(toUser);log.info(转账成功);return 转账成功;} catch (Exception e) {throw new RuntimeException(转账失败: e.getMessage());}}测试函数 Testvoid test() throws InterruptedException {Thread t1 new Thread(()-{userService.payment(1,500.0);});t1.start();Thread.sleep(2000);userService.Transfer(1,2,600.0);}转账接口读取到了支付接口修改的数据最终转账成功。而支付接口支付失败但是张三还是少了1100