• (十六)Spring对事务的支持



    Spring学习目录

    上一篇:(十五)Spring之面向切面编程AOP

    下一篇:(十七)Spring6整合JUnit

    环境

    spring6里程碑版本的仓库
    依赖:spring context依赖、junit依赖、log4j2依赖、mysql驱动依赖、德鲁伊连接池依赖、@Resource注解依赖、spring jdbc依赖
    log4j2.xml文件放到类路径下。
    数据库:账户表t_act
    请添加图片描述
    初始数据:
    请添加图片描述
    实体类:Account

    /**
     * 简单的账户类
     */
    public class Account {
        private Long id;
        private String actno;
        private Double balance;
    
        public Account() {
        }
    
        public Account(Long id, String actno, Double balance) {
            this.id = id;
            this.actno = actno;
            this.balance = balance;
        }
    
        @Override
        public String toString() {
            return "Account{" +
                    "id=" + id +
                    ", actno='" + actno + '\'' +
                    ", balance=" + balance +
                    '}';
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getActno() {
            return actno;
        }
    
        public void setActno(String actno) {
            this.actno = actno;
        }
    
        public Double getBalance() {
            return balance;
        }
    
        public void setBalance(Double balance) {
            this.balance = balance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    事务概述

    什么是事务:

    • 在一个业务流程当中,通常需要多条DML(insert delete
      update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。
    • 多条DML要么同时成功,要么同时失败,这叫做事务。
    • 事务:Transaction(tx)

    事务的四个处理过程:

    • 第一步:开启事务 (start transaction)
    • 第二步:执行核心业务代码
    • 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
    • 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)

    事务的四个特性:

    • A 原子性:事务是最小的工作单元,不可再分。
    • C 一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。
    • I 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰。
    • D 持久性:持久性是事务结束的标志。

    引入事务场景

    以银行账户转账为例。两个账户act-001和act-002。act-001账户向act-002账户转账,必须同时成功,或者同时失败。(一个减成功,一个加成功, 这两条update语句必须同时成功,或同时失败。)

    连接数据库的技术采用Spring框架的JdbcTemplate

    采用三层架构搭建:

    第一步:准备环境

    相关依赖、数据库表(t_act)及数据、实体类(Account)

    第二步:编写持久层

    创建dao包,在dao包下创建接口:AccountDao

    /**
     * 专门负责账户信息的CRUD操作,不和业务挂钩
     */
    public interface AccountDao {
        /**
         * 根据账号查询账户信息
         * @param actno
         * @return
         */
        Account selectByActno(String actno);
    
        /**
         * 更新账户信息
         * @param act
         * @return
         */
        int update(Account act);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在dao层下,创建impl包,新建AccountDao的实现类:AccountDaoImpl

    @Repository("accountDao")
    public class AccountDaoImpl implements AccountDao {
    
        @Resource(name = "jdbcTemplate")
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public Account selectByActno(String actno) {
            String sql = "select * from t_act where actno = ?";
            Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
            return account;
        }
    
        @Override
        public int update(Account act) {
            String sql = "update t_act set balance = ? where actno = ?";
            int update = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
            return update;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    第三步:编写业务层

    创建service包,在service包创建接口:AccountService

    public interface AccountService {
        /**
         * 转账业务
         * @param fromAct 转出账户
         * @param toAct 转出账户
         * @param money 转账金额
         */
        void transfer(String fromAct,String toAct,double money);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在service层下创建impl包,编写AccountService的实现类:AccountServiceImpl

    @Service("accountService")
    public class AccountServiceImpl implements AccountService {
    
        @Resource(name = "accountDao")
        private AccountDao accountDao;
    
        @Override
        public void transfer(String fromActno, String toActno, double money) {
            //查询转出账户余额是否充足
            Account fromAccount = accountDao.selectByActno(fromAct);
            if (fromAccount.getBalance() < money) {//余额不足抛异常
                throw new RuntimeException("余额不足");
            }
            //修改内存中两个对象的余额
            Account toAccount = accountDao.selectByActno(toAct);
            fromAccount.setBalance(fromAccount.getBalance() - money);
            toAccount.setBalance(toAccount.getBalance() + money);
            //数据库更新
            int count = accountDao.update(fromAccount);
    
            //模拟异常
            /*String s = null;
            s.toString();*/
    
            count += accountDao.update(toAccount);
    
            if (count != 2) {
                throw new RuntimeException("转账失败");
            }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    第四步:编写Spring配置文件

    创建spring.xml:
    添加组件扫描、配置jdbcTemplate、配置德鲁伊连接池

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
        <!--添加组件扫描-->
        <context:component-scan base-package="com.bank"/>
    
        <!--配置jdbcTemplate-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--配置德鲁伊连接池-->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/mvc"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </bean>
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    第五步:编写表示层(测试程序)

    @Test
        public void testSpringTx(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
            try {
                accountService.transfer("act001","act002",1000);
                System.out.println("转账成功。。。");
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行程序:
    请添加图片描述
    数据库数据改变:
    请添加图片描述

    模拟异常

    把在业务代码里的模拟异常松开:

    @Service("accountService")
    public class AccountServiceImpl implements AccountService {
    
        @Resource(name = "accountDao")
        private AccountDao accountDao;
    
        @Override
        public void transfer(String fromActno, String toActno, double money) {
            //查询转出账户余额是否充足
            Account fromAccount = accountDao.selectByActno(fromAct);
            if (fromAccount.getBalance() < money) {//余额不足抛异常
                throw new RuntimeException("余额不足");
            }
            //修改内存中两个对象的余额
            Account toAccount = accountDao.selectByActno(toAct);
            fromAccount.setBalance(fromAccount.getBalance() - money);
            toAccount.setBalance(toAccount.getBalance() + money);
            //数据库更新
            int count = accountDao.update(fromAccount);
    
            //模拟异常
            String s = null;
            s.toString();
    
            count += accountDao.update(toAccount);
    
            if (count != 2) {
                throw new RuntimeException("转账失败");
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    再次运行:
    请添加图片描述
    数据库表中数据:
    请添加图片描述
    数据异常,丢失1千

    Spring对事务的支持

    Spring实现事务的两种方式

    • 编程式事务(基本不会用)
      通过编写代码的方式来实现事务的管理。
    • 声明式事务
      • 基于注解方式(常用)
      • 基于XML配置方式(不常用)

    Spring事务管理API

    Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
    在这里插入图片描述
    PlatformTransactionManager接口:spring事务管理器的核心接口。在Spring6中它有两个实现:

    • DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
    • JtaTransactionManager:支持分布式事务管理。

    如果要在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager来管理事务。(Spring内置写好了,可以直接用。)

    声明式事务之注解实现方式

    第一步:在spring配置文件中配置事务管理器。

    	<!--配置事务管理器-->
        <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!--设置数据源-->
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第二步:在spring配置文件中引入tx命名空间。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第三步:在spring配置文件中配置“事务注解驱动器”,开始注解的方式控制事务。

        <!--开启事务注解驱动器,开启事务注解-->
        <tx:annotation-driven transaction-manager="txManager"/>
    
    • 1
    • 2

    第四步:在service类上或方法上添加@Transactional注解
    @Transactional

    • 写在方法上,只针对于该方法应用事务。
    • 写在类上,代表这个类上所有方法应用事务。
    	@Override
        @Transactional
        public void transfer(String fromAct, String toAct, double money) {
            //查询转出账户余额是否充足
            Account fromAccount = accountDao.selectByActno(fromAct);
            if (fromAccount.getBalance() < money) {//余额不足抛异常
                throw new RuntimeException("余额不足");
            }
            //修改内存中两个对象的余额
            Account toAccount = accountDao.selectByActno(toAct);
            fromAccount.setBalance(fromAccount.getBalance() - money);
            toAccount.setBalance(toAccount.getBalance() + money);
            //数据库更新
            int count = accountDao.update(fromAccount);
    
            //模拟异常
            String s = null;
            s.toString();
    
            count += accountDao.update(toAccount);
    
            if (count != 2) {
                throw new RuntimeException("转账失败");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    执行测试程序:
    请添加图片描述
    虽然出现异常了,再次查看数据库表中数据:
    请添加图片描述
    发现数据没有变化,事务起作用了。

    事务属性

    事务属性包括哪些

    我们可以查看一下@Transactional注解源码:

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Transactional {
        @AliasFor("transactionManager")
        String value() default "";
    
        @AliasFor("value")
        String transactionManager() default "";
    
        String[] label() default {};
    
        Propagation propagation() default Propagation.REQUIRED;
    
        Isolation isolation() default Isolation.DEFAULT;
    
        int timeout() default -1;
    
        String timeoutString() default "";
    
        boolean readOnly() default false;
    
        Class<? extends Throwable>[] rollbackFor() default {};
    
        String[] rollbackForClassName() default {};
    
        Class<? extends Throwable>[] noRollbackFor() default {};
    
        String[] noRollbackForClassName() default {};
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    有很多的属性,我们只关注重点属性:
    事务中的重点属性:

    • 事务传播行为 propagation()
    • 事务隔离级别 isolation()
    • 事务超时 timeout()
    • 只读事务 readOnly()
    • 设置出现哪些异常回滚事务 rollbackFor()
    • 设置出现哪些异常不回滚事务 noRollbackFor()
    事务传播行为

    什么是事务的传播行为?
    例如在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。

    事务传播行为在spring框架中被定义为枚举类型:

    public enum Propagation {
        REQUIRED(0),
        SUPPORTS(1),
        MANDATORY(2),
        REQUIRES_NEW(3),
        NOT_SUPPORTED(4),
        NEVER(5),
        NESTED(6);
    
        private final int value;
    
        private Propagation(int value) {
            this.value = value;
        }
    
        public int value() {
            return this.value;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    一共有七种传播行为:

    • REQUIRED:支持当前事务,如果不存在就新建一个(默认)
      没有就新建,有就加入

    • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行
      有就加入,没有就不管了

    • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常
      有就加入,没有就抛异常

    • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起
      不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起

    • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务
      不支持事务,存在就挂起

    • NEVER:以非事务方式运行,如果有事务存在,抛出异常
      不支持事务,存在就抛异常

    • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。
      有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。

    演示程序

    需要在数据库中新增两个账户,在一个保存方法里调用另一个保存方法。
    首先在持久层接口添加方法,并在实现类实现:
    AccountDao接口新增方法:

        /**
         * 新增账户信息
         * @param act
         * @return
         */
        int insert(Account act);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    AccountDaoImpl实现:

        @Override
        public int insert(Account act) {
            String sql = "insert into t_act values(null,?,?)";
            int count = jdbcTemplate.update(sql, act.getActno(), act.getBalance());
            return count;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在业务层接口添加方法,及实现类实现:
    AccountService接口:

        /**
         * 保存账户信息
         * @param act
         */
        void save(Account act);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    新增第二个实现类:AccountServiceImpl2

    @Service("accountService2")
    public class AccountServiceImpl2 implements AccountService {
    
        @Resource(name = "accountDao")
        private AccountDao accountDao;
    
    
        @Override
        public void transfer(String fromAct, String toAct, double money) {
    
        }
    
        @Override
        @Transactional(propagation = Propagation.REQUIRED)
        public void save(Account act) {
            accountDao.insert(act);
            //模拟异常
            /*String s = null;
            s.toString();*/
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    AccountServiceImpl实现方法:调用AccountServiceImpl2的save方法

        @Resource(name = "accountService2")
        private AccountService accountService;
        @Override
        @Transactional(propagation = Propagation.REQUIRED)
        public void save(Account act) {
            //先保存act账户
            accountDao.insert(act);
    
            //再保存act2账户
            Account act2 = new Account();
            act2.setActno("act-004");
            act2.setBalance(1000.0);
            try {
                accountService.save(act2);//调用AccountServiceImpl2的save方法
            }catch (Exception e){
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    测试程序:

        @Test
        public void testPropagation(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
            Account account = new Account();
            account.setActno("act-003");
            account.setBalance(10000.0);
            accountService.save(account);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    请添加图片描述
    可以看见在两个SQL语句之间,Participating in existing transaction,翻译为:参与现有交易,实际上就是加入事务了,查看数据库数据:
    请添加图片描述
    正常插入。
    如果把AccountServiceImpl2模拟注释松开:

    @Override
        @Transactional(propagation = Propagation.REQUIRED)
        public void save(Account act) {
            accountDao.insert(act);
            //模拟异常
            String s = null;
            s.toString();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    发生异常了,回滚事务
    请添加图片描述
    数据库数据不改变:
    请添加图片描述
    如果把AccountServiceImpl2的事务传播类型换成REQUIRES_NEW

        @Override
        //@Transactional(propagation = Propagation.REQUIRED)
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void save(Account act) {
            accountDao.insert(act);
            //模拟异常
            String s = null;
            s.toString();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    再次运行:
    请添加图片描述
    可以看见在两个SQL语句之间,Suspending current transaction, creating new transaction with name,翻译为:正在挂起当前事务,正在创建名为的新事务
    并且事务2发生异常回滚了,然后
    Resuming suspended transaction after completion of inner transaction
    Initiating transaction commit
    翻译为:

    内部事务完成,恢复暂停的事务
    启动事务提交
    
    • 1
    • 2

    也就是说事务1不会受到事务2的影响,并且如果事务2发生异常,进行捕获,也捕获不到,发生异常后会自动回滚,事务1恢复,并且进行往下执行。

    查看数据库:只新增了事务1的账户
    请添加图片描述
    余下的事务传播行为就不演示了。

    事务隔离级别

    数据库中读取数据存在的三大问题:(三大读问题)

    • 脏读:读取到没有提交到数据库的数据,叫做脏读。
    • 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
    • 幻读:读到的数据是假的。

    事务隔离级别包括四个级别:

    • 读未提交:READ_UNCOMMITTED
      这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
    • 读提交:READ_COMMITTED
      解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
    • 可重复读:REPEATABLE_READ
      解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
    • 序列化:SERIALIZABLE
      解决了幻读问题,事务排队执行。不支持并发。

    表格归纳:

    隔离级别脏读不可重复读
    读未提交
    读提交
    可重复读
    序列化

    隔离级别在spring中也是以枚举类型存在:

    public enum Isolation {
        DEFAULT(-1),
        READ_UNCOMMITTED(1),
        READ_COMMITTED(2),
        REPEATABLE_READ(4),
        SERIALIZABLE(8);
    
        private final int value;
    
        private Isolation(int value) {
            this.value = value;
        }
    
        public int value() {
            return this.value;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • DEFAULT:
      这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应
    • READ_UNCOMMITTED: 读未提交
    • READ_COMMITTED:读提交
    • REPEATABLE_READ: 可重复读
    • SERIALIZABLE:序列化
    演示程序

    一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。
    创建AccountServiceImpl3类,不实现接口了,负责查询:

    /**
     * 演示隔离级别
     * 3号负责查询,4号负责插入
     */
    @Service("accountService3")
    public class AccountServiceImpl3 {
        @Resource(name = "accountDao")
        private AccountDao accountDao;
    
        /**
         * 根据账户查询
         * @param actno
         */
        @Transactional(isolation = Isolation.READ_UNCOMMITTED)
        public void getByActno(String actno){
            Account account = accountDao.selectByActno(actno);
            System.out.println("账户信息:" + account);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    创建AccountServiceImpl4类,不实现接口了,负责插入:

    @Service("accountService4")
    public class AccountServiceImpl4 {
        @Resource(name = "accountDao")
        private AccountDao accountDao;
    
        @Transactional
        public void save(Account act){        
            accountDao.insert(act);
            //让程序睡眠一会
            try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }       
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    测试程序:

        @Test
        public void testIsolation1() {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            AccountServiceImpl4 accountService4 = applicationContext.getBean("accountService4", AccountServiceImpl4.class);
            Account account = new Account();
            account.setActno("act-003");
            account.setBalance(50000.0);
            accountService4.save(account);
        }
    
        @Test
        public void testIsolation2(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            AccountServiceImpl3 accountService3 = applicationContext.getBean("accountService3", AccountServiceImpl3.class);
            accountService3.getByActno("act-003");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    执行前需要把数据库数据删除到初始数据。
    请添加图片描述

    先执行testIsolation1,在执行testIsolation2
    testIsolation1:
    请添加图片描述
    testIsolation2:读到了未提交的数据
    请添加图片描述
    等testIsolation1执行完后查看数据库数据:
    请添加图片描述
    余下的隔离级别就不演示了。

    事务超时

    事务超时:使用timeout=整数设置,表示超过多少秒如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。默认值是-1,表示没有时间限制。

    • 最后一条DML语句执行之后的时间就不会被记录到超时时间里。
    • 反之在最后一条DML语句执行之前的时间就会被记录到超时时间里。
    演示程序

    在AccountServiceImpl4里设置事务超时
    在最后一条DML语句执行之后让程序睡眠情况:

    	//@Transactional
    	@Transactional(timeout = 10)//设置超时事务10秒
        public void save(Account act) throws IOException {
           
            accountDao.insert(act);
            //让程序睡眠一会
            try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行测试程序testIsolation1:
    请添加图片描述
    事务提交了,睡眠程序没有计入超时时间,查看数据库数据:成功插入数据
    请添加图片描述
    如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句。
    在最后一条DML语句执行之前让程序睡眠情况:

    //@Transactional
        @Transactional(timeout = 10)
        public void save(Account act){
            try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            accountDao.insert(act);
            //让程序睡眠一会
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    再次运行测试程序testIsolation1:
    请添加图片描述
    睡眠时间计入超时时间,事务超时,进行了回滚,数据库数据:原封不动
    请添加图片描述

    只读事务

    使用readOnly = ture|false 配置,默认未false
    将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可执行。

    该特性的作用是:启动spring的优化策略。提高select语句执行效率。

    如果该事务中确实没有增删改操作,建议设置为只读事务。

    演示程序

    在AccountServiceImpl4里设置为只读事务

    	//@Transactional
        //@Transactional(timeout = 10)    
        @Transactional(readOnly = true)
        public void save(Account act) throws IOException {
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
    
            accountDao.insert(act);
            
            
            //让程序睡眠一会
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行测试程序testIsolation1:
    出现异常,说Connection是只读的,也就是事务是只读的,不允许导致数据修改
    请添加图片描述
    数据库数据不变:
    请添加图片描述

    设置哪些异常回滚事务

    使用rollbackFor = {异常1.class,异常2.class,...}进行设置,表示只有发生异常或该异常的子类异常才回滚。默认为空,表示所有异常都回滚。

    演示程序

    在AccountServiceImpl4里设置:

    	//@Transactional
        //@Transactional(timeout = 10)
        //@Transactional(readOnly = true)
        @Transactional(rollbackFor = RuntimeException.class)//只要发生RuntimeException或者这个子类的异常,都回滚
        public void save(Account act) throws IOException {
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
    
            accountDao.insert(act);
            //模拟异常
            //throw new IOException();//IO异常是编译时异常
            throw new RuntimeException();
            
            //让程序睡眠一会
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行测试程序testIsolation1:
    请添加图片描述
    数据库数据不变:
    请添加图片描述
    把异常改为其他异常,把另一个异常注释松开,注释RuntimeException异常,再次运行程序:
    请添加图片描述
    出现异常,但是事务提交了,数据库数据改变:
    请添加图片描述

    设置哪些异常不回滚事务

    使用norollbackFor = {异常1.class,异常2.class,...}进行设置,表示发生该异常或该异常的子类异常不回滚,其他异常则回滚,默认为空,表示所有异常都回滚。

    演示程序

    还是修改AccountServiceImpl4:

    	//@Transactional
        //@Transactional(timeout = 10)
        //@Transactional(readOnly = true)
        //@Transactional(rollbackFor = RuntimeException.class)//只要发生RuntimeException或者这个子类的异常,都回滚
        @Transactional(noRollbackFor = NullPointerException.class)
        public void save(Account act) throws IOException {
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
    
            accountDao.insert(act);
            //模拟异常
            //throw new IOException();//IO异常是编译时异常
            //throw new RuntimeException();
            throw new NullPointerException();
            //让程序睡眠一会
            /*try {
                Thread.sleep(1000 * 20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行测试程序testIsolation1:
    请添加图片描述
    发生了空指针异常,但是事务提交了,数据库数据改变:
    请添加图片描述
    把异常改为其他异常,把另一个RuntimeException异常注释松开,注释NullPointerException异常,再次运行程序:
    请添加图片描述
    发生异常,但是回滚了,数据库数据不变:
    请添加图片描述

    事务的全注解式开发

    编写一个类来代替配置文件,怎么编写jdbcTemplate、德鲁伊数据源、事务管理器这些Bean呢?

    可以在配置文件编写方法使用@Bean注解修饰,Bean的id就使用name属性设置。

    Spring框架看到这个@Bean注解的时候,会调用这个注解标注的方法,返回该方法的对象,该对象会自动纳入IoC容器管理。

    这个返回值就是Spring容器的Bean对象了,并且这个Bean对象的名字就是name。

    这些Bean的属性是怎么注入呢?
    简单类型的属性可以使用注解的value进行配置
    如果注入非简单类型的话,它是一个Bean,有两种注入方式

    • 1.想要注入哪个Bean,可以调用@Bean标注的那个方法
    • 2.通过参数传递,在方法@Bean标注的方法里设置一个参数,这个参数是你要注入的类型,Spring框架会自动寻找这个类型的Bean,传给这个方法使用。

    一般非简单类型使用第二种设置

    @Configuration// 代替Spring配置文件
    @ComponentScan("com.bank")// 代替组件扫描
    @EnableTransactionManagement //开启事务注解
    public class SpringConfig {
        @Bean(name = "jdbcTemplate")
        public JdbcTemplate getJdbcTemplate(DataSource dataSource){
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            //两种注入方式
            //jdbcTemplate.setDataSource(getDruidDataSource());
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
        @Bean(name = "dataSource")
        public DruidDataSource getDruidDataSource(){
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/mvc");
            dataSource.setUsername("root");
            dataSource.setPassword("root");
            return dataSource;
        }
        @Bean(name = "txManager")
        public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
            DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
            transactionManager.setDataSource(dataSource);
            return transactionManager;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    测试还是使用账户转账,把异常去掉:

        @Test
        public void testNoXML(){
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
            AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
            try {
                accountService.transfer("act001","act002",1000);
                System.out.println("转账成功。。。");
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请添加图片描述
    请添加图片描述

    声明式事务之XML实现方式

    测试的话还是使用账户转账。
    配置步骤:

    • 第一步:基础配置
    • 第二步:配置通知
    • 第三步:配置切面

    记得添加aspectj的依赖:

    <!--aspectj依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>6.0.0-M2</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    第一步:基础配置

    **添加aop的命名空间。**为后面配置通知和切面做准备。
    并且配置组件扫描、配置jdbcTemplate、配置德鲁伊连接池、配置事务管理器

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--添加组件扫描-->
        <context:component-scan base-package="com.bank"/>
        <!--配置jdbcTemplate-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--配置德鲁伊连接池-->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/mvc"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </bean>
        <!--配置事务管理器-->
        <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!--设置数据源-->
            <property name="dataSource" ref="dataSource"/>
        </bean>
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    第二步:配置通知

    配置通知:使用tx:advice标签

    • id属性:唯一标识
    • transaction-manager:配置关联事务管理器

    在tx:advice标签里面配置通知的相关属性,使用嵌套标签配置
    之前说的所有的事务属性都可以在标签以属性的方式配置
    如果方法比较多可以采用通配符的方式进行模糊匹配配置
    例如 就是save开始发方法都需要进行事务管理

    	<tx:advice id="txAdive" transaction-manager="txManager">
            <tx:attributes>
                <tx:method name="transfer"/>
                <tx:method name="save*"/>
            </tx:attributes>
        </tx:advice>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    第三步:配置切面

    配置切点:使用标签配置

    • id属性:唯一标识
    • expression:切点表达式

    配置切面:等于通知加切点,使用标签配置

    • advice-ref属性:通知的id
    • pointcut-ref属性:切点的id
    	<!--配置切面-->
        <aop:config>
            
            <aop:pointcut id="txPointcut" expression="execution(* com.bank.service..*(..))"/>
            <aop:advisor advice-ref="txAdive" pointcut-ref="txPointcut"/>
        </aop:config>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试

    将AccountServiceImpl类上的@Transactional注解注释或删除。并且模拟异常
    测试程序:

        @Test
        public void testXML(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
            try {
                accountService.transfer("act001","act002",1000);
                System.out.println("转账成功。。。");
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请添加图片描述
    数据库数据:
    请添加图片描述
    通过测试可以看到配置XML已经起作用了。

  • 相关阅读:
    IDEA2020.3无法输入中文问题解决
    视觉SLAM笔记一之经典框架解读:视觉里程估计、后端优化、回环检测、建图
    微服务项目搭建之技术选型
    递归实现指数型枚举
    北京冬奥一项AI黑科技即将走进大众:实时动捕三维姿态,误差不到5毫米
    计算机网络——物理层
    最短路径专题5 最短路径
    java基于springboot+vue的课程资源在线学习网站
    EIP-3664合约研究笔记04--Metacore平台功能分析
    【Spring Cloud 远程调用】管理员服务系统
  • 原文地址:https://blog.csdn.net/weixin_45832694/article/details/128053499