• Spring framework day 03:Spring 整合 事务管理


    前言

    在现代的软件开发领域,事务管理是一项至关重要的技术。无论是在企业应用程序还是在网站开发中,保证数据的一致性和完整性都是至关重要的。Spring Framework 提供了丰富的功能来简化事务管理,并与其它模块无缝集成,使得开发人员可以轻松地处理事务操作。

    在本篇博客中,我们将深入探讨 Spring 框架中的事务管理,并介绍如何整合事务管理到我们的应用程序中。我们将学习什么是事务管理,为什么使用事务管理,以及如何配置和使用 Spring 的事务管理器来实现数据的一致性。

    无论你是一个有经验的 Spring 开发人员,还是刚开始接触 Spring 的初学者,本篇博客都会为你提供清晰而全面的指导。让我们一同进入 Spring 整合事务管理的世界,探索其强大的功能和灵活性。

    一、前期准备

    本次通过一个简单的银行转账的案例来介绍事务管理。

    1、准备一张表 cardInfos
    1. create table card_infos(
    2. card_id int auto_increment primary key ,
    3. card_num varchar(20), -- 卡号
    4. balance decimal -- 余额
    5. );
    6. insert into card_infos(card_num, balance) VALUE ('456736478372714678',1000),('564738264736475823',1000);
    2、要求

     3、新建项目,结构如下

    4、导入 spring 依赖
    1. <dependencies>
    2. <dependency>
    3. <groupId>com.alibabagroupId>
    4. <artifactId>druidartifactId>
    5. <version>1.2.15version>
    6. dependency>
    7. <dependency>
    8. <groupId>org.springframeworkgroupId>
    9. <artifactId>spring-contextartifactId>
    10. <version>5.3.23version>
    11. dependency>
    12. <dependency>
    13. <groupId>org.springframeworkgroupId>
    14. <artifactId>spring-jdbcartifactId>
    15. <version>5.3.23version>
    16. dependency>
    17. <dependency>
    18. <groupId>org.springframeworkgroupId>
    19. <artifactId>spring-txartifactId>
    20. <version>5.3.23version>
    21. dependency>
    22. <dependency>
    23. <groupId>ch.qos.logbackgroupId>
    24. <artifactId>logback-classicartifactId>
    25. <version>1.4.5version>
    26. dependency>
    27. <dependency>
    28. <groupId>mysqlgroupId>
    29. <artifactId>mysql-connector-javaartifactId>
    30. <version>8.0.33version>
    31. dependency>
    32. <dependency>
    33. <groupId>org.mybatisgroupId>
    34. <artifactId>mybatisartifactId>
    35. <version>3.5.6version>
    36. dependency>
    37. <dependency>
    38. <groupId>org.mybatisgroupId>
    39. <artifactId>mybatis-springartifactId>
    40. <version>2.0.6version>
    41. dependency>
    42. <dependency>
    43. <groupId>org.projectlombokgroupId>
    44. <artifactId>lombokartifactId>
    45. <version>1.18.30version>
    46. dependency>
    47. dependencies>

     二、开始学习

    1、在 entity 包下新建一个 CardInfos 实体类
    1. @Data
    2. public class CardInfos {
    3. private Integer cardId;
    4. private String cardNum;
    5. private BigDecimal balance;
    6. }

    这个实体类 CardInfos 主要用于表示一个卡片信息的数据模型。在软件开发中,实体类用于封装和表示某个具体对象的属性和行为。

    2、在 dao 包下新建一个 CardDao 接口
    1. public interface CardDao {
    2. /**
    3. * 查询转账卡号是否有足够余额
    4. * @param cardNum
    5. * @return
    6. */
    7. CardInfos getAccountByCardNum(String cardNum);
    8. /**
    9. * 更新账号信息
    10. * @param cardInfos
    11. */
    12. void update(CardInfos cardInfos);
    13. }
     分析:

     根据题目要求:转账前需要插叙自己的余额是否充足。所以我们需要定义一个查询的方法,查询余额。那么当 A、B交易成功后,它们的余额都发生了改变,所以需要定义一个 update 修改的方法,对相应的用户的余额进行修改

     3、在 service 包下新建一个 CardService 接口,在 impl 包下新建一个 CardServiceImpl 实现类

    CardService 接口

    1. public interface CardService {
    2. /**
    3. *
    4. * @param formCardNum 转账人卡号
    5. * @param toCardNum 收帐人卡号
    6. * @param money 金额
    7. */
    8. void transfer(String formCardNum, String toCardNum, BigDecimal money);
    9. /**
    10. * 查询转账卡号是否有足够余额
    11. * @param cardNum
    12. * @return
    13. */
    14. CardInfos getAccountByCardNum(String cardNum);
    15. /**
    16. * 更新账号信息
    17. * @param cardInfos
    18. */
    19. void update(CardInfos cardInfos);
    20. }

    CardServiceImpl 实现类

    1. @Service
    2. @RequiredArgsConstructor
    3. @Slf4j
    4. public class CardServiceImpl implements CardService {
    5. private final CardDao cardDao;
    6. private final DemoService demoService;
    7. @Override
    8. public CardInfos getAccountByCardNum(String cardNum) {
    9. return cardDao.getAccountByCardNum(cardNum);
    10. }
    11. @Override
    12. public void update(CardInfos cardInfos) {
    13. cardDao.update(cardInfos);
    14. }
    15. /**
    16. * @param formCardNum 转账人卡号
    17. * @param toCardNum 收帐人卡号
    18. * @param money 金额
    19. */
    20. @Override
    21. public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
    22. // 获取转账人的信息
    23. CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
    24. // 获取接收人的信息
    25. CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
    26. // 转账人的金额
    27. BigDecimal formbalance = formAccount.getBalance();
    28. // 接收人的金额
    29. BigDecimal tobalance = toAccount.getBalance();
    30. // 转账人如果有足够的余额才进行转账
    31. if (formbalance.doubleValue() >= money.doubleValue()) {
    32. // 设置转账人的余额(扣除转账金额)
    33. formAccount.setBalance(formbalance.subtract(money));
    34. formAccount.setCardNum(formCardNum);
    35. // 设置接收人的余额(添加转账的金额)
    36. toAccount.setBalance(tobalance.add(money));
    37. toAccount.setCardNum(toCardNum);
    38. // 更新转账人和接收人的金额
    39. cardDao.update(formAccount);
    40. cardDao.update(toAccount);
    41. log.info("转账完成!");
    42. } else {
    43. log.info("卡号:" + formCardNum + ",余额不足 ");
    44. throw new RuntimeException("余额不足");
    45. }
    46. }
    47. }

    作用:处理转账的业务逻辑 

    4、在 resources 包下新建 db.properties ,在 mappers 包下新建 CardMapper.xml

    db.properties

    1. driver = com.mysql.cj.jdbc.Driver
    2. url = jdbc:mysql://localhost:3306/psm
    3. username= root
    4. password = 123456
    5. maxActive = 200
    6. initialSize = 5
    7. minIdle = 5
    8. maxWait = 2000
    9. minEvictableIdleTimeMillis = 300000
    10. timeBetweenEvictionRunsMillis = 60000
    11. testWhileIdle = true
    12. testOnReturn = false
    13. validationQuery = select 1
    14. poolPreparedStatements = false

    作用:这段配置是一个典型的数据库连接池的配置,用于连接和管理与 MySQL 数据库的连接。 以上配置项提供了对数据库连接池的一些基本配置和管理,包括连接数量、连接等待时间、连接回收策略等。通过连接池来管理数据库连接可以提升系统性能,并且避免频繁地创建和销毁连接,从而减少资源开销和提高响应速度。

    CardMapper.xml

    1. "1.0" encoding="utf-8"?>
    2. mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    4. <mapper namespace="edu.nf.ch03.dao.CardDao">
    5. <resultMap id="cardMap" type="edu.nf.ch03.entity.CardInfos">
    6. <id property="cardId" column="card_id"/>
    7. <result property="cardNum" column="card_num"/>
    8. <result property="balance" column="balance"/>
    9. resultMap>
    10. <select id="getAccountByCardNum" parameterType="string" resultMap="cardMap">
    11. select balance from card_infos where card_num = #{cardNum}
    12. select>
    13. <update id="update" parameterType="edu.nf.ch03.entity.CardInfos">
    14. update card_infos set balance = #{balance} where card_num = #{cardNum}
    15. update>
    16. mapper>

    作用:用于定义 DAO 接口 edu.nf.ch03.dao.CardDao 中的 SQL 映射关系和操作语句。

    通过这个配置文件,Mybatis 可以自动生成对应的 SQL 语句,并提供给 edu.nf.ch03.dao.CardDao 接口的实现类使用。在程序中,通过调用相应的方法,就可以执行对应的数据库操作,例如查询账户余额或更新账户余额。

    5、 在 config 包下新建一个 AppConfig 配置类
    1. @Configuration
    2. @MapperScan(basePackages = "edu.nf.ch03.dao")
    3. @ComponentScan(basePackages = "edu.nf.ch03")
    4. public class AppConfig {
    5. @Bean(initMethod = "init",destroyMethod = "close")
    6. public DruidDataSource dataSource() throws Exception {
    7. Properties properties = new Properties();
    8. InputStream stream = AppConfig.class.getClassLoader().getResourceAsStream("db.properties");
    9. properties.load(stream);
    10. return (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
    11. }
    12. @Bean
    13. public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
    14. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    15. sqlSessionFactoryBean.setDataSource(dataSource);
    16. sqlSessionFactoryBean.setTypeAliasesPackage("edu.nf.homework.entity");
    17. PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    18. Resource[] resources = resolver.getResources("classpath:mappers/*.xml");
    19. sqlSessionFactoryBean.setMapperLocations(resources);
    20. //启用mybatis日志功能
    21. org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
    22. conf.setLogImpl(StdOutImpl.class);
    23. //将日志配置设置到SqlSessionFactoryBean中
    24. sqlSessionFactoryBean.setConfiguration(conf);
    25. return sqlSessionFactoryBean;
    26. }
    27. }

    作用:通过 dataSource 方法配置了一个 Druid 数据源,通过 sqlSessionFactoryBean 方法配置了一个 SqlSessionFactoryBean 对象,设置了数据源、实体类别名包扫描路径和映射文件位置。这里还启用了 MyBatis 的日志功能,可以打印 SQL 执行的日志信息。

     6、测试

    我们先测试一下保证转账功能可以正常运行。

    1. public class Main {
    2. public static void main(String[] args) {
    3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    4. CardService bean = context.getBean(CardService.class);
    5. // 转账金额
    6. BigDecimal bigDecimal = new BigDecimal("500");
    7. // 执行转账
    8. bean.transfer("564738264736475823","456736478372714678",bigDecimal);
    9. }
    10. }

    运行结果

    三、为什么要使用事务管理

     事务管理是为了确保数据库操作的一致性、可靠性和完整性而引入的机制。在一个业务操作中可能涉及多个数据库操作,而这些操作要么全部成功,要么全部失败,不能出现部分操作成功、部分操作失败的情况。事务管理可以确保这种一致性。

    以下是几个使用事务管理的原因:

    1. 数据库一致性:如果一个业务操作需要执行多个数据库操作,例如插入、更新、删除等,那么这些操作之间可能存在依赖关系,需要保证在事务提交前所有操作都成功执行,如果其中某个操作失败了,整个事务可以回滚,保持数据的一致性。

    2. 并发控制:在多用户、多线程环境下,多个事务可能同时对数据库进行读写操作,如果没有事务管理,可能会导致数据的不一致或并发冲突。通过使用事务管理,可以有效控制并发访问,保证数据的正确性和完整性。

    3. 异常处理:在业务操作中可能发生各种异常情况,例如数据库连接异常、事务超时、网络异常等。如果没有事务管理,这些异常可能导致数据不一致或资源泄露。通过使用事务管理,可以对异常进行捕获和处理,保证数据的完整性,并进行相应的回滚操作。

    4. 性能优化:事务管理还可以提供一些性能优化的手段,例如批量提交、脏读、隔离级别设置等,可以根据业务需求和数据库性能要求进行相应的配置,提高数据库操作的效率和性能。

    综上所述,事务管理是确保数据库操作的一致性和可靠性的重要机制,通过对多个操作进行事务管理,可以保证数据的正确性,并提供异常处理和性能优化的功能

    四、使用事务管理

    1、不使用事务的情况下带异常转账
     

     在 CardServiceImpl 实现类中,引发了一个异常,如果是正常的转账,如果有异常的话,A和B 的余额都不应该发生改变,只要有异常这个流程就要终止,我们现在来测试一下。

    运行结果:

    看到问题没有!我们的异常已经生效了,但是是不是还扣除了转账人的金额。这就是我们为什么要使用事务的原因,那我们现在就来使用事务来管理这个实现类吧。

    2、使用声明式事务 (方法上)

    在 CardServiceImpl 中的 transfet 方法上使用 @Transactional 注解 

    1. /**
    2. * @param formCardNum 转账人卡号
    3. * @param toCardNum 收帐人卡号
    4. * @param money 金额
    5. * @Transactional 注解使用 spring 提供的声明式事务,可以用在方法上,也可以用在类上
    6. * 当用在类上的时候,表示这个类的所有方法都享有事务功能
    7. */
    8. @Transactional
    9. @Override
    10. public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
    11. // 获取转账人的信息
    12. CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
    13. // 获取接收人的信息
    14. CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
    15. // 转账人的金额
    16. BigDecimal formbalance = formAccount.getBalance();
    17. // 接收人的金额
    18. BigDecimal tobalance = toAccount.getBalance();
    19. // 转账人如果有足够的余额才进行转账
    20. if (formbalance.doubleValue() >= money.doubleValue()) {
    21. // 设置转账人的余额(扣除转账金额)
    22. formAccount.setBalance(formbalance.subtract(money));
    23. formAccount.setCardNum(formCardNum);
    24. // 设置接收人的余额(添加转账的金额)
    25. toAccount.setBalance(tobalance.add(money));
    26. toAccount.setCardNum(toCardNum);
    27. // 更新转账人和接收人的金额
    28. cardDao.update(formAccount);
    29. // 引发异常
    30. System.out.println(10 / 0);
    31. cardDao.update(toAccount);
    32. log.info("转账完成!");
    33. // demoService.add();
    34. // 调用本类的其他方法,不会启用事务
    35. // this.update();
    36. } else {
    37. log.info("卡号:" + formCardNum + ",余额不足 ");
    38. throw new RuntimeException("余额不足");
    39. }
    40. }

     @Transactional 是 Spring 框架提供的一个注解,用于标记方法或类,表示该方法或类需要进行事务管理。

    使用 @Transactional 注解时,Spring 会在方法执行前开启一个事务,并在方法执行完成后根据执行情况决定是提交事务还是回滚事务。在方法中如果发生了异常,则事务会回滚,保证数据的一致性。

    @Transactional 注解可以应用于方法级别和类级别:

    • 方法级别:标注在方法上,表示该方法需要进行事务管理。
    • 类级别:标注在类上,表示该类的所有方法都需要进行事务管理。
     1、更改 AppConfig 配置类
    1. @Configuration
    2. @MapperScan(basePackages = "edu.nf.ch03.dao")
    3. @ComponentScan(basePackages = "edu.nf.ch03")
    4. // 启用事务注解驱动,这样就可以在业务类中使用 @Transaction 注解
    5. @EnableTransactionManagement
    6. public class AppConfig {
    7. @Bean(initMethod = "init",destroyMethod = "close")
    8. public DruidDataSource dataSource() throws Exception {
    9. Properties properties = new Properties();
    10. InputStream stream = AppConfig.class.getClassLoader().getResourceAsStream("db.properties");
    11. properties.load(stream);
    12. return (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
    13. }
    14. @Bean
    15. public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
    16. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    17. sqlSessionFactoryBean.setDataSource(dataSource);
    18. sqlSessionFactoryBean.setTypeAliasesPackage("edu.nf.homework.entity");
    19. PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    20. Resource[] resources = resolver.getResources("classpath:mappers/*.xml");
    21. sqlSessionFactoryBean.setMapperLocations(resources);
    22. //启用mybatis日志功能
    23. org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
    24. conf.setLogImpl(StdOutImpl.class);
    25. //将日志配置设置到SqlSessionFactoryBean中
    26. sqlSessionFactoryBean.setConfiguration(conf);
    27. return sqlSessionFactoryBean;
    28. }
    29. /**
    30. * 装配事务管理器,并注入数据源,
    31. * 这样事务管理器就可以基于 AOP 来管理 Connection 对象的事务操作
    32. * @param dataSource
    33. * @return
    34. */
    35. @Bean
    36. public PlatformTransactionManager txManager(DataSource dataSource){
    37. return new DataSourceTransactionManager(dataSource);
    38. }
    39. }

    @EnableTransactionManagement 是一个 Spring 框架提供的注解,用于启用Spring的事务管理功能。 

    1. 通过添加 @EnableTransactionManagement 注解,告诉 Spring 启用事务管理。Spring 将会根据配置寻找合适的事务管理器。

    2. 确保已经配置了一个与数据源关联的事务管理器。可以使用 @Bean 注解创建一个 PlatformTransactionManager 的实例,并将其与数据源相关联。

    2、测试

     运行结果:

    在引发异常后是不是并没有扣除转账人的余额了,因为使用事务管理,在引发异常时,就会终止这个流程,只要引发异常就不会再执行下去了。一句话总结:

    在一个业务操作中可能涉及多个数据库操作,而这些操作要么全部成功,要么全部失败,不能出现部分操作成功、部分操作失败的情况。

     五、事务传播级别

    1、使用事务(类上)
    1. @Service
    2. @RequiredArgsConstructor
    3. @Slf4j
    4. @Transactional(rollbackFor = RuntimeException.class,
    5. propagation = Propagation.REQUIRED
    6. )
    7. public class CardServiceImpl implements CardService {
    8. private final CardDao cardDao;
    9. private final DemoService demoService;
    10. @Override
    11. public CardInfos getAccountByCardNum(String cardNum) {
    12. return cardDao.getAccountByCardNum(cardNum);
    13. }
    14. @Override
    15. public void update(CardInfos cardInfos) {
    16. cardDao.update(cardInfos);
    17. }
    18. /**
    19. * @param formCardNum 转账人卡号
    20. * @param toCardNum 收帐人卡号
    21. * @param money 金额
    22. * @Transactional 注解使用 spring 提供的声明式事务,可以用在方法上,也可以用在类上
    23. * 当用在类上的时候,表示这个类的所有方法都享有事务功能
    24. */
    25. // @Transactional
    26. @Override
    27. public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
    28. // 获取转账人的信息
    29. CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
    30. // 获取接收人的信息
    31. CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
    32. // 转账人的金额
    33. BigDecimal formbalance = formAccount.getBalance();
    34. // 接收人的金额
    35. BigDecimal tobalance = toAccount.getBalance();
    36. // 转账人如果有足够的余额才进行转账
    37. if (formbalance.doubleValue() >= money.doubleValue()) {
    38. // 设置转账人的余额(扣除转账金额)
    39. formAccount.setBalance(formbalance.subtract(money));
    40. formAccount.setCardNum(formCardNum);
    41. // 设置接收人的余额(添加转账的金额)
    42. toAccount.setBalance(tobalance.add(money));
    43. toAccount.setCardNum(toCardNum);
    44. // 更新转账人和接收人的金额
    45. cardDao.update(formAccount);
    46. // 引发异常
    47. System.out.println(10 / 0);
    48. cardDao.update(toAccount);
    49. log.info("转账完成!");
    50. // demoService.add();
    51. // 调用本类的其他方法,不会启用事务
    52. // this.update();
    53. } else {
    54. log.info("卡号:" + formCardNum + ",余额不足 ");
    55. throw new RuntimeException("余额不足");
    56. }
    57. }
    58. public void update(){
    59. }
    60. }
    @Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.REQUIRED )
    

    当 @Transactional 写在类上时,表示这个类中的所有方法都享有事务功能 rollbackFor 属性表示当遇到什么异常将进行事务的回滚, 默认是遇到任意的运行时异常将自动回滚事务。 propagation 属性:表示事务传播级别,不同的事务传播级别,支持的事务范围将不一样(传播级别的类型说明参考文档)
     注意:在这种情况下事务传播级别是不会生效的, 当一个业务类的方法启用了一个事务,然后再调用本类的其他方法时,事务是失效的

    @Transactional 注解的 rollbackFor 属性用于指定在哪些异常发生时需要回滚事务。在示例中,rollbackFor = RuntimeException.class 表示只有当方法抛出运行时异常(RuntimeException)时才会回滚事务。

    同时,@Transactional 注解的 propagation 属性用于指定事务的传播行为。在示例中,propagation = Propagation.REQUIRED 表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。REQUIRED 是默认的传播行为。

    综合起来,使用 @Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.REQUIRED) 注解在方法上,表示:

    • 如果方法抛出运行时异常,则事务将会回滚。
    • 如果当前存在事务,则方法会加入该事务进行执行。
    • 如果当前没有事务,则会创建一个新的事务。

    这样可以确保在方法执行过程中发生异常时,事务会根据设置进行回滚,并保持数据的一致性。

     这里只是讲了常用的而已,还有很多其他的传播级别没有讲到!

    六、总结

    本次案例通过整合 Mybatis 和事务管理,综合起来讲了事务管理最常用的一些配置,并没有讲的很细,事务管理还有很多的属性配置,和传播级别还有事务类型等。不过这里讲的都是常用的应该差不多了,不过呢剩下的还是需要去了解的。

    七、gitee 案例

    完整代码地址:ch03 · qiuqiu/conformity-study - 码云 - 开源中国 (gitee.com)

  • 相关阅读:
    《XSS-Labs》02. Level 11~20
    前端进击笔记第二十二节 如何进行性能分析的自动化实现
    开题报告:基于java企业公司网站系统 毕业设计论文开题报告模板
    【Linux】线程同步
    2.3 编程语言分类
    网络安全(黑客)自学笔记
    AWS 中文入门开发教学 25- 高可用性设计 - 建立 ALB 负载均衡
    好用软件推荐
    C++基础第9章:序列与关联容器(2)——序列容器
    Git安装
  • 原文地址:https://blog.csdn.net/zhiqiuqiu2/article/details/133990724