• 终极篇章——解释Spring事务,全网最全,看不懂不可能


    Spring事务

    欢迎关注公众号“小东方不败”,码字不易!

    0x01_回顾事务的概念

    相信你在学习mysql或者JDBC的时候一定接触过事务,这里的0x01_回顾事务的概念只作为简单的回顾,方便你回顾起事务的基本概念。

    事务是什么?

    事务(Transaction)指的是一个操作序列,该操作序列中的多个操作要么都做,要么都不做,是一个不可分割的工作单位,是数据库环境中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。

    目前常用的存储引擎有InnoDB(MySQL5.5以后默认的存储引擎)和MyISAM(MySQL5.5之前默认的存储引擎),其中InnoDB支持事务处理机制,而MyISAM不支持。

    事务的特点(ACID)

    ACID指的是:事务的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。这四个特性简称为ACID特性。


    事务处理可以确保除非事务性序列内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的序列,可以简化错误恢复并使应用程序更加可靠。

    但并不是所有的操作序列都可以称为事务,这是因为一个操作序列要成为事务,必须满足事务的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability).

    原子性

    原子是自然界最小的颗粒,具有不可再分的特性。事务中的所有操作可以看做一个原子,事务是应用中不可再分的最小的逻辑执行体

    使用事务对数据进行修改的操作序列,要么全部执行,要么全不执行。通常,某个事务中的操作都具有共同的目标,并且是相互依赖的。如果数据库系统只执行这些操作中的一部分,则可能会破坏事务的总体目标,而原子性消除了系统只处理部分操作的可能性。

    一致性

    一致性是指事务执行的结果必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库中只包含事务成功提交的结果时,数据库处于一致性状态。一致性是通过原子性来保证的

    例如:在转账时,只有保证转出和转入的金额一致才能构成事务。也就是说事务发生前和发生后,数据的总额依然匹配。

    隔离性

    隔离性是指各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务,都是隔离的。也就是说:并发执行的事务之间既不能看到对方的中间状态,也不能相互影响

    例如:在转账时,只有当A账户中的转出和B账户中转入操作都执行成功后才能看到A账户中的金额减少以及B账户中的金额增多。并且其他的事务对于转账操作的事务是不能产生任何影响的。

    持久性

    持久性指事务一旦提交,对数据所做的任何改变,都要记录到永久存储器中,通常是保存进物理数据库,即使数据库出现故障,提交的数据也应该能够恢复。但如果是由于外部原因导致的数据库故障,如硬盘被损坏,那么之前提交的数据则有可能会丢失。

    事务的并发问题

    事务的并发问题分为以下三种

    脏读(Dirty read)

    当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

    不可重复读(Unrepeatableread)

    指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

    (同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。

    幻读 (Phantom read)

    幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

    (同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读

    不可重复读和幻读区别

    不可重复读的重点是修改,幻读的重点在于新增或者删除。

    解决不可重复读和幻读的问题

    解决不可重复读只需锁住满足条件的行,解决幻读需要锁表

    事务的隔离级别

    事务的隔离级别用于决定如何控制并发用户读写数据的操作。数据库是允许多用户并发访问的,如果多个用户同时开启事务并对同一数据进行读写操作的话,有可能会出现脏读、不可重复读和幻读问题,所以MySQL中提供了四种隔离级别来解决上述问题

    事务的隔离级别从低到高依次为READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ以及SERIALIZABLE,隔离级别越低,越能支持高并发的数据库操作。

    隔离级别脏读不可重复读幻读
    READ UNCOMMITTED
    READ COMMITTED×
    REPEATABLE READ××
    SERIALIZABLE×××

    学习Spring中事务控制要了解的几个点

    1.Spring事务控制的实现有两种编程风格:

    • 编程式:其实就是try-catch-finally,举个例子:
    try{
               ......
                //设置事务手动提交
                ......
            }catch (Exception e){
      					//捕获异常
                ......
                //回滚事务
                ......
            }finally {
                //提交事务
                ......
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    编程式进行事务控制有以下缺点:

    1. 太多无用的固定代码
    2. 如果一个请求需要调用多个服务接口,难以更精细的控制事务
    3. 跨多种底层数据层,如jdbc,mybatis,hibernate,jta,难以统一编码方式。

    • 声明式(重点)

    有两种开发方法:注解和XML,其中XML配置起来略微麻烦,不建议掌握,了解即可。但是注解方式进行事务控制是必须掌握的。

    Spring声明式事务的实现方式,底层就是AOP,AOP的底层就是动态代理。

    spring提供了声明式事务,使得我们不用关注底层的具体实现,屏蔽了多种不同的底层实现细节,为了支持多种复杂业务对事务的精细控制,spring提供了事务的传播属性,结合声明式事务,成就了一大事务利器。

    0x02_项目搭建

    创建模块

    创建一个标准的maven模块即可(不勾选任何archetype)

    image-20221105200541692

    导入依赖

    在pom.xml中导入依赖:

    注意,个别依赖,需要根据你本地的环境而更改,比如mysql的驱动,我的mysql版本是8,所以用的驱动是下面这样的。

    <dependencies>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>5.3.23version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-aspectsartifactId>
            <version>5.3.23version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.1.10version>
        dependency>
        
        <dependency>
            <groupId>aopalliancegroupId>
            <artifactId>aopallianceartifactId>
            <version>1.0version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.30version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>5.3.23version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-ormartifactId>
            <version>5.3.23version>
        dependency>
        
        <dependency>
            <groupId>commons-logginggroupId>
            <artifactId>commons-loggingartifactId>
            <version>1.2version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-txartifactId>
            <version>5.3.23version>
        dependency>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.13.2version>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.24version>
            <scope>providedscope>
        dependency>
    
    dependencies>
    
    • 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
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    准备数据库

    这里直接提供一个account表的建表语句:

    /*
     Navicat Premium Data Transfer
    
     Source Server         : testconn
     Source Server Type    : MySQL
     Source Server Version : 80026
     Source Host           : localhost:3306
     Source Schema         : mydb
    
     Target Server Type    : MySQL
     Target Server Version : 80026
     File Encoding         : 65001
    
     Date: 05/11/2022 20:12:33
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for accout
    -- ----------------------------
    DROP TABLE IF EXISTS `accout`;
    CREATE TABLE `accout` (
      `id` int DEFAULT NULL,
      `name` varchar(255) DEFAULT NULL,
      `money` varchar(255) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    
    -- ----------------------------
    -- Records of accout
    -- ----------------------------
    BEGIN;
    INSERT INTO `accout` (`id`, `name`, `money`) VALUES (1, 'Amy', '10000');
    INSERT INTO `accout` (`id`, `name`, `money`) VALUES (2, 'Bob', '10000');
    COMMIT;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
    • 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

    项目结构搭建

    image-20221105202315758

    resources目录下的两个配置文件:

    jdbc.properties

    注意:

    1.根据你的实际情况修改properties配置文件,需要更改用户名和密码的话。

    2.其次如果你的mysql版本是8,建议用下面的URL(当然要连接的数据库名可能需要根据你的数据库更改),特别是后面的参数?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

    以避免发生不可描述的错误

    3.如果你的mysql版本不是8,那么请注意,你想要自己百度寻找适合你的版本的URL写法。(其次,也要根据你的情况,修改依赖)

    jdbc_username=root
    jdbc_password=root
    jdbc_driver=com.mysql.cj.jdbc.Driver
    jdbc_url=jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    
    • 1
    • 2
    • 3
    • 4

    applicationContext.xml

    
    
    • 1

    pojo/Account实体类:

    package com.bones.pojo;
    
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Account {
        private Integer id;
        private String name;
        private Integer money;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    service/AccountService接口

    package com.bones.service;
    
    public interface AccountService {
        int transferMoney(int from,int to ,int money);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    service/impl/AccountServiceImpl实现类:

    package com.bones.service.impl;
    
    import com.bones.dao.AccountDao;
    import com.bones.service.AccountService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class AccountServiceImpl implements AccountService {
        @Autowired
        private AccountDao  accountDao;
        @Override
        public int transferMoney(int from, int to, int money) {
            int rows = 0;
            rows += accountDao.transferMoney(from,0-money);
            int i = 1/0;
            rows += accountDao.transferMoney(to, money);
            return rows;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    dao/AccountDao接口:

    package com.bones.dao;
    
    public interface AccountDao {
        int transferMoney(int id,int money);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    dao/impl/AccountDaoImpl

    package com.bones.dao.impl;
    
    import com.bones.dao.AccountDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class AccountDaoImpl implements AccountDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
        @Override
        public int transferMoney(int id, int money) {
            String sql = "update account set money = money +? where id = ?";
            return jdbcTemplate.update(sql, money, id);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    test包下面的测试类:

    package com.bones.test01;
    
    import com.bones.service.AccountService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class Test1 {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        @Test
        public void testTransferMoney(){
            AccountService accountService = context.getBean(AccountService.class);
            int res = accountService.transferMoney(1, 2, 1000);
            System.out.println("res = " + res);
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上面的service层的实现类中,模拟了一个异常int i = 1/0,为的是模拟并发情况下的异常情况。(虽然 / by zero不是一个并发异常):

    @Override
    public int transferMoney(int from, int to, int money) {
        int rows = 0;
        rows += accountDao.transferMoney(from,0-money);
        int i = 1/0;
        rows += accountDao.transferMoney(to, money);
        return rows;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    异常前的方法:accountDao.transferMoney(from,0-money);是执行了并且提交了事务,但是异常之后的方法accountDao.transferMoney(to, money);,没有执行。此时账户就出现了不一致。所以应该将一个转出钱和另一个账户收到钱作为一个事务,进行执行,提交。如果出现异常,那么已经执行的方法应该回滚,然后不管是否出现异常都应该提交。

    下面实验完成Spring中的控制事务。

    0x03_注解方式实现事务控制

    配置事务控制

    applicationContext.xml添加控制事务控制的配置:

    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    bean>
    
    • 1
    • 2
    • 3
    • 4

    如果报红,那么就是没有配置tx的命名空间:

    image-20221105205718131

    然后在需要事务控制的方法前面加上@Transactional注解即可。

    image-20221105205815749

    @Transactional注解也可以加在类上,此时表示对于这个类的所有方法进行事务控制。

    0x04_物理事务和逻辑事务

    Spring事务模块的文档中有描述了两个专业术语,分别是物理事务(physical transaciton)逻辑事务(logic transaction)。读者可能会一头雾水,因为Spring文档中并没有对这两个术语进行过多的介绍。

    物理事务
    在JDBC的事务API中展示了事务对应的是Connection,一个Connection只能开启一个事务。所谓物理事务指的就是一个Connection。

    逻辑事务
    在一个复杂的业务系统中,可能会调用多个service,每个service都有自己的事务(标注了@Transactional),此时我们需要根据事务传播方式(Propagation)来决定当前事务的行为(比如要挂起创建新事务,还是加入当前事务)。

    我们可以认为每个注解了@Transactional的方法都是一个逻辑事务,这些逻辑事务被Spring事务管理,Spring会根据事务传播方式来决定是否开启新事务。


    一个物理事务对应着一个JDBC Connection。一个Connection可能会存在多个逻辑事务,这取决于Propagation的配置。

    0x05_事务的传播行为

    这个主要说的是@Transactionalpropagation配置

    @Transactional源码中,有默认值Propagation.REQUIRED

    @Transactional源码:

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //
    
    package org.springframework.transaction.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Inherited;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import org.springframework.core.annotation.AliasFor;
    
    @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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    TransactionDefinition类中,spring提供了6种传播属性,接下来分别用简单示例来说明。

    面试会问到的是PROPAGATION_REQUIRED(默认值)和PROPAGATN_REQUIRES_NEW

    image-20221105211354218

    下文提到的加入当前事务,指的是底层使用同一个Connection,但是事务状态对象是可以重新创建的,并不影响。文章提到的当前只存在一个事务,表示的是共用底层的一个Connection,而不在乎创建了多少个事务状态对象(TransactionStatus,是对事务状态的抽象)。

    PROPAGATION_REQUIRED(默认值)

    如果当前已经存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。

    (下面的例子是基于上面的代码的。)

    数据库中重置为money都是10000

    @Transactional
    public void service(){
        serviceA();
        serviceB();
    }
    
    @Transactional
    void serviceA(){
        accountDao.transferMoney(1,1000);
    }
    @Transactional
    void serviceB(){
        accountDao.transferMoney(2,1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    serviceA serviceB都声明了事务,默认情况下,propagation=PROPAGATION_REQUIRED,整个service调用过程中,只存在一个共享的事务,当有任何异常发生的时候,所有操作回滚。

    现在在serviceA或者serviceB或者service任何一个方法中制造异常,都会回滚。

    PROPAGATN_REQUIRES_NEW

    如果当前存在事务,先把当前事务相关内容封装到一个实体,然后重新创建一个新事务,接受这个实体为参数,用于事务的恢复。更直白的说法就是暂停当前事务(当前无事务则不需要),创建一个新事务。 针对这种情况,两个事务没有依赖关系,可以实现新事务回滚了,但外部事务继续执行。

    @Transactional
    public void service(){
        serviceA();
        try {
            serviceB();
        }catch (Exception e){
    
        }
    }
    @Transactional
    void serviceA(){
        accountDao.transferMoney(1,1000);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void serviceB(){
        int i = 1/0;
        accountDao.transferMoney(2,1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    当调用service接口时,由于serviceB使用的是REQUIRES_NEW,它会创建一个新的事务,但由于serviceB抛出了运行时异常,导致serviceB整个被回滚了,而在service方法中,捕获了异常,所以serviceA是正常提交的。 注意,service中的try … catch 代码是必须的,否则service也会抛出异常,导致serviceA也被回滚。

    PROPAGATION_SUPPORTS

    如果当前已经存在事务,那么加入该事务,否则创建一个所谓的空事务(可以认为无事务执行)。

    public void service(){
        serviceA();
        throw new RuntimeException();
    }
    
    @Transactional(propagation=Propagation.SUPPORTS)
    void serviceA(){
        accountDao.transferMoney(1,1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    service执行时当前没有事务,所以service中抛出的异常不会导致 serviceA回滚

    PROPAGATION_MANDATORY

    当前方法必须在一个事务中运行,如果没有事务,将抛出异常

    @Transactional(propagation = Propagation.REQUIRED)
    public void service(){
        serviceA();
        int i = 1/0;
    }
    
    @Transactional(propagation=Propagation.MANDATORY)
    void serviceA(){
        accountDao.transferMoney(1,1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    service()调用serviceA()方法的时候,因为外部已经存在了物理事务(通过Propagation.REQUIRED创建),那么serviceA()将会加入这个事务,如果内部事务回滚了,外部事务也会回滚,这一点和Propagation.REQUIRED相同。

    NOT_SUPPORTED

    如果当前存在事务,挂起当前事务,然后新的方法在没有事务的环境中执行,没有spring事务的环境下,sql的提交完全依赖于 defaultAutoCommit属性值

    PROPAGATION_NEVER

    如果当前存在事务,则抛出异常,否则在无事务环境上执行代码。

    总结一下:

    事务传播行为类型说明
    PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择(默认)。
    PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
    PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
    PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
    PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
    PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

    0x06_事务的隔离级别

    1. DEFAULT (默认)
      这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。
      MySQL默认REPEATABLE_READ

    Oracle默认READ_COMMITTED

    1. READ_UNCOMMITTED (读未提交)
      这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。

    2. READ_COMMITTED (读已提交)
      保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。

    3. REPEATABLE_READ (可重复读)
      这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读。

    4. SERIALIZABLE(串行化)
      这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防止脏读、不可重复读外,还避免了幻像读。

    0x07_其他配置事务的参数

    timeout 超时时间

    事务一定要在多长时间之内提交,如果不提交就会回滚

    readOnly 只读事务

    事务是否只能读取数据库的数据,如果为true,则不允许进行增删改

    rollbackFor 指定发生回滚的异常

    当方法发生哪些异常时才会回滚

    noRollbackFor 指定不发生回滚的异常

    当方法发生哪些异常时,不会回滚

    0x08_XML方式控制事务

    实际中XML方式控制事务用的很少,因为配置较为繁琐。

    关键的配置:

    
    <tx:advice id="txAdvice">
    
        <tx:attributes>
            <tx:method name="transferMoney" isolation="DEFAULT" propagation="REQUIRED"/>
        tx:attributes>
    tx:advice>
    
    
    <aop:config>
    
        <aop:pointcut id="pt" expression="execution(* com.bones.service.AccountService.transferMoney(..))"/>
    
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
    aop:config>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    完整的applicationContext.xml

    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:c="http://www.springframework.org/schema/c"
           xmlns:util="http://www.springframework.org/schema/util"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           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/util
           http://www.springframework.org/schema/util/spring-util.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
             http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
    ">
    
        <context:component-scan base-package="com.bones"/>
    
        <context:property-placeholder location="classpath:jdbc.properties"/>
    
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="url" value="${jdbc_url}"/>
            <property name="username" value="${jdbc_username}"/>
            <property name="password" value="${jdbc_password}"/>
            <property name="driverClassName" value="${jdbc_driver}"/>
        bean>
        
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        
            <property name="dataSource" ref="dataSource"/>
        bean>
    
    
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        bean>
    
    
        <tx:advice id="txAdvice">
    
            <tx:attributes>
                <tx:method name="transferMoney" isolation="DEFAULT" propagation="REQUIRED"/>
            tx:attributes>
        tx:advice>
    
    
        <aop:config>
    
            <aop:pointcut id="pt" expression="execution(* com.bones.service.AccountService.transferMoney(..))"/>
    
            <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
        aop:config>
    
    
    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
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    0x09_零XML方式配置事务

    其实XML方式在springboot中就会淘汰,变成yml配置。所以零XML方式配置事务也作为了解即可(其实就是准备一个配置类SpringConfig)。

    SpringConfig.xml

    package com.bones.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    @Configuration //定义配置类
    @ComponentScan(basePackages = "com.bones")//扫描哪个包
    @PropertySource("classpath:jdbc.properties")
    @EnableTransactionManagement //开启注解控制事务
    public class SpringConfig {
        @Value("${jdbc_username}")
        private String username;
        @Value("${jdbc_password}")
        private String password;
        @Value("${jdbc_url}")
        private String url;
        @Value("${jdbc_driver}")
        private String driver;
    
    
        @Bean
        public DruidDataSource getDruidDataSource(){
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setPassword(password);
            druidDataSource.setUsername(username);
            druidDataSource.setUrl(url);
            druidDataSource.setDriverClassName(driver);
            return druidDataSource;
        }
    
        @Bean
        public JdbcTemplate getJdbcTemplate(DataSource dataSource){
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
    
        @Bean
        public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            dataSourceTransactionManager.setDataSource(dataSource);
            return dataSourceTransactionManager;
        }
    
    }
    
    
    • 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
    • 52
    • 53
    • 54
    • 55

    注意:

    • @Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器

    • @EnableTransactionManagement 开启注解控制事务

    • @Value("${jdbc_username}")读取properties配置文件中的内容

    • @Bean
          public JdbcTemplate getJdbcTemplate(DataSource dataSource){
              JdbcTemplate jdbcTemplate = new JdbcTemplate();
              jdbcTemplate.setDataSource(dataSource);
              return jdbcTemplate;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      上面初始化JdbcTemplate对象的方法中,需要传入参数DataSource dataSource,特别注意,这个参数是从Spring容器中取的。getDataSourceTransactionManager方法也是一样。

  • 相关阅读:
    java计算机毕业设计医疗器械销售电子商城源码+系统+mysql数据库+lw文档
    LeetCode高频题88. 合并两个有序数组
    2021-03-11 51蛋骗鸡串口中断流水灯回复
    [kafka]二.优化(如何保证不丢数据,且不重复[一次且仅一次])
    ATTransUNet:一种增强型混合Transformer结构用于超声图像分割
    超全整理,Jmeter接口性能测试-Beanshell调用jar包加密(详细)
    第八章《Java高级语法》第1节:数制及数制间的转换
    Python学习:类与实例
    文献 | 教师主观幸福感变迁:横断历史研究的视角
    记录一次Powerjob踩的坑(Failed to deserialize message)
  • 原文地址:https://blog.csdn.net/qq_51550750/article/details/127715745