Spring framework day 04:Spring 整合 事务管理

前言

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

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

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

一、前期准备

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

1、准备一张表 cardInfos
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);
2、要求

 3、新建项目,结构如下

4、导入 spring 依赖

    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>


        <!-- spring 的核心依赖 -->
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.23</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.23</version>
        </dependency>
        <!-- spring 事务框架,支持编程式事务也支持声明式事务,
             声明式事务基于 AOP 来实现
        -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.3.23</version>
        </dependency>

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.5</version>
        </dependency>


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <!-- Mybatis 依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>



    </dependencies>

 二、开始学习

1、在 entity 包下新建一个 CardInfos 实体类
@Data
public class CardInfos {

    private Integer cardId;
    private String cardNum;
    private BigDecimal balance;

}

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

2、在 dao 包下新建一个 CardDao 接口

public interface CardDao {

    /**
     * 查询转账卡号是否有足够余额
     * @param cardNum
     * @return
     */
    CardInfos getAccountByCardNum(String cardNum);

    /**
     * 更新账号信息
     * @param cardInfos
     */
    void update(CardInfos cardInfos);

}
 分析:

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

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

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("余额不足");
        }
    }


}

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

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

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

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE 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 接口的实现类使用。在程序中,通过调用相应的方法,就可以执行对应的数据库操作,例如查询账户余额或更新账户余额。

5、 在 config 包下新建一个 AppConfig 配置类

@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 执行的日志信息。

 6、测试

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

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);

    }
}

运行结果

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

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

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

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

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

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

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

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

四、使用事务管理

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

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

运行结果:

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

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

在 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 注解可以应用于方法级别和类级别:

  • 方法级别:标注在方法上,表示该方法需要进行事务管理。
  • 类级别:标注在类上,表示该类的所有方法都需要进行事务管理。
 1、更改 AppConfig 配置类

@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的事务管理功能。 

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

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

2、测试

 运行结果:

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

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

 五、事务传播级别

1、使用事务(类上)

@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 和事务管理,综合起来讲了事务管理最常用的一些配置,并没有讲的很细,事务管理还有很多的属性配置,和传播级别还有事务类型等。不过这里讲的都是常用的应该差不多了,不过呢剩下的还是需要去了解的。

七、gitee 案例

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值