👉 博客主页:准Java全栈开发工程师
👉 00年出生,即将进入职场闯荡,目标赚钱,可能会有人觉得我格局小、觉得俗,但不得不承认这个世界已经不再是以一条线来分割的平面,而是围绕财富旋转的球面,成为有钱人不是为了去掌控球体的转向,而是当有人恶意掌控时,努力保护好家人和自己。
Spring 事务管理
一、Spring事务简介
该部分内容是对和 Spring 事务管理相关的概念介绍,可能会有一点晦涩难懂,但是没关系,可以结合银行转账案例理解学习。
1、为什么要使用 Spring 事务管理?
- 使用 Spring 的事务管理机制,是为了保障数据层或业务层中一系列的数据库操作同成功或同失败。
- 简单举一个例子,我们在银行进行转账操作时,会涉及到转入账户和转出账户,转出账户会转出一定金额的钱到转入账户中,该操作会涉及到两个账户中余额的变化。如果中间出现了异常,会导致转账操作没能成功完成,如果不对其进行事务管理,会导致只有一方账户的金额发生了变化,这是我们不希望看到的。
- 当发生异常时,需要借助 Spring 的事务管理机制来实现相关操作同成功或同失败。
2、Spring 事务角色
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法。
- 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法。
3、Spring 事务配置
- 上述属性均可以在 @Transactional 注解的参数上设置。
- readOnly:true 表示只读事务,false 读写事务,如果该事务存在增删改操作设置为 false ;如果只进行查询操作则设置为 true 。
- timeout:设置超时时间,单位为秒,在指定时间之内事务没有提交成功就自动回滚,-1 表示不设置超时时间。
- rollbackFor:当出现指定异常时进行事务回滚。
- Spring的事务只会对 Error 异常和 RuntimeException 运行时异常及其子类进行事务回滚,,除此之外的其他的异常类型是不会回滚的,这也就意味着,如果在方法执行时出现了上述两类异常外的其他异常(如 IOException ),异常前的语句成功执行,异常后的语句没有执行,这就导致了一个事务中的操作没有做到“同生共死”,是不正确的,所以,在必要时,我们需要通过 rollbackFor 来执行一些异常信息。
- noRollbackFor:当出现指定异常不进行事务回滚。
- rollbackForClassName 等同于 rollbackFor,只不过属性为异常的全限定类名。
- noRollbackForClassName 等同于noRollbackFor,只不过属性为异常的全限定类名。
- isolation:设置事务的隔离级别。
- DEFAULT:默认的隔离级别,采用数据库的隔离级别。
- READ_UNCOMMITTED:读未提交。
- READ_COMMITTED:读已提交。
- REPEATABLE_READ:重复读取。
- SERIALIZABLE:串行化。
4、Spring 事务的传播行为
- 默认的传播行为为 REQUIRED ,作为事务协调员,如果事务管理员开启了事务,则加入事务管理员事务,两者共用同一个事务。如果没有,则自己新建一个事务。
- REQUIRES_NEW :不管事务管理员是否开启了事务,事务协调员都会新建一个事务供自己使用,并不和事务管理员共用一个事务。
- SUPPORTS :如果事务管理员开启了事务,则事务协调员加入该事务。如果事务管理员没有开启事务,事务协调员也不会新建事务。
- NOT_SUPPORTED :无论事务管理员是否开启事务,事务协调员既不会加入事务,也不会自己新建事务,是不支持事务的。
- MANDATORY :要求事务管理员必须开启事务,如果有就加入;如果没有,会报错。
- NEVER :要求事务管理员不能开启事务,如果事务员开启了事务,就会报错。
二、银行转账案例
1、需求分析
- 实现任意两个账户间的转账操作。A 账户转出一定金额的金钱到 B 账户。
- A 账户减钱,B 账户加钱。
- 无论是否转账操作是否成功,均进行日志记录工作。
2、思路分析
- 数据层提供指定账户减钱(outMoney),指定账户加钱(inMoney)操作。
- 业务层提供转账操作(transfer),在该方法内调用减钱和加钱的操作。
3、案例实现
1)在数据库中创建相应的表
- 账户(account)表结构
- 日志(log)表结构
2)导入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
</dependency>
</dependencies>
3)创建与表对应的实体类
public class Account {
private Integer id;
private String name;
private Double money;
// 省略构造器和相关 get、set方法,toString方法等
}
public class Log {
private Integer id;
private String info;
private Date createTime;
// 省略构造器和相关 get、set方法,toString方法等
}
4)编写操作 Account 账户的数据层和业务层
@Repository
public interface AccountMapper {
@Update("update account set money = money + #{money} where name = #{name}")
public void inMoney(@Param("name") String name,@Param("money") Double money);
@Update("update account set money = money - #{money} where name = #{name}")
public void outMoney(@Param("name") String name,@Param("money") Double money);
}
public interface AccountService {
@Transactional
public void transfer(String out,String in,Double money);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private LogService logService;
public void transfer(String out, String in, Double money) {
try {
accountMapper.outMoney(out,money);
accountMapper.inMoney(in,money);
}finally {
logService.log(out, in, money);
}
}
}
- @Transactional 注解用于标注该方法为事务方法,在执行时方法里的每一条语句会有种“同生共死”的感觉,要么全部成功执行,要么都不执行。
- @Transactional 注解可以声明在接口,也可以声明在接口的实现类。该案例中博主是将其声明在了接口的方法上。
- @Transactional 注解既可以声明在方法上,也可以声明在类上。声明在方法上表明为该方法开启事务管理;声明在类上表明为该类中所有存在的方法都开启事务管理。
- 因为案例需求是无论转账操作是否成功执行,都需要记录日志信息,所以使用了我们在捕捉异常时常使用的 try … finally 结构,catch 部分可以省略,将日志记录工作放到了 finally 中,因为放在 finally 中的结构无论如何都是会执行的,刚好满足我们对日志信息的记录需求。
- 借助转账 transfer 方法来分析一下事务的传播行为。
- transfer 方法在转账操作中充当的角色就是 事务管理员 的角色,而 outMoney 操作和 inMoney 操作充当的是 事务协调员 的角色,因为这两个方法是需要同时执行成功或同时执行失败的,所以可以将这两个方法加入到 transfer 中,和 transfer 共用同一个事务。
- 而 log 记录日志方法是无论执行失败或成功,都是需要执行的,所以 log 不能和他们公用一个事务。
5)编写 log 日志相关的数据层和业务层
@Repository
public interface LogMapper {
@Insert("insert into log(info,createTime) values(#{info},now())")
public void log(String info);
}
public interface LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
public void log(String out, String in, Double money) {
String info = "转账操作:" + out + "账户向" + in + "账户转账" + money + "元";
logMapper.log(info);
}
}
- 在@Transactional 中使用 propagation 指定事务的传播行为,REQUIRES_NEW 表示事务协调员自己会新建一个事务,不和事务管理员共用同一个事务。
- 因为案例需求是日志记录操作必须执行,如果和 transfer 方法共用同一个事务,在转账操作失败时,日志记录也无法执行,是不满足需求的,所以为该方法新建一个自己独有的事务。
6)编写相应的配置类
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driver);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
return druidDataSource;
}
// 使用注解方式的配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
}
- 光在方法上添加 @Transactional 注解还不够,还需要配置事务管理器,Spring 提供有默认的事务管理器 PlatformTransactionManager ,需要使用注解的方式配置。
- 事务管理器需要根据所使用的技术进行选择,Mybatis 框架使用的是 JDBC 事务,所以使用DataSourceTransactionManager 对象。
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("pojo");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("mapper");;
return msc;
}
}
@Configuration
@ComponentScan({"mapper","service"})
@PropertySource({"jdbc.properties"})
@Import({JdbcConfig.class,MyBatisConfig.class})
@EnableTransactionManagement // 开启注解式事务驱动
public class SpringConfig {
}
7)数据库表中初始数据
- account 表初始数据
- log 表初始数据为空
8)测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void test(){
accountService.transfer("qdxorigin","wkb",100D);
}
}
9)测试结果
- account 表中数据
- log 表中数据
三、总结
通过练习银行转账案例,可以提炼在 Spring 事务管理的三步骤。
- 在方法或类上添加 @Transactional 。
- 在相关配置类中配置事务管理器。注解方式如下:
// 使用注解方式的配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
- 在 Spring 配置类中开启注解式事务驱动 @EnableTransactionManagement 。
👉 以上就是文章的全部内容啦,诸佬如果有任何建议都可以提哦。
👉 创作不易,如果觉得对您有帮助的话,欢迎关注✨点赞👍收藏📂哦。