【Spring】事务管理

👉 博客主页:准Java全栈开发工程师
👉 00年出生,即将进入职场闯荡,目标赚钱,可能会有人觉得我格局小、觉得俗,但不得不承认这个世界已经不再是以一条线来分割的平面,而是围绕财富旋转的球面,成为有钱人不是为了去掌控球体的转向,而是当有人恶意掌控时,努力保护好家人和自己。

一、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、思路分析

  1. 数据层提供指定账户减钱(outMoney),指定账户加钱(inMoney)操作。
  2. 业务层提供转账操作(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 事务管理的三步骤。

  1. 在方法或类上添加 @Transactional 。
  2. 在相关配置类中配置事务管理器。注解方式如下:
    // 使用注解方式的配置事务管理器
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource);
        return tm;
    }
  1. 在 Spring 配置类中开启注解式事务驱动 @EnableTransactionManagement 。

👉 以上就是文章的全部内容啦,诸佬如果有任何建议都可以提哦。
👉 创作不易,如果觉得对您有帮助的话,欢迎关注✨点赞👍收藏📂哦。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

准Java全栈开发工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值