拿下 Spring 事务

2022-08-30  本文已影响0人  分布式与微服务

什么是事务

事务是数据库操作的最基本单元,是逻辑上的一组操作,要么都成功,要么都失败。是一个不可分割的工作单元。

事务的使用

事务具有 4 个特性:原子性、一致性、隔离性】持久性,简称为 ACID 特性。

为什么要用事务

举例:银行转账。小明给小红转 100 元。小明需要减少余额 100,小红需要增加余额 100。这是两个操作,需要一起成功。如果在小明转账成功之后发生了异常,就会出现小明 减 100 余额,但是小红并没有加 100 余额。就会造成钱丢失的情况。这是绝对不允许的。伪代码如下:

public void accountMoney() {
    int money = 100;
    //小明 少 100
    userDao.reduceMoney(money);
    // 其他业务 发生异常
    int i = 1/0;
    //小红 多 100
    userDao.addMoney(money);
}

事务管理方式

Spring 支持 2 种事务管理方式。

事务管理器

Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。

PlatformTransactionManager

Spring 提供了一个 PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下:

public interface PlatformTransactionManager {
// 根据传入的 TransactionDefinition 对象获取一个事务状态对象
   TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

// 提交事务
   void commit(TransactionStatus var1) throws TransactionException;
// 回滚事务
   void rollback(TransactionStatus var1) throws TransactionException;
}

该接口的源码很简单。这个接口针对不同的框架提供了不同的实现类,如下:

实现类 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 提供给 Spring JDBC 、MBatis 的事务管理器
org.springframework.orm.hibernate5.HibernateTransactionManager 提供给 Hibernate 的事务管理器
org.springframework.orm.jpa.JpaTransactionManager 提供给 JPA 的事务管理器
org.springframework.jdo.JdoTransactionManager 提供给 Jdo 的事务管理器
org.springframework.transaction.jta.JtaTransactionManager 提供给 JTA 的事务管理器

注意:这些实现类,需要导入对应的依赖才能看到。 该接口中还有两个对象,分别是 TransactionDefinition 和 TransactionStatus。

TransactionDefinition

public interface TransactionDefinition {

    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;

    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;

    int TIMEOUT_DEFAULT = -1;

    default int getPropagationBehavior() {
        return 0;
    }

    default int getIsolationLevel() {
        return -1;
    }

    default int getTimeout() {
        return -1;
    }

    default boolean isReadOnly() {
        return false;
    }

    @Nullable
    default String getName() {
        return null;
    }

    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

TransactionStatus

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

    boolean hasSavepoint();

    @Override
    void flush();

}

方法说明如下:

名称 说明
hasSavepoint 事务内部是否带有保存点
flush 刷新事务
public interface TransactionExecution {

   boolean isNewTransaction();

   void setRollbackOnly();

   boolean isRollbackOnly();

   boolean isCompleted();

}

方法说明如下:

名称 说明
isNewTransaction 当前事务是否是新的
setRollbackOnly 设置事务回滚
isRollbackOnly 事务是否已被标记为回滚
isCompleted 事务是否完成,即是否已经提交或回滚
public interface SavepointManager {

    Object createSavepoint() throws TransactionException;

    void rollbackToSavepoint(Object savepoint) throws TransactionException;

    void releaseSavepoint(Object savepoint) throws TransactionException;

}

方法说明如下:

名称 说明
createSavepoint 创建保存点
rollbackToSavepoint 回滚到给定的保存点
releaseSavepoint 释放给定的保存点

有一个默认的抽象实现 AbstractTransactionStatus,对 TransactionExecution、savepoint、SavepointManager 有具体的实现逻辑,代码有点多,就不贴了,但是非常好理解。 对 TransactionExecution、savepoint、SavepointManager 有具体的实现逻辑,代码有点多,就不贴了,但是非常好理解。 DefaultTransactionStatus 又继承了 AbstractTransactionStatus,继续进行扩充。

public class DefaultTransactionStatus extends AbstractTransactionStatus {

    // 具体事务对象
    @Nullable
    private final Object transaction;

    //是否是新开启的事务(=true时才会提交事务)
    private final boolean newTransaction;

// 是否是新建的同步器(=true时才会执行回调事件)
    private final boolean newSynchronization;

// 是否只读
    private final boolean readOnly;
// 是否已调试
    private final boolean debug;

// 挂起的资源信息(事务传播行为要求挂起当前事务时,挂起的事务暂存信息,执行完后用于恢复)
    @Nullable
    private final Object suspendedResources;

    //省略。。。。
}

事务传播行为

事务传播行为指的是,多事务方法之间进行调用时,这个过程中事务应该如何进行管理。例如,事务方法 A 在调用事务方法 B 时,B 方法是在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。

事务方法:能让数据库表数据发生改变的方法,例如新增、删除、修改数据的方法。

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}

行为 说明
REQUIRED 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。
MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
REQUIRES_NEW 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起
NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED 如果当前存在事务,则创建一个新事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。

根据上面的描述,我们可以将行为分为三大类。

隔离级别

事务有一个特性为隔离性,多事务操作之间不会产生影响。但如果不考虑隔离性,则会产生三个读问题:脏读、不可重复读、虚(幻)读。

加入依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.6</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

xml方式

我们先来看看不使用事务会发生什么情况。创建名为 aopxml的包。

提供数据库脚本

CREATE TABLE `tx_test` (
  `id` int(11) NOT NULL,
  `name` varchar(64) DEFAULT NULL,
  `money` decimal(10,0) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `test`.`tx_test`(`id`, `name`, `money`) VALUES (1, '张三', 1000);
INSERT INTO `test`.`tx_test`(`id`, `name`, `money`) VALUES (2, '李四', 1000);

开发代码

新建 dao 包

在类中提供两个方法,一个张三增加金额,一个李四减金额。

@Repository
public class TXDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 给张三增加金额
     */
    public void add(){
        String sql = "update `tx_test`  set money = money + 100 where id = 1;";
        jdbcTemplate.update(sql);
    }

    /**
     * 给李四减金额
     */
    public void reduce(){
        String sql = "update `tx_test`  set money = money - 100 where id = 2;";
        jdbcTemplate.update(sql);
    }
}

新建 entity

public class TxTest {

    private Integer id;

    private String name;

    private BigDecimal money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getMoney() {
        return money;
    }

    public void setMoney(BigDecimal money) {
        this.money = money;
    }
}

新建 service 包

@Service
public class TXServiceImpl {

    @Autowired
    private TXDao tx;

    public void transfer(){
        tx.add();
        int i = 1/0;
        tx.reduce();
    }
}

项目结构如下:

测试

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("tx.xml");
    TXServiceImpl bean = context.getBean(TXServiceImpl.class);
    bean.transfer();
}

控制台出现异常

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at cn.cxyxj.txannon.service.TestServiceImpl.transfer(TestServiceImpl.java:20)
    at cn.cxyxj.txannon.AppMain.main(AppMain.java:20)

再来查看数据库数据,可以发现张三的金额增加了,但是李四的金额没有减。银行哭死!!! 所以我们需要引入 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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" 
       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/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd 
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd">

</beans>

注意: 上面说过 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此还需要添加 aop 命名空间配置。当然我还额外引入了 spring-context 命名空间。

配置事务管理器以及 JdbcTemplate

<!--引入 jdbc.properties 中的配置-->
    <context:property-placeholder location="classpath:jdbc.properties">
    </context:property-placeholder>

    <!--配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!--数据库连接地址-->
        <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>

    <!--定义 JdbcTemplate Bean-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--将数据源的 Bean 注入到 JdbcTemplate 中-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事务管理器,以 JDBC 为例-->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

配置的事务管理器实现为 DataSourceTransactionManager,是 JDBC 和 MBatis 的PlatformTransactionManager 接口实现。

jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.50.172/tx_test?useSSL=false
jdbc.username=Yj19980402
jdbc.password=root

配置事务通知

配置事务通知,指定所需要使用的事务管理器以及指定事务作用的方法和该事务属性。

 <!--配置通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--配置事务参数-->
        <tx:attributes>
            <!--指定哪个方法上面添加事务-->
            <tx:method name="transfer*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/>
            <!--可以配置多个方法 <tx:method name="account*"/>-->
        </tx:attributes>
    </tx:advice>

transaction-manage参数的默认值就是 transactionManager,如果事务管理器 id 与其一致,则可以不用指定。 <tx:method>元素包含多个属性参数,可以为某个或某些方法(name 属性指定的方法)定义事务属性,如下表所示:

事务属性 说明
propagation 指定事务的传播行为,默认为 REQUIRED
isolation 指定事务的隔离级别,默认为所使用数据库的隔离级别
read-only 指定是否为只读事务,默认为 false
timeout 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时
rollback-for 指定出现哪些异常进行事务回滚
no-rollback-for 指定出现哪些异常不进行事务回滚

配置切入点和切面

<aop:config>
    <!--配置切入点-->
    <aop:pointcut id="pt" expression="execution(*
com.cxyxj.aopxml.service.TXServiceImpl.*(..))"/>
    <!--配置切面-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>

如上写法就对 transfer 方法进行了事务管理。就不会出现小明减少余额,而小红没有增加余额的情况,发生了异常就进行回滚。

注解方式

使用注解方式就不会有上面如此琐碎的配置了。再重新创建名为 txannon包,将 xml 方式使用到的 entity、dao、service 相关代码 copy 过来。

开启事务

使用 EnableTransactionManagement注解开启事务。

@ComponentScan(basePackages = "com.cxyxj.txannon")
@EnableTransactionManagement //开启事务
public class AppMain {

}

相当于 tx:annotation-driven 标签。

创建配置类

@Configuration
@PropertySource("jdbc.properties")
public class TxConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Value("${jdbc.driver}")
    private String driverClassName;

    //创建数据库连接池
    @Bean
    public DriverManagerDataSource getDruidDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    //创建 JdbcTemplate 对象
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        //到 ioc 容器中根据类型找到 dataSource
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        //注入 dataSource
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    //创建事务管理器
    @Bean
    public DataSourceTransactionManager
    getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new
                DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }

}

可以不需要在配置切入点和切面了。

添加事务注解

在需要添加事务的方法上添加 @Transactional注解,表明该方法需要进行事务管理。

@Service
public class TXServiceImpl {

    @Autowired
    private TXDao tx;

    @Transactional
    public void transfer(){
        tx.add();
        int i = 1/0;
        tx.reduce();
    }
}

@Transactional这个注解可以添加到类上面,也可以添加方法上面。如果把这个注解添加到类上面,这个类里面所有的方法都添加事务,如果把这个注解添加方法上面,则是为这个方法添加事务。

@Transactional

Transactional 这个注解里面可以配置很多事务相关参数。

public @interface Transactional {

    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

事务属性 说明
value 指定不同的事务管理器。
transactionManager 跟 value 一致。
propagation 指定事务的传播行为,默认为 REQUIRED
isolation 指定事务的隔离级别,默认为所使用数据库的隔离级别
read-only 指定是否为只读事务,默认为 false
timeout 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时
rollbackFor 指定出现哪些异常进行事务回滚
rollbackForClassName 指定异常类名称,进行事务回滚
noRollbackFor 指定出现哪些异常不进行事务回滚
noRollbackForClassName 指定出现哪些异常类名称不进行事务回滚

基本用法会了,现在就来看看事务的传播行为,这是Spring事务中难以理解的一块,因为它的场景很多。

事务传播行为详解

REQUIRED

如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行

REQUIRES_NEW

当前方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起。

reduce 方法行为修改为 Propagation.REQUIRES_NEW。transfer 方法创建新事务,然后调用 reduce 方法,reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。所以在该例子中会创建两个事务。由于有两个事务,那事务的回滚就出现了几种情况。

 transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。
 两个方法的操作都会回滚。这是由于 reduce 方法的异常会向 transfer 方法传递。

transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。

NESTED

如果当前存在事务(主事务),则创建一个新事务作为当前事务的嵌套事务(子事务)来运行;如果当前没有事务,则该取值等价于 REQUIRED。

transfer 方法发生异常并回滚,会导致 reduce 方法 同时回滚。

transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。注意:transfer 方法需要进行 catch,不然 transfer 方法也会回滚。

主事务方法异常回滚时,会同时回滚子事务。而子事务可以单独异常回滚,可以不影响主事务和其他子事务(前提是需要处理掉子事务的异常)

MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

由于 transfer 方法没有事务,在启动时就会抛出异常,如下:

No existing transaction found for transaction marked with propagation 'mandatory'

SUPPORTS

如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。

由于 transfer 方法没有事务,所以 reduce 方法也不会创建事务,发生了异常也不会进行回滚。

NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

transfer 方法有事务,但 reduce 方法传播行为是 NOT_SUPPORTED,所以会将 transfer 方法事务挂起,reduce 方法以非事务的方式运行。

所以图片例子会出现 transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。

NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

由于 transfer 方法有事务,在启动时就会抛出异常,如下:

Existing transaction found for transaction marked with propagation 'never'

回滚规则

上面一直在说遇到异常就回滚,那是遇到所有异常都会回滚吗?不是的,默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException。

那如果想在发生检查型异常时也进行回滚呢,可以使用 rollbackFor 属性进行如下配置:

那同理,如果遇到某个异常,不想进行回滚,使用 noRollbackFor 属性配置如下:

上一篇 下一篇

猜你喜欢

热点阅读