7、Spring事务配置下篇

一、事务细节-七种传播行为

1、概述

1、事务的传播行为是Spring的特性,它指的是多个事务方法之间相互调用时,事务如何在这些方法之间的传播。比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的事务提交或回滚,还是内层的事务合并到外层的事务一起提交或回滚,这就是事务的传播行为来确定的。
2、MySQL数据库本身不支持嵌套事务,因此Spring的“嵌套事务”是通过MySQL的Savepoint保存点来实现的
3、如果使用声明式事务管理,那么Spring采用的是AOP来支持事务管理,在方法前会根据配置的事务属性来决定是否需要通过AOP拦截器来开启一个事务。因此,传播行为主要是针对声明式事务管理的扩展
4、Spring提供了七种事务的传播行为(当前事务是指调用者自带的事务,A调用B,那么A的事务就是当前事务)
  • PROPAGATION_REQUIRED:Spring默认的传播行为,如果当前存在事务(即A方法存在事务),则加入到当前事务,使用当前外层事务的属性;如果当前不存在事务,就创建一个新事务运行。
  • PROPAGATION_SUPPORTS:如果当前存在事务,则加入到当前事务中去;如果当前不存在事务,则直接以非事务的方式执行。
  • PROPAGATION_MANDATORY:如果当前存在事务,则加入到当前事务中去;如果当前不存在事务,则直接抛出异常new IllegalTransactionStateException(“No existing transaction found for transaction marked with propagation ‘mandatory’”)
  • PROPAGATION_REQUIRES_NEW:新建一个事务独立运行,如果当前存在事务就将当前事务挂起,内层事务结束时,外层事务将继续执行;内层事务可以独立提交或回滚,外层事务不受内层事务的回滚状态的影响
  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务就将当前事务挂起,执行完当前代码后,才恢复外层事务。
  • PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务就抛出异常new IllegalTransactionStateException(“Existing transaction found for transaction marked with propagation ‘never’”)
  • PROPAGATION_NESTED:新建一个事务,如果当前存在事务就嵌套在当前事务内执行;如果当前不存在事务,则等价于PROPAGATION_REQUIRED会新建一个事务运行。

2、PROPAGATION_REQUIRED

1、Spring默认的传播行为,TransactionDefinition.PROPAGATION_REQUIRED=0;如果当前存在事务,则加入到当前事务,使用当前外层事务的属性;如果当前不存在事务,就创建一个新事务运行。
2、假设有一个事务方法A,它的传播行为是PROPAGATION_REQUIRED,如果它被调用时不存在事务,那么它将开启一个新事务;如果在A中调用了事务方法B,且B方法的传播行为是PROPAGATION_REQUIRED,那么B将加入到A的事务中去。

在这里插入图片描述

3、对加入的解释:表示使用同一个物理事务,内部的事务(即B方法的事务)将会忽略自己设置的隔离级别、超时时间、只读标识这几个属性,这些属性统一使用最外层事务方法设置的值(即A方法的事务属性)。
4、注意点:
  • rollbackFor回滚异常类型,则针对单个方法可以单独设置,最终该事务是否回滚是通过判断所有外层和内层的事务方法是否都回滚或者不回滚来设置的,如果有任何一个方法指定回滚,则所有方法和操作都会回滚。
  • 如果仅仅是在内层加入的方法中通过setRollbackOnly方法设置为仅回滚(不是在最外层事务方法中设置的),并且事务回滚时如果没有其他异常抛出,则会抛出一个异常:“UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only”,来提醒开发者所有的外部和内部操作都已被回滚!这一般对于内层PROPAGATION_SUPPORTS、PROPAGATION_REQUIRED、PROPAGATION_MANDATORY生效,对于内层PROPAGATION_NOT_SUPPORTED则无效。
// 方法A
@Transactional(propagation = Propagation.REQUIRED)
public void A(){
    // do something
    B();
    // do something
}

// 方法B
@Transactional(propagation = Propagation.REQUIRED)
public void B(){
    // do something
}

3、PROPAGATION_SUPPORTS

1、TransactionDefinition.PROPAGATION_SUPPORTS=1;如果当前存在事务,则加入到当前事务中去;如果当前不存在事务,则直接以非事务的方式执行。
2、假设有一个事务方法B,它的传播行为是PROPAGATION_SUPPORTS,如果它被调用时不存在事务,那么它将以非事务的方式运行;如果B被调用时已经在调用方法中开启了事务,那么B将加入到外层的事务中。

在这里插入图片描述

4、PROPAGATION_MANDATORY

1、TransactionDefinition.PROPAGATION_SUPPORTS=2;如果当前存在事务,则加入到该事务中去;如果当前不存在事务,则直接抛出异常new IllegalTransactionStateException(“No existing transaction found for transaction marked with propagation ‘mandatory’”)
2、假设有一个事务方法B,它的传播行为是PROPAGATION_MANDATORY,如果它被调用时不存在事务,那么它将直接抛出异常;如果B被调用时已经在调用方法中开启了事务,那么B将加入到外层的事务中。

在这里插入图片描述

5、PROPAGATION_REQUIRES_NEW

1、TransactionDefinition.PROPAGATION_REQUIRES_NEW=3;新建一个事务独立运行,如果当前存在事务就将当前事务挂起,内层事务结束时,外层事务将继续执行;内层事务可以独立提交或回滚,外层事务不受内层事务的回滚状态的影响。

在这里插入图片描述

6、PROPAGATION_NOT_SUPPORTED

1、TransactionDefinition.PROPAGATION_NOT_SUPPORTED=4;以非事务方式执行,如果当前存在事务就将当前事务挂起,执行完当前代码后,则恢复外层事务。

在这里插入图片描述

7、PROPAGATION_NEVER

1、TransactionDefinition.PROPAGATION_NEVER=5;以非事务方式执行,如果当前存在事务就抛出异常new IllegalTransactionStateException(“Existing transaction found for transaction marked with propagation ‘never’”)。与PROPAGATION_MANDATORY正好相反。

在这里插入图片描述

8、PROPAGATION_NESTED

1、TransactionDefinition.PROPAGATION_NESTED=6;如果当前存在事务,则新建一个事务作为当前事务的嵌套事务来运行;如果当前不存在事务,则等价于PROPAGATION_REQUIRED会新建一个事务运行。
2、对嵌套事务的解释:
  • 嵌套事务实际上是通过保存点(SavePoint)来实现的,这个保存点属于当前已存在的外层事务,所以说仍然只有一个物理事务,这就是真正的嵌套事务的实现
  • 基于保存点的特性,内层事务依赖外层事务(实际上是同一个事务)。内层事务操作失败时只是自身回到保存点的位置,不会引起外层事务的回滚;而外层事务因失败回滚时,内层事务也会跟着一起回滚;在提交时,在外层事务提交之后,内层事务才能提交,只需要提交外层事务即可
  • 由于实际上只有一个物理事务,那么内层事务会继承外层事务的隔离界别和超时时间、只读标记等属性
  • 要想使用嵌套事务还需要把AbstractPlatformTransactionManager的nestedTransactionAllowed属性设为true(默认为false),但是DataSourceTransactionManager的构造方法中会将nestedTransactionAllowed设置为true

在这里插入图片描述

在这里插入图片描述

二、事务细节-隔离级别

1、概述

1、数据库允许多个事务的并行,事务的隔离级别就用来表示并行事务之间的隔离的程度。例如某个事务能够看到来自其他事务的未提交的写入。
2、事务的隔离级别是数据库事务自身的特性,Spring和JDBC中的用于设置隔离级别的常量仅仅是为了与数据库的隔离级别对应,也就是说隔离级别最终还是依赖数据库来实现的。
3、Spring事务隔离级别比数据库事务隔离级别多一个default。

2、并发(并行)事务处理带来的问题

1、并发和并行是十分容易混淆的概念;并发指的是多个任务交替执行,而并行则是指真正意义上的“同时进行”。
2、丢失更新(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题,最后的更新覆盖了由其他事务所做的更新。
3、脏读(Dirty Reads):事务A读取到了事务B已修改但尚未提交的的数据,但是事务B由于某种原因取消对数据的修改(即事务回滚),此时事务A读取的数据与数据库中数据不一致,不符合一致性要求。
4、不可重复读(Non-Repeatable Reads):事务A读取数据库中的数据后,事务B进行修改或删除并提交了符合事务A查询条件的数据,当事务A再次执行同一查询时,就会发现数据前后不一致,这就是不可重复读;即不可重复读发生在一个事务执行两次或两次以上的相同查询时,查询结果不一致。
5、幻读(Phantom Reads):事务A读取数据库中的数据后,事务B进行插入并提交了符合事务A查询条件的数据,当事务A再次执行同一查询时,就会发现多出来了数据,这就是幻读;即幻读发生在一个事务执行两次或两次以上的相同查询时,查询结果不一致。

3、数据库中的四种事务隔离级别

1、在SQL92标准中,针对大部分数据库定义了4个标准的事务隔离级别,用于解决上述并发事务所带来的问题;事务的隔离级别设置越高,问题就会出现的越少,但并发效果就越低;相反的级别越低,问题越多,并发效果越好。
2、读未提交(Read Uncommitted):
  • 最低的隔离级别,一个事务可以读取到另一个事务未提交的数据。
  • 一个事务在执行过程中,可以访问其他事务未提交的新插入或修改的数据;如果一个事务已经开始写数据,则另一个事务则不允许同时进行写操作,但允许其他事务读取此行数据。此隔离级别可防止丢失更新,但是存在脏读、不可重复读、幻读的问题
  • 写事务阻止其他写事务,避免了丢失更新,但是没有阻止其他读事务;读事务则不会阻止其他任何事务
3、读已提交(Read Committed):
  • 一个事务修改的数据提交后才能被另一个事务读取到,另一个事务不能读取该事务未提交的数据;Sql Server,Oracle默认的隔离级别。
  • 解决了脏读问题,但是可能会出现不可重复读和幻读的问题
  • 写事务会阻止其他读写事务;读事务不会阻止其他任何事务
4、可重复读(Repeatable Read):
  • 读取数据事务开启时,不再允许修改操作;MySQL的默认事务隔离级别。
  • 写事务会阻止其他读写事务;读事务会阻止其他写事务,但不会阻止其他读事务;可重复读阻止的写事务只包括update、delete操作,不包括insert操作,因此可能出现幻读,但这不是绝对的
  • 实际上,MySQL在默认的可重复读隔离级别下,通过MVCC解决了幻读问题
5、序列化/串行化(Serializable):
  • 最高的事务隔离级别。它通过强制事务串行化顺序执行,避免了脏读、幻读、不可重复读问题
  • 它会在读取的每一行数据上都加锁,所以可能导致大量的超时和竞争锁问题,严重影响程序性能,实际应用中很少使用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑使用
6、隔离级别与是否会发生脏读、不可重复读、幻读现象对应关系表:
隔离级别脏读不可重复度幻读
读未提交(Read Uncommitted)
读已提交(Read Committed)
可重复读(Repeatable Read)
可串行化(Serializable)
7、MySQL默认隔离级别是Repeatable Read,并且支持全部四种级别;Oracle默认的隔离级别是Read Committed,并且支持上述四种隔离级别中的两种:Read Committed和Serializable。

4、Spring中的事务隔离级别

1、ISOLATION_DEFAULT:值为-1,是PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。
2、ISOLATION_READ_UNCOMMITTED:值为1,对应Read Uncommitted(读未提交),最低的事务隔离级别。
3、ISOLATION_READ_COMMITTED:值为2,对应Read Committed(读已提交)。
4、ISOLATION_REPEATABLE_READ:值为4,对应Repeatable Read(可重复读)。
5、ISOLATION_SERIALIZABLE:值为8,对应Serializable(可串行化),最高的事务隔离级别。

5、验证事务隔离级别

1、事务的隔离级别是在一个事务启动的时候设置的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明的事务隔离级别参数才有意义
2、通过测试发现test01方法能够获取到返回值,test02方法报错且数据回滚。

在这里插入图片描述

在这里插入图片描述

/**
 * @Date: 2023/2/12
 * 事务隔离级别相关操作
 */
@Service
public class IsolationServiceImpl {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 由于@Transactional采用的是代理模式,采用this调用不会生效,所以这里通过自引用的方式来调用。
    @Autowired
    private IsolationServiceImpl isolationService;

    /**
     * 由于调用的方法是读未提交的,所以会返回数据
     * @return
     */
    @Transactional
    public User readUncommitIsolation(User user) {
        // 插入的sql
        String sql = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        jdbcTemplate.update(sql, user.getName(), user.getAge());
        // 调用查询获取数据
        User readUncommit = isolationService.getReadUncommit(user.getName());
        System.out.println("查询事务返回的数据:" + readUncommit);
        return readUncommit;
    }

    /**
     * 隔离级别设置为读未提交,调用这个方法的会读取到未提交的事务
     * 传播行为设置为REQUIRES_NEW,重新开启一个事务,此时调用它的方法的事务会被挂起,即事务未提交
     * @param name
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
    public User getReadUncommit(String name) {
        // 查询sql
        String sql = "select * from users where name = ?";
        // 执行查询
        User user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(User.class), name);
        return user;
    }

    /**
     * 默认隔离级别的方法,即可重复读,因此此处会返回null
     * @return
     */
    @Transactional
    public User defaultIsolation(User user) {
        // 插入的sql
        String sql = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        jdbcTemplate.update(sql, user.getName(), user.getAge());
        // 调用查询获取数据
        User readUncommit = getDefault(user.getName());
        System.out.println("查询事务返回的数据:" + readUncommit);
        return readUncommit;
    }

    /**
     * 未配置隔离级别,会使用默认隔离级别,即数据库默认的事务隔离级别,
     * mysql的默认的隔离级别是可重复读,所以调用这个方法不会返回数据,
     * 会报错
     * @param name
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public User getDefault(String name) {
        // 查询sql
        String sql = "select * from users where name = ?";
        // 执行查询
        User user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(User.class), name);
        return user;
    }
}
/**
 * @Date: 2023/2/12
 * 测试事务隔离级别
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DataSourceAnnoConfig.class)
public class Test6 {

    @Autowired
    private IsolationServiceImpl isolationService;

    @Test
    public void test01() {
        // 调用隔离级别为读未提交的方法
        User user = isolationService.readUncommitIsolation(new User("喜羊羊", 29));
    }

    @Test
    public void test02() {
        // 调用隔离级别为默认的方法
        User user = isolationService.defaultIsolation(new User("沸羊羊", 29));
    }
}

三、超时时间与只读状态及回滚规则

1、超时时间

1、表示允许一个事务执行的最长时间,时间单位为秒;默认值为-1,表示没有超时时间;如果超过该时间限制但事务还没有执行完,则自动回滚事务。
2、事务的超时时间是在一个事务启动的时候开始计算的,因此只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明的事务超时时间参数才有意义。
/**
 * @Date: 2023/2/12
 * 超时时间、只读状态、回滚规则测试
 */
@Service
public class TimeOutAndReadOnlyAndRollbackServiceImpl {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 默认的事务传播行为,超时时间设置为1s
     * @param user
     * @return
     */
    @Transactional(timeout = 1)
    public int timeOut(User user) {
        // 获取当前事务
        DefaultTransactionStatus transactionStatus = (DefaultTransactionStatus) TransactionAspectSupport.currentTransactionStatus();
        // 设置休眠时间5s,目的让事务超时
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 插入的sql
        String insertSQL = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        int update = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
        // 设置休眠时间5s,目的让事务超时
        // try {
        //     Thread.sleep(5000);
        // } catch (InterruptedException e) {
        //     e.printStackTrace();
        // }
        // // 查询sql
        // String querySQL = "select * from users where name = ?";
        // // 执行查询
        // User user1 = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
        // System.out.println(user1);
        return update;
    }
}
/**
 * @Date: 2023/2/12
 * 超时时间、只读状态、回滚规则测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DataSourceAnnoConfig.class)
public class Test7 {

    @Autowired
    private TimeOutAndReadOnlyAndRollbackServiceImpl otherService;

    @Test
    public void test01() {
        // 调用隔离级别为读未提交的方法
        int i = otherService.timeOut(new User("慢羊羊", 29));
    }
}
1、插入操作之前休眠5秒执行结果如下:

在这里插入图片描述

2、插入操作之后休眠5秒执行结果如下:

在这里插入图片描述

3、通过上面两种情况可以得出这样的结论
  • 如果将休眠时间放到插入操作后面,超时之后并没有报错,也就没有事务回滚这一说法,同时数据库中会插入一条记录,说明超时时间指的是数据库执行最多能用的时间,不是Java程序执行的时间
  • Spring事务超时时间 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行完的时间。这也说明了为什么休眠代码放到插入操作代码前面时候,会超时且回滚了

2、只读状态

1、当代码只有读取操作没有修改操作时,可以设置readOnly=true,默认为false,表示只读事务。
2、事务的只读状态是在一个事务启动的时候设置的,因此只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明的事务只读状态参数才有意义。
3、提醒:在oracle下测试,发现不支持readOnly属性,也就是不论Connection里的readOnly属性是true还是false均不影响SQL的增删改查
4、改为使用Oracle配置,只读事务中进行插入操作,数据库中成功插入一条数据,只读事务没有生效

在这里插入图片描述

5、改为使用MySQL配置,只读事务中进行插入操作会报错,只读事务生效

在这里插入图片描述

<!--oracle数据库驱动,注意这个依赖是无法从maven中直接获取的,我是下载好放到本地maven中的-->
<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.4</version>
</dependency>
# oracle连接信息配置
url=jdbc:oracle:thin:@//192.168.1.7:1521/orcl
className=oracle.jdbc.driver.OracleDriver
userName=kolonfair2
password=tgbyhn624
/**
 * @Date: 2023/2/12
 * Oracle数据库访问配置文件
 */
@Configuration
@ComponentScan("com.itan.transactional.anno.*")
@PropertySource(value = "classpath:oracle.properties", encoding = "UTF-8")
@EnableTransactionManagement
public class OracleDataSourceConfig {
    @Value("${url}")
    private String url;

    @Value("${className}")
    private String className;

    @Value("${userName}")
    private String userName;

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

    /**
     * 配置Druid数据源
     */
    @Bean
    public DruidDataSource druidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setDriverClassName(className);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        return dataSource;
    }

    /**
     * 配置JdbcTemplate
     */
    @Bean
    public JdbcTemplate jdbcTemplate() {
        // 传入一个数据源
        return new JdbcTemplate(druidDataSource());
    }

    /**
     * 配置DataSourceTransactionManager,用于管理某一个数据库的事务
     */
    @Bean
    public DataSourceTransactionManager transactionManager() {
        // 传入一个数据源
        return new DataSourceTransactionManager(druidDataSource());
    }
}
/**
 * Oracle设置只读事务,数据库中成功新增一条数据,说明oracle设置readOnly属性无效
 * @param user
 * @return
 */
@Transactional(readOnly = true)
public int oracleReadOnly(User user) {
    // 插入的sql
    String insertSQL = "insert into user_test (id, name, age) values (1, ?, ?)";
    // 调用jdbcTemplate的update方法,插入数据
    int update = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
    // 查询sql
    String querySQL = "select * from users where name = ?";
    // 执行查询
    User user1 = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
    System.out.println(user1);
    return update;
}
/**
 * @Date: 2023/2/12
 * oracle数据库的只读状态测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = OracleDataSourceConfig.class)
public class Test8 {

    @Autowired
    private TimeOutAndReadOnlyAndRollbackServiceImpl otherService;
    
    @Test
    public void test01() {
        // 调用只读事务的方法
        int i = otherService.oracleReadOnly(new User("慢羊羊", 29));
    }
}

3、回滚规则

1、默认情况下,Spring事务执行时如果抛出RuntimeException(运行时异常,非受检异常)和Error及其它们的子类异常时,才会回滚。
2、如果抛出其他类型的异常,比如受检异常(除了RuntimeException和Error之外的异常)时不会回滚,因为受检异常可能是作为一个业务异常而代替返回值的结果,因此,如果遇到了受检异常,仍然会提交事务。
3、可以声明在出现特定受检查异常时像运行时异常一样回滚,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。回滚规则可以通过@Transactional注解的rollbackFor属性和noRollbackFor属性,以及<tx:method>标签的rollback-for和no-rollback-for属性来设置
4、注意:
  • 如果回滚和不回滚属性设置了相同的异常,那么在抛出该异常时将会回滚
  • 如果抛出的异常没有匹配设定的异常,那么会采用默认规则,即异常属于RuntimeException或Error级别的异常时,才会回滚
/**
 * 使用默认的回滚规则,抛出受检异常,事务不会回滚
 * @param user
 * @return
 */
@Transactional
public int checkedException(User user) throws IOException {
    // 插入的sql
    String insertSQL = "insert into users (name, age) values (?,?)";
    // 调用jdbcTemplate的update方法,插入数据
    int update = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
    // 查询sql
    String querySQL = "select * from users where name = ?";
    // 执行查询
    User user1 = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
    System.out.println(user1);
    // 手动抛出一个受检异常
    throw new IOException();
}

/**
 * 使用默认的回滚规则,抛出非受检异常,事务会回滚
 * @param user
 * @return
 */
@Transactional
public int uncheckedException(User user) {
    // 插入的sql
    String insertSQL = "insert into users (name, age) values (?,?)";
    // 调用jdbcTemplate的update方法,插入数据
    int update = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
    // 查询sql
    String querySQL = "select * from users where name = ?";
    // 执行查询
    User user1 = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
    System.out.println(user1);
    // 制造非受检异常:java.lang.ArithmeticException
    int i = 1 / 0;
    return update;
}

/**
 * 设置回滚规则为受检异常java.io.IOException,事务回滚
 * @param user
 * @return
 */
@Transactional(rollbackFor = IOException.class)
public int checkedExceptionRollback(User user) throws IOException {
    // 插入的sql
    String insertSQL = "insert into users (name, age) values (?,?)";
    // 调用jdbcTemplate的update方法,插入数据
    int update = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
    // 查询sql
    String querySQL = "select * from users where name = ?";
    // 执行查询
    User user1 = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
    System.out.println(user1);
    // 手动抛出一个受检异常
    throw new IOException();
}

/**
 * 设置回滚规则和不回滚规则都为java.lang.ArithmeticException,事务回滚
 * @param user
 * @return
 */
@Transactional(rollbackFor = ArithmeticException.class, noRollbackFor = ArithmeticException.class)
public int rollbackAndNoRollbackExceptionSame(User user) {
    // 插入的sql
    String insertSQL = "insert into users (name, age) values (?,?)";
    // 调用jdbcTemplate的update方法,插入数据
    int update = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
    // 查询sql
    String querySQL = "select * from users where name = ?";
    // 执行查询
    User user1 = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
    System.out.println(user1);
    // 制造非受检异常:java.lang.ArithmeticException
    int i = 1 / 0;
    return update;
}
上述四个方法运行结果依次如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

四、事务失效场景分析

1、同一类中方法间调用

1、在同一个被Spring管理的类中,如果方法之间互相调用,那么被调用的方法的事务不会生效;这是因为方法之间相互调用的时候,本质上是类对象自身(this)的调用,而不是使用代理对象去调用,也就不会触发AOP,Spring也就无法将事务控制的代码逻辑织入到被调用方法代码中,事务也就失效了。
2、实际上所有基于AOP原理的配置(如普通的AOP方法的配置、@Async异步任务方法的配置)都不会生效,这是底层AOP技术的局限性导致的,要想AOP配置生效,只有通过代理对象去调用对应的方法才行。
3、三种解决方法:
  1. 当前类自己注入自己,实际上注入的是一个代理对象,然后就可以使用这个注入的代理对象去调用内层方法了
  2. 对于XML配置方式,设置<aop:config>或者<aop:aspectj-autoproxy>标签的expose-proxy属性为true;对于注解配置方式,则设置@EnableAspectJAutoProxy注解的exposeProxy属性为true,其目的就是将代理对象暴露出来,然后代码中使用AopContext.currentProxy()即可获取代理对象,然后强制转型去调用内层方法即可(注意类型兼容性)
  3. 将方法放到不同的类中,跨类调用
public interface TransactionalLapseService {
    User getUser(User user);

    int insertUser(User user);

    int updateUser(User user);
}
/**
 * @Date: 2023/2/13
 * 同一类中方法间调用事务失效示例
 */
@Service
public class TransactionalLapseServiceImpl implements TransactionalLapseService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public User getUser(User user) {
        // 查询sql
        String querySQL = "select * from users where name = ?";
        // 修改前查询
        User one = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), user.getName());
        // 调用带有事务的修改方法
        one.setAge(user.getAge());
        updateUser(one);
        return one;
    }

    @Override
    @Transactional
    public int updateUser(User user) {
        // 修改的sql
        String updateSQL = "update users set age = ? where name = ?";
        // 调用jdbcTemplate的update方法,修改数据
        int update = jdbcTemplate.update(updateSQL, user.getAge(), user.getName());
        // 抛出一个RuntimeException
        throw new RuntimeException();
    }
}
/**
 * @Date: 2023/2/13
 * 事务失效测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DataSourceAnnoConfig.class)
public class Test9 {

    @Autowired
    private TransactionalLapseService transactionalLapseService;

    @Test
    public void test01() {
        // 同一类方法相互调用,事务失效
        transactionalLapseService.getUser(new User("同一类方法相互调用", 100));
    }
}

2、private、final、static修饰的方法

1、Spring事务底层使用了AOP,也就是通过JDK动态代理或者CGLIB动态代理,来生成代理类,在代理类中实现的事务功能;如果方法被private/final/static等关键字修饰了,那么在它的代理类中,就无法重写被这些关键字修饰的方法,Spring也就无法将事务控制的代码逻辑织入方法代码中,事务也就失效了。
2、方法重写规则:
  • 声明为final的方法不能被重写。
  • 声明为static的方法不能被重写,但是能够再次声明。
  • 构造方法不能被重写。
  • 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为private和final的方法。
  • 子类和父类不在同一个包中时,子类只能重写父类的声明为public和protected的非final方法。
3、注意:如果将事务注解的方法使用private/final/static修饰,在修改的时候就会提醒:Methods annotated with '@Transactional' must be overridable

3、事务方法未被Spring管理

1、如果事务方法所在的类没有注册到Spring IOC容器中,也就是说,事务方法所在类并没有被Spring管理,事务也就失效了。
2、通常情况下,通过@Controller、@Service、@Component、@Repository等注解,可以自动实现Bean实例化和依赖注入的功能。

4、方法的事务传播行为不支持事务

1、只有事务传播行为设置为REQUIRED,REQUIRES_NEW,NESTED这三种的方法才会创建新事务;如果传播行为设置成这三种之外的,那么该方法也就不会被事务管理了,事务也就失效了。

5、数据库不支持事务

1、Spring事务生效的前提是连接的数据库支持事务,如果底层的数据库都不支持事务,则Spring事务肯定会失效的。
2、如果使用MySQL数据库,选用MyISAM存储引擎,因为MyISAM存储引擎本身不支持事务,因此事务毫无疑问会失效。

6、异常被内部catch,程序生吞异常或抛出异常类型不正确

1、如果在代码中try…catch了异常,并且没有手动抛出,换句话说就是把异常吞掉了,事务不会回滚。
2、如果抛出的异常类型不是所设置的异常类型或者子类类型,事务不会回滚。
3、如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则Spring认为程序是正常的;默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚
/**
 * 异常被内部catch,事务失效示例
 * @param user
 * @return
 */
@Override
@Transactional
public int insertUser(User user) {
    int insert = 0;
    try {
        // 插入的sql
        String insertSQL = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        insert = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
        System.out.println("插入操作受影响行数:" + insert);
        // 制造非受检异常:java.lang.ArithmeticException
        int i = 1 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return insert;
}
/**
 * 抛出异常类型不正确,事务失效示例
 * @param user
 * @return
 */
@Override
@Transactional
public int throwExceptionTypeIsIncorrect(User user) throws Exception {
    int insert = 0;
    try {
        // 插入的sql
        String insertSQL = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        insert = jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
        System.out.println("插入操作受影响行数:" + insert);
        // 制造非受检异常:java.lang.ArithmeticException
        int i = 1 / 0;
    } catch (Exception e) {
        // 重新抛出其他类型的异常
        throw new Exception(e.getMessage());
    }
    return insert;
}

7、多线程调用

1、如果在事务方法中,通过多线程的方式调用另一个事务方法时,被调用的方法出现异常时,调用的方法不会回滚。
2、这是因为两个方法不在同一个线程中,Spring实现事务的原理是通过ThreadLocal把数据库连接绑定到当前线程中,因此获取到的数据库连接不一样,不是同一个事务;同一事务是指同一个数据库连接,只有使用相同的数据库连接才能同时提交和回滚事务
3、解决方法:使用Spring提供的编程式事务方式。
/**
 * @Date: 2023/2/13
 * 多线程事务不生效示例
 */
@Service
public class ThreadTransactional {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private ThreadTransactional threadTransactional;

    /**
     * 线程调用其他事务方法,被调用方法异常,调用方法是不会回滚的,仍然修改了数据
     * @param user
     * @return
     */
    @Transactional
    public int threadTransactionalMethod(User user) {
        // 修改的sql
        String updateSQL = "update users set age = ? where name = ?";
        // 调用jdbcTemplate的update方法,修改数据
        int update = jdbcTemplate.update(updateSQL, user.getAge(), user.getName());
        // 线程调用其他方法
        user.setName("线程调用");
        user.setAge(30);
        new Thread(() -> threadTransactional.doOtherThing(user)).start();
        return 0;
    }

    @Transactional
    public void doOtherThing(User user) {
        // 插入的sql
        String insertSQL = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
        // 制造非受检异常:java.lang.ArithmeticException
        int i = 1 / 0;
    }
}
@Test
public void test04() {
    // 线程调用其他事务方法,被调用方法异常,调用方法是不会回滚的
    threadTransactional.threadTransactionalMethod(new User("抛出异常类型不正确", 100));
}

8、嵌套事务多回滚了

1、一个事务方法调用一个传播行为是Propagation.NESTED(嵌套事务)的方法时,当被调用的嵌套事务方法中发生异常,且没有手动捕获异常,异常会继续往上抛出,到外层的调用方法中,这种情况是直接回滚了整个事务,不是回滚单个保存点
2、解决方法:将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务
/**
 * @Date: 2023/2/13
 * 嵌套事务不生效示例
 */
@Service
public class NestedTransactional {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private NestedTransactional nestedTransactional;

    /**
     * 调用嵌套事务方法,嵌套事务发生异常,调用方法和嵌套方法都回滚
     * @param user
     * @return
     */
    @Transactional
    public int nestedTransactionalMethod(User user) {
        // 修改的sql
        String updateSQL = "update users set age = ? where name = ?";
        // 调用jdbcTemplate的update方法,修改数据
        int update = jdbcTemplate.update(updateSQL, user.getAge(), user.getName());
        user.setName("线程调用");
        user.setAge(30);
        // 调用传播行为是Propagation.NESTED的方法
        nestedTransactional.doOtherThing(user);
        return 0;
    }

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing(User user) {
        // 插入的sql
        String insertSQL = "insert into users (name, age) values (?,?)";
        // 调用jdbcTemplate的update方法,插入数据
        jdbcTemplate.update(insertSQL, user.getName(), user.getAge());
        // 制造非受检异常:java.lang.ArithmeticException
        int i = 1 / 0;
    }
}

五、事务与锁冲突

1、复现问题

1、问题现象:在高并发情况下,且事务隔离级别设置为可重复读(REPEATABLE_READ)时,如果Spring事务中包含锁的情况(使用Java锁或Redis锁),会造成读取的数据不是最新的情况,如果后续的逻辑要依赖读取到的数据,可能会造成最后结果与预期的不一致
2、示例代码执行结果中的qty并不等于20,说明事务与锁冲突了。
/**
 * @Date: 2023/2/13
 * 锁和事务一起使用失效问题
 */
@Slf4j
@Service
public class LockAndTransactional {
    // 可重入公平锁
    private ReentrantLock lock = new ReentrantLock(true);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void addQty(String name) {
        // 加锁
        lock.lock();
        try {
            log.info("线程:{},获取锁成功", Thread.currentThread().getName());
            // 查询sql
            String querySQL = "select * from users where name = ?";
            // 执行查询
            User user = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), name);
            // 获取库存
            Integer qty = user.getQty();
            log.info("线程:{},查询的原始订量为:{}", Thread.currentThread().getName(), qty);
            // 每次加1
            qty = qty + 1;
            // 修改的sql
            String updateSQL = "update users set qty = ? where id = ?";
            // 调用jdbcTemplate的update方法,修改数据
            int update = jdbcTemplate.update(updateSQL, qty, user.getId());
            log.info("线程:{},修改订量完成,修改后的订量为:{}", Thread.currentThread().getName(), qty);
        } finally {
            // 释放锁
            lock.unlock();
            log.info("线程:{},释放锁成功", Thread.currentThread().getName());
        }
    }

    /**
     * 获取qty
     * @param name
     * @return
     */
    public User getQty(String name) {
        // 查询sql
        String querySQL = "select id, name, age, qty from users where name = ?";
        // 执行查询
        User user = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), name);
        return user;
    }
}
/**
 * @Date: 2023/2/13
 * 锁和事务一起使用失效问题测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DataSourceAnnoConfig.class)
public class Test10 {
    @Autowired
    private LockAndTransactional lockAndTransactional;

    @Test
    public void test1() throws InterruptedException {
        User user = lockAndTransactional.getQty("张三");
        System.out.println("递增之前的结果:" + user);
        // 递增20次,如果生成最后查询的结果应该为20
        CountDownLatch countDownLatch = new CountDownLatch(20);
        for (int i = 0; i < 20; i++) {
            final int k = i;
            new Thread(() -> {
                lockAndTransactional.addQty("张三");
                // 计数减1
                countDownLatch.countDown();
            }, "线程" + (k + 1)).start();
        }
        // 等待所有线程执行完成。主线程才继续向下执行
        countDownLatch.await();
        user = lockAndTransactional.getQty("张三");
        System.out.println("递增之后的结果:" + user);
    }
}
/**
 * 运行结果如下:递增之后的qty并不等于20
 * 递增之前的结果:User(id=18, name=张三, age=50, qty=0, createTime=null, updateTime=null)
 * 递增之后的结果:User(id=18, name=张三, age=50, qty=16, createTime=null, updateTime=null)
 */

2、事务与锁的先后顺序

1、首先先看上述代码执行的日志:通过日志可以看到释放锁的顺序是在事务提交之前,但是无法知道获取锁是在事务开始之前还是之后

在这里插入图片描述

2、通过select * from information_schema.innodb_trx;语句查询当前数据库有哪些事务正在执行的语句,再配合DEBUG模式就能知道开始事务与加锁的先后顺序。
3、调式步骤:
  1. 首先进入到lock.lock()处,查询数据库发现没有事务记录,说明加锁在事务开启之前
  2. F9跳至jdbcTemplate.queryForObject()处,查询数据库还是没有发现事务记录。
  3. F8执行下一步,这时查询数据库发现有事务记录,说明事务这才真正开启,也即只有遇到对数据库操作的时候才会真正开启事务

在这里插入图片描述

4、事务与锁的正确流程:加锁->遇到第一个对数据库的操作时开启事务->执行业务->释放锁->提交事务

3、失效原因及解决方法

1、原因分析:假设多个线程都调用事务方法,假设这时线程A加锁成功,其他线程阻塞,线程A执行到第一个数据库操作时事务开启,接着执行完业务逻辑之后,释放了锁;此时线程A的事务还没有提交,这时线程B抢占锁成功,并且获取到的qty并不是线程A加1之后的qty,因此就会出现达不到预期的情况。
2、解决方法:
  • 正确使用锁,把整个事务放在锁的工作范围之内,这样就能保证事务的提交一定是在释放锁之前了
  • 将隔离级别设置为串行化Isolation.SERIALIZABLE,都不需要使用加锁逻辑了,但是性能会跟不上,不建议使用
/**
 * @Date: 2023/2/13
 * 解决锁和事务一起使用失效的问题
 */
@Slf4j
@Service
public class LockAndTransactional {
    // 可重入公平锁
    private ReentrantLock lock = new ReentrantLock(true);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 由于@Transactional采用的是代理模式,采用this调用不会生效,所以这里通过自引用的方式来调用。
    @Autowired
    private LockAndTransactional lockAndTransactional;

    /**
     * 事务正确使用锁的方式:锁要将整个事务代码包含在内,这样就一定能保
     * 证事务提交一定是在释放锁之前的
     * @param name
     */
    public void transactionsUseLocksCorrectly(String name) {
        // 加锁
        lock.lock();
        try {
            log.info("线程:{},获取锁成功", Thread.currentThread().getName());
            lockAndTransactional.addQty(name);
        } finally {
            // 释放锁
            lock.unlock();
            log.info("线程:{},释放锁成功", Thread.currentThread().getName());
        }
    }

    @Transactional
    public void addQty(String name) {
        // 查询sql
        String querySQL = "select * from users where name = ?";
        // 执行查询
        User user = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), name);
        // 获取库存
        Integer qty = user.getQty();
        log.info("线程:{},查询的原始订量为:{}", Thread.currentThread().getName(), qty);
        qty = qty + 1;
        // 修改的sql
        String updateSQL = "update users set qty = ? where id = ?";
        // 调用jdbcTemplate的update方法,修改数据
        int update = jdbcTemplate.update(updateSQL, qty, user.getId());
        log.info("线程:{},修改订量完成,修改后的订量为:{}", Thread.currentThread().getName(), qty);
    }

    /**
     * 获取qty
     * @param name
     * @return
     */
    public User getQty(String name) {
        // 查询sql
        String querySQL = "select id, name, age, qty from users where name = ?";
        // 执行查询
        User user = jdbcTemplate.queryForObject(querySQL, BeanPropertyRowMapper.newInstance(User.class), name);
        return user;
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DataSourceAnnoConfig.class)
public class Test10 {
    @Autowired
    private LockAndTransactional lockAndTransactional;

    @Test
    public void test1() throws InterruptedException {
        User user = lockAndTransactional.getQty("张三");
        System.out.println("递增之前的结果:" + user);
        CountDownLatch countDownLatch = new CountDownLatch(20);
        // 递增20次,如果生成最后查询的结果应该为20
        for (int i = 0; i < 20; i++) {
            final int k = i;
            new Thread(() -> {
                lockAndTransactional.transactionsUseLocksCorrectly("张三");
                // 计数减1
                countDownLatch.countDown();
            }, "线程" + (k + 1)).start();
        }
        // 等待所有线程执行完成。主线程才继续向下执行
        countDownLatch.await();
        user = lockAndTransactional.getQty("张三");
        System.out.println("递增之后的结果:" + user);
    }
}
/**
 * 运行结果如下:qty等于20
 * 递增之前的结果:User(id=18, name=张三, age=50, qty=0, createTime=null, updateTime=null)
 * 递增之后的结果:User(id=18, name=张三, age=50, qty=20, createTime=null, updateTime=null)
 */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值