在现代的软件开发领域,事务管理是一项至关重要的技术。无论是在企业应用程序还是在网站开发中,保证数据的一致性和完整性都是至关重要的。Spring Framework 提供了丰富的功能来简化事务管理,并与其它模块无缝集成,使得开发人员可以轻松地处理事务操作。
在本篇博客中,我们将深入探讨 Spring 框架中的事务管理,并介绍如何整合事务管理到我们的应用程序中。我们将学习什么是事务管理,为什么使用事务管理,以及如何配置和使用 Spring 的事务管理器来实现数据的一致性。
无论你是一个有经验的 Spring 开发人员,还是刚开始接触 Spring 的初学者,本篇博客都会为你提供清晰而全面的指导。让我们一同进入 Spring 整合事务管理的世界,探索其强大的功能和灵活性。
本次通过一个简单的银行转账的案例来介绍事务管理。
- create table card_infos(
- card_id int auto_increment primary key ,
- card_num varchar(20), -- 卡号
- balance decimal -- 余额
- );
-
- insert into card_infos(card_num, balance) VALUE ('456736478372714678',1000),('564738264736475823',1000);


-
- <dependencies>
-
- <dependency>
- <groupId>com.alibabagroupId>
- <artifactId>druidartifactId>
- <version>1.2.15version>
- dependency>
-
-
-
-
- <dependency>
- <groupId>org.springframeworkgroupId>
- <artifactId>spring-contextartifactId>
- <version>5.3.23version>
- dependency>
-
- <dependency>
- <groupId>org.springframeworkgroupId>
- <artifactId>spring-jdbcartifactId>
- <version>5.3.23version>
- dependency>
-
- <dependency>
- <groupId>org.springframeworkgroupId>
- <artifactId>spring-txartifactId>
- <version>5.3.23version>
- dependency>
-
- <dependency>
- <groupId>ch.qos.logbackgroupId>
- <artifactId>logback-classicartifactId>
- <version>1.4.5version>
- dependency>
-
-
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- <version>8.0.33version>
- dependency>
-
-
-
- <dependency>
- <groupId>org.mybatisgroupId>
- <artifactId>mybatisartifactId>
- <version>3.5.6version>
- dependency>
-
-
- <dependency>
- <groupId>org.mybatisgroupId>
- <artifactId>mybatis-springartifactId>
- <version>2.0.6version>
- dependency>
-
- <dependency>
- <groupId>org.projectlombokgroupId>
- <artifactId>lombokartifactId>
- <version>1.18.30version>
- dependency>
-
-
-
- dependencies>
- @Data
- public class CardInfos {
-
- private Integer cardId;
- private String cardNum;
- private BigDecimal balance;
-
- }
这个实体类
CardInfos主要用于表示一个卡片信息的数据模型。在软件开发中,实体类用于封装和表示某个具体对象的属性和行为。
-
- public interface CardDao {
-
- /**
- * 查询转账卡号是否有足够余额
- * @param cardNum
- * @return
- */
- CardInfos getAccountByCardNum(String cardNum);
-
- /**
- * 更新账号信息
- * @param cardInfos
- */
- void update(CardInfos cardInfos);
-
- }

根据题目要求:转账前需要插叙自己的余额是否充足。所以我们需要定义一个查询的方法,查询余额。那么当 A、B交易成功后,它们的余额都发生了改变,所以需要定义一个 update 修改的方法,对相应的用户的余额进行修改
CardService 接口
-
- public interface CardService {
-
- /**
- *
- * @param formCardNum 转账人卡号
- * @param toCardNum 收帐人卡号
- * @param money 金额
- */
- void transfer(String formCardNum, String toCardNum, BigDecimal money);
-
- /**
- * 查询转账卡号是否有足够余额
- * @param cardNum
- * @return
- */
- CardInfos getAccountByCardNum(String cardNum);
-
- /**
- * 更新账号信息
- * @param cardInfos
- */
- void update(CardInfos cardInfos);
-
- }
CardServiceImpl 实现类
- @Service
- @RequiredArgsConstructor
- @Slf4j
- public class CardServiceImpl implements CardService {
-
- private final CardDao cardDao;
-
- private final DemoService demoService;
-
- @Override
- public CardInfos getAccountByCardNum(String cardNum) {
- return cardDao.getAccountByCardNum(cardNum);
- }
-
- @Override
- public void update(CardInfos cardInfos) {
- cardDao.update(cardInfos);
- }
-
- /**
- * @param formCardNum 转账人卡号
- * @param toCardNum 收帐人卡号
- * @param money 金额
- */
- @Override
- public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
-
- // 获取转账人的信息
- CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
- // 获取接收人的信息
- CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
-
- // 转账人的金额
- BigDecimal formbalance = formAccount.getBalance();
- // 接收人的金额
- BigDecimal tobalance = toAccount.getBalance();
-
- // 转账人如果有足够的余额才进行转账
- if (formbalance.doubleValue() >= money.doubleValue()) {
-
- // 设置转账人的余额(扣除转账金额)
- formAccount.setBalance(formbalance.subtract(money));
- formAccount.setCardNum(formCardNum);
-
- // 设置接收人的余额(添加转账的金额)
- toAccount.setBalance(tobalance.add(money));
- toAccount.setCardNum(toCardNum);
-
- // 更新转账人和接收人的金额
- cardDao.update(formAccount);
- cardDao.update(toAccount);
-
- log.info("转账完成!");
-
-
- } else {
- log.info("卡号:" + formCardNum + ",余额不足 ");
- throw new RuntimeException("余额不足");
- }
- }
-
-
- }
作用:处理转账的业务逻辑
db.properties
- driver = com.mysql.cj.jdbc.Driver
- url = jdbc:mysql://localhost:3306/psm
- username= root
- password = 123456
- maxActive = 200
- initialSize = 5
- minIdle = 5
- maxWait = 2000
- minEvictableIdleTimeMillis = 300000
- timeBetweenEvictionRunsMillis = 60000
- testWhileIdle = true
- testOnReturn = false
- validationQuery = select 1
- poolPreparedStatements = false
作用:这段配置是一个典型的数据库连接池的配置,用于连接和管理与 MySQL 数据库的连接。 以上配置项提供了对数据库连接池的一些基本配置和管理,包括连接数量、连接等待时间、连接回收策略等。通过连接池来管理数据库连接可以提升系统性能,并且避免频繁地创建和销毁连接,从而减少资源开销和提高响应速度。
CardMapper.xml
- "1.0" encoding="utf-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="edu.nf.ch03.dao.CardDao">
-
- <resultMap id="cardMap" type="edu.nf.ch03.entity.CardInfos">
- <id property="cardId" column="card_id"/>
- <result property="cardNum" column="card_num"/>
- <result property="balance" column="balance"/>
- resultMap>
-
-
- <select id="getAccountByCardNum" parameterType="string" resultMap="cardMap">
- select balance from card_infos where card_num = #{cardNum}
- select>
-
-
- <update id="update" parameterType="edu.nf.ch03.entity.CardInfos">
- update card_infos set balance = #{balance} where card_num = #{cardNum}
- update>
-
-
- mapper>
作用:用于定义 DAO 接口
edu.nf.ch03.dao.CardDao中的 SQL 映射关系和操作语句。通过这个配置文件,Mybatis 可以自动生成对应的 SQL 语句,并提供给
edu.nf.ch03.dao.CardDao接口的实现类使用。在程序中,通过调用相应的方法,就可以执行对应的数据库操作,例如查询账户余额或更新账户余额。
-
- @Configuration
- @MapperScan(basePackages = "edu.nf.ch03.dao")
- @ComponentScan(basePackages = "edu.nf.ch03")
- public class AppConfig {
-
- @Bean(initMethod = "init",destroyMethod = "close")
- public DruidDataSource dataSource() throws Exception {
- Properties properties = new Properties();
- InputStream stream = AppConfig.class.getClassLoader().getResourceAsStream("db.properties");
- properties.load(stream);
- return (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
- }
-
- @Bean
- public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
- SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
- sqlSessionFactoryBean.setDataSource(dataSource);
- sqlSessionFactoryBean.setTypeAliasesPackage("edu.nf.homework.entity");
- PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
- Resource[] resources = resolver.getResources("classpath:mappers/*.xml");
- sqlSessionFactoryBean.setMapperLocations(resources);
-
- //启用mybatis日志功能
- org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
- conf.setLogImpl(StdOutImpl.class);
- //将日志配置设置到SqlSessionFactoryBean中
- sqlSessionFactoryBean.setConfiguration(conf);
- return sqlSessionFactoryBean;
- }
-
-
- }
作用:通过
dataSource方法配置了一个 Druid 数据源,通过sqlSessionFactoryBean方法配置了一个SqlSessionFactoryBean对象,设置了数据源、实体类别名包扫描路径和映射文件位置。这里还启用了 MyBatis 的日志功能,可以打印 SQL 执行的日志信息。
我们先测试一下保证转账功能可以正常运行。
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- CardService bean = context.getBean(CardService.class);
- // 转账金额
- BigDecimal bigDecimal = new BigDecimal("500");
- // 执行转账
- bean.transfer("564738264736475823","456736478372714678",bigDecimal);
-
- }
- }
运行结果

事务管理是为了确保数据库操作的一致性、可靠性和完整性而引入的机制。在一个业务操作中可能涉及多个数据库操作,而这些操作要么全部成功,要么全部失败,不能出现部分操作成功、部分操作失败的情况。事务管理可以确保这种一致性。
以下是几个使用事务管理的原因:
数据库一致性:如果一个业务操作需要执行多个数据库操作,例如插入、更新、删除等,那么这些操作之间可能存在依赖关系,需要保证在事务提交前所有操作都成功执行,如果其中某个操作失败了,整个事务可以回滚,保持数据的一致性。
并发控制:在多用户、多线程环境下,多个事务可能同时对数据库进行读写操作,如果没有事务管理,可能会导致数据的不一致或并发冲突。通过使用事务管理,可以有效控制并发访问,保证数据的正确性和完整性。
异常处理:在业务操作中可能发生各种异常情况,例如数据库连接异常、事务超时、网络异常等。如果没有事务管理,这些异常可能导致数据不一致或资源泄露。通过使用事务管理,可以对异常进行捕获和处理,保证数据的完整性,并进行相应的回滚操作。
性能优化:事务管理还可以提供一些性能优化的手段,例如批量提交、脏读、隔离级别设置等,可以根据业务需求和数据库性能要求进行相应的配置,提高数据库操作的效率和性能。
综上所述,事务管理是确保数据库操作的一致性和可靠性的重要机制,通过对多个操作进行事务管理,可以保证数据的正确性,并提供异常处理和性能优化的功能

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

看到问题没有!我们的异常已经生效了,但是是不是还扣除了转账人的金额。这就是我们为什么要使用事务的原因,那我们现在就来使用事务来管理这个实现类吧。
在 CardServiceImpl 中的 transfet 方法上使用 @Transactional 注解
- /**
- * @param formCardNum 转账人卡号
- * @param toCardNum 收帐人卡号
- * @param money 金额
- * @Transactional 注解使用 spring 提供的声明式事务,可以用在方法上,也可以用在类上
- * 当用在类上的时候,表示这个类的所有方法都享有事务功能
- */
- @Transactional
- @Override
- public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
-
- // 获取转账人的信息
- CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
- // 获取接收人的信息
- CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
-
- // 转账人的金额
- BigDecimal formbalance = formAccount.getBalance();
- // 接收人的金额
- BigDecimal tobalance = toAccount.getBalance();
-
- // 转账人如果有足够的余额才进行转账
- if (formbalance.doubleValue() >= money.doubleValue()) {
-
- // 设置转账人的余额(扣除转账金额)
- formAccount.setBalance(formbalance.subtract(money));
- formAccount.setCardNum(formCardNum);
-
- // 设置接收人的余额(添加转账的金额)
- toAccount.setBalance(tobalance.add(money));
- toAccount.setCardNum(toCardNum);
-
- // 更新转账人和接收人的金额
- cardDao.update(formAccount);
- // 引发异常
- System.out.println(10 / 0);
- cardDao.update(toAccount);
-
- log.info("转账完成!");
-
- // demoService.add();
-
- // 调用本类的其他方法,不会启用事务
- // this.update();
-
- } else {
- log.info("卡号:" + formCardNum + ",余额不足 ");
- throw new RuntimeException("余额不足");
- }
- }
@Transactional是 Spring 框架提供的一个注解,用于标记方法或类,表示该方法或类需要进行事务管理。使用
@Transactional注解时,Spring 会在方法执行前开启一个事务,并在方法执行完成后根据执行情况决定是提交事务还是回滚事务。在方法中如果发生了异常,则事务会回滚,保证数据的一致性。
@Transactional注解可以应用于方法级别和类级别:
- 方法级别:标注在方法上,表示该方法需要进行事务管理。
- 类级别:标注在类上,表示该类的所有方法都需要进行事务管理。
-
- @Configuration
- @MapperScan(basePackages = "edu.nf.ch03.dao")
- @ComponentScan(basePackages = "edu.nf.ch03")
- // 启用事务注解驱动,这样就可以在业务类中使用 @Transaction 注解
- @EnableTransactionManagement
- public class AppConfig {
-
- @Bean(initMethod = "init",destroyMethod = "close")
- public DruidDataSource dataSource() throws Exception {
- Properties properties = new Properties();
- InputStream stream = AppConfig.class.getClassLoader().getResourceAsStream("db.properties");
- properties.load(stream);
- return (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
- }
-
- @Bean
- public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
- SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
- sqlSessionFactoryBean.setDataSource(dataSource);
- sqlSessionFactoryBean.setTypeAliasesPackage("edu.nf.homework.entity");
- PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
- Resource[] resources = resolver.getResources("classpath:mappers/*.xml");
- sqlSessionFactoryBean.setMapperLocations(resources);
- //启用mybatis日志功能
- org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
- conf.setLogImpl(StdOutImpl.class);
- //将日志配置设置到SqlSessionFactoryBean中
- sqlSessionFactoryBean.setConfiguration(conf);
- return sqlSessionFactoryBean;
- }
-
- /**
- * 装配事务管理器,并注入数据源,
- * 这样事务管理器就可以基于 AOP 来管理 Connection 对象的事务操作
- * @param dataSource
- * @return
- */
- @Bean
- public PlatformTransactionManager txManager(DataSource dataSource){
- return new DataSourceTransactionManager(dataSource);
- }
-
- }
@EnableTransactionManagement是一个 Spring 框架提供的注解,用于启用Spring的事务管理功能。
通过添加
@EnableTransactionManagement注解,告诉 Spring 启用事务管理。Spring 将会根据配置寻找合适的事务管理器。确保已经配置了一个与数据源关联的事务管理器。可以使用
@Bean注解创建一个PlatformTransactionManager的实例,并将其与数据源相关联。
运行结果:

在引发异常后是不是并没有扣除转账人的余额了,因为使用事务管理,在引发异常时,就会终止这个流程,只要引发异常就不会再执行下去了。一句话总结:
在一个业务操作中可能涉及多个数据库操作,而这些操作要么全部成功,要么全部失败,不能出现部分操作成功、部分操作失败的情况。
-
- @Service
- @RequiredArgsConstructor
- @Slf4j
- @Transactional(rollbackFor = RuntimeException.class,
- propagation = Propagation.REQUIRED
- )
- public class CardServiceImpl implements CardService {
-
- private final CardDao cardDao;
-
- private final DemoService demoService;
-
- @Override
- public CardInfos getAccountByCardNum(String cardNum) {
- return cardDao.getAccountByCardNum(cardNum);
- }
-
- @Override
- public void update(CardInfos cardInfos) {
- cardDao.update(cardInfos);
- }
-
- /**
- * @param formCardNum 转账人卡号
- * @param toCardNum 收帐人卡号
- * @param money 金额
- * @Transactional 注解使用 spring 提供的声明式事务,可以用在方法上,也可以用在类上
- * 当用在类上的时候,表示这个类的所有方法都享有事务功能
- */
- // @Transactional
- @Override
- public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
-
- // 获取转账人的信息
- CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
- // 获取接收人的信息
- CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
-
- // 转账人的金额
- BigDecimal formbalance = formAccount.getBalance();
- // 接收人的金额
- BigDecimal tobalance = toAccount.getBalance();
-
- // 转账人如果有足够的余额才进行转账
- if (formbalance.doubleValue() >= money.doubleValue()) {
-
- // 设置转账人的余额(扣除转账金额)
- formAccount.setBalance(formbalance.subtract(money));
- formAccount.setCardNum(formCardNum);
-
- // 设置接收人的余额(添加转账的金额)
- toAccount.setBalance(tobalance.add(money));
- toAccount.setCardNum(toCardNum);
-
- // 更新转账人和接收人的金额
- cardDao.update(formAccount);
- // 引发异常
- System.out.println(10 / 0);
- cardDao.update(toAccount);
-
- log.info("转账完成!");
-
- // demoService.add();
-
- // 调用本类的其他方法,不会启用事务
- // this.update();
-
- } else {
- log.info("卡号:" + formCardNum + ",余额不足 ");
- throw new RuntimeException("余额不足");
- }
- }
-
- public void update(){
-
- }
-
- }
@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 和事务管理,综合起来讲了事务管理最常用的一些配置,并没有讲的很细,事务管理还有很多的属性配置,和传播级别还有事务类型等。不过这里讲的都是常用的应该差不多了,不过呢剩下的还是需要去了解的。
完整代码地址:ch03 · qiuqiu/conformity-study - 码云 - 开源中国 (gitee.com)