SpringAOP 事务

事务:本地事务

@Transactional(rollbackFor = Exception.class)

  • error 是一定会回滚的 (try/catch 是 catch 不住 error 的)
  • exception 分为运行时异常非运行时异常
  • 非运行时异常是指 RuntimeException 以外的异常,以及用户自定义的 Exception 异常 (java 编译器会强制要求对异常进行处理)

脏读、幻读、不可重复读的区别是什么

https://m.php.cn/article/459597.html

  1. 脏读 :脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。

  2. 不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。

  3. 幻读 : 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。

不可重复读的重点是修改 :

同一事务,两次读取到的数据不一样。

幻读的重点在于新增或者删除

同样的条件 , 第 1 次和第 2 次读出来的记录数不一样

脏读:

强调的是第二个事务读到的不够新。

Isolation 属性一共支持五种事务隔离级别,具体介绍如下:

● DEFAULT 使用数据库设置的隔离级别 ( 默认 ) ,由 DBA 默认的设置来决定隔离级别 .
● READ_UNCOMMITTED 会出现脏读、不可重复读、幻读 ( 隔离级别最低,并发性能高 )
● READ_COMMITTED 会出现不可重复读、幻读问题(锁定正在读取的行)
● REPEATABLE_READ 会出幻读(锁定所读取的所有行)
● SERIALIZABLE 保证所有的情况不会发生(锁表)

传播特性

  • 作用于内层方法上,加在外层方法上是无效的;
  • 内层异常(内层、外层均未catch的情况下)总会影响外层;
  • PROPAGATION_REQUIRED:如果不存在外层事务,就主动创建事务(内层会单独回滚);否则使用外层事务(内外同一个事务,一起回滚,即使外层catch内层之后也会回滚)
  • PROPAGATION_SUPPORTS:如果不存在外层事务,就不开启事务;否则使用外层事务(内层始终与外层有关)
  • PROPAGATION_MANDATORY(强制、义务):如果不存在外层事务,就抛出异常;否则使用外层事务
  • PROPAGATION_REQUIRES_NEW:总是主动开启事务;如果存在外层事务,就将外层事务挂起(不是嵌套事务,是两个事务。外层异常不会影响内层)
  • PROPAGATION_NOT_SUPPORTED:总是不开启事务;如果存在外层事务,就将外层事务挂起(与外层脱钩)
  • PROPAGATION_NEVER:总是不开启事务;如果存在外层事务,则抛出异常(与mandatory相反)
  • PROPAGATION_NESTED:如果不存在外层事务,就主动创建事务;否则创建嵌套的子事务(两个事务,包含关系,区别是catch内层之后外层不回滚)

简述

打新
Propagation required: same transaction
propagation requires new: different transaction

打新-嵌套 (使用同一个事物, 但不互相影响)
propagation nested: same transaction, but nested that commit only outside commit

子事务 或 潜套事务, 开始执行时取得一个 savepoint 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交, 如果外部事务 commit, 潜套事务也会被 commit, rollback 同样

不用-挂起 or 勉强用
propagation not supported: always hang-up, don’t use transaction 事不关己(不用事务)
propagation supports: no transaction unless nested by method call 墙头草

义务-duty
propagation mandatory: must have transaction, 约束 exception protect
propagation never: No transaction, 约束 exception

嵌套事务

savepoint t1
release savepoint t1
rollback to savepoint t1

Test

CREATE TABLE `demo_transaction` (
  `id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

全部提交

-- 格式化
TRUNCATE demo_transaction;
-- 开启事务
BEGIN;
-- 插入一条数据
INSERT INTO `demo_transaction`(id) VALUES(1);
-- 开启 SAVEPOINT
SAVEPOINT t1;
	INSERT INTO `demo_transaction`(id) VALUES(2);
-- 释放 SAVEPOINT
RELEASE SAVEPOINT t1;
-- 提交事务
COMMIT;

部分回滚

-- 格式化
TRUNCATE demo_transaction;
-- 开启事务
BEGIN;
-- 插入一条数据
INSERT INTO `demo_transaction`(id) VALUES(1);
-- 开启 SAVEPOINT
SAVEPOINT t1;
	INSERT INTO `demo_transaction`(id) VALUES(2);
-- 回滚 SAVEPOINT
ROLLBACK TO SAVEPOINT t1;
-- 提交事务
COMMIT;

全部回滚

-- 格式化
TRUNCATE demo_transaction;
-- 开启事务
BEGIN;
-- 插入一条数据
INSERT INTO `demo_transaction`(id) VALUES(1);
-- 开启 SAVEPOINT
SAVEPOINT t1;
	INSERT INTO `demo_transaction`(id) VALUES(2);
-- 释放 SAVEPOINT
RELEASE SAVEPOINT t1;
-- 提交事务
ROLLBACK;

事务管理器执行事务原理

  1. 有2个数据源分别连接数据库 ds1和 ds2
  2. 为每个数据源定义了1个事务管理器 transactionManager1和 transactionManager2
// 数据源1
@Bean
public DataSource dataSource1() {
    DataSource dataSource = new DataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/ds1?characterEncoding=UTF-8");
    dataSource.setUsername("root");
    dataSource.setPassword("root123");
    dataSource.setInitialSize(5);
    return dataSource;
}

// 事务管理器1,对应数据源1
@Bean
public PlatformTransactionManager transactionManager1(@Qualifier("dataSource1")DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

// 数据源2
@Bean
public DataSource dataSource2() {
    DataSource dataSource = new DataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/ds2?characterEncoding=UTF-8");
    dataSource.setUsername("root");
    dataSource.setPassword("root123");
    dataSource.setInitialSize(5);
    return dataSource;
}

// 事务管理器2,对应数据源2
@Bean
public PlatformTransactionManager transactionManager2(@Qualifier("dataSource2")DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}
  1. 指定事务的管理器 bean

在spring容器中已经定义了一个事务管理器,spring启动事务的时候,默认会按类型在容器中查找事务管理器,刚好容器中只有一个就拿过来用了

!!! 如果像上面那样有多个事务管理器时,如果不指定,spring是不知道具体用哪个事务管理器的

这里使用PROPAGATION_REQUIRED传播:如果不存在外层事务,就主动创建事务(内层会单独回滚);否则使用外层事务(内外同一个事务,一起回滚,even 外层 catch 内层之后也会回滚)

// Service1中:
@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void m1(){
    jdbcTemplate1.update("insert into user1(name) VALUES ('张三')");
 service2.m2();
}

// Service2中:
@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void m2(){
    jdbcTemplate1.update("insert into user1(name) VALUES ('李四')");
}
  1. spring 事务中有个resources的ThreadLocal,static修饰的,用来存放共享的资源,稍后过程中会用到。
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

事务管理器执行过程

  • 事务过程

整个过程中, 有 2 个地方需要用到数据库连接 Connection 对象

  1. spring 事务拦截器启动事务的时候会从 datasource 中获取一个连接,通过这个连接开启事务并设置手动提交.
  2. 最终执行 sql 操作时,用到同一个连接 (这两个连接是同一个连接时,执行 sql 的操作才会受 spring 事务控制)

保证事务管理器中的 datasource 和 JdbcTemplate 中的 datasource 是同一个,就是 spring 的事务管理器做的事.

		// TransactionInterceptor 拦截方法, 获取事务配置信息: 1)事务管理器 bean 名称 2)事务传播行为
		// 从 spring 容器中找到事务管理器 transactionManager1, 然后通过 transactionManager1 查询当前上下文中有没有事务
		// (显然现在是没有的) 创建一个新的事务
    // 获取事务管理器对应的数据源 dataSource1
    DataSource dataSource1 = transactionManager1.getDataSource();
    // 即从 dataSource1 中获取一个连接
    Connection conn = transactionManager1.dataSource1.getConnection();
    // 开启事务手动提交
    conn.setAutoCommit(false);
    // 将 dataSource1 -> conn 放入 map 中
    map.put(dataSource1,conn);
    // 将 map 放到上面的 resources ThreadLocal 中
    resources.set(map);

    // 执行更新: jdbctemplate 内部获取数据连接
    // 获取连接的过程: 从 resources 这个 ThreadLocal 中获取到 map
    Map map = resources.get();
    // 通过 jdbcTemplate1.datasource 从 map 获取可用连接
    Connection conn = map.get(jdbcTemplate1.datasource);
    // 如果从 map 没有找到连接,那么从 jdbcTemplate1.datasource 中重新获取一个
    // 大家应该可以看出来,jdbcTemplate1 和 transactionManager1 指定的是同一个dataSource, 索引这个地方conn是不为null的
    if(conn == null){
     conn = jdbcTemplate1.datasource.getConnection();
    }
    // 通过上面 map 里获取的 conn 执行 db 操作,插入张三


    // m2方法上面也有@Transactional,TransactionInterceptor拦截m2方法
    // (同上) 获取m2方法的事务配置信息:事务管理器 bean名称 和 事务传播行为
		// (同上) 从 spring 容器中找到事务管理器 transactionManager1, 然后通过 transactionManager1 查询当前上下文中有没有事务
    // (显然是是有的) m1 开启的事务正在执行中,所以 m2 方法就直接加入这个事务


    // jdbctemplate 内部需要获取数据连接,获取连接的过程
    // 从 resources 这个 ThreadLocal 中获取到 map
    Map map = resources.get();
    // 通过 jdbcTemplate1.datasource 从 map 看一下没有可用的连接
    Connection conn = map.get(jdbcTemplate1.datasource);
    // 如果从map没有找到连接,那么重新从 jdbcTemplate1.datasource 中获取一个
    // 应该可以看出来,这个地方 conn 是不为 null 的
    if(conn == null){
        conn = jdbcTemplate1.datasource.getConnection();
    }
    // 通过上面 map 里获取的 conn 执行 db 操作,插入李四

    // 最终 TransactionInterceptor 发现 2 个方法都执行完毕了,没有异常,执行事务提交操作
    // 获取事务管理器对应的数据源,即 dataSource1
    DataSource dataSource1 = transactionManager1.getDataSource();
    // 从 resources 这个 ThreadLocal 中获取到 map
    Map map = resources.get();
    // 通过 map 拿到事务管理器开启的连接
    Connection conn = map.get(dataSource1);
    // 通过 conn 提交事务
    conn.commit();
    // 管理连接
    conn.close();
    // 清理ThreadLocal中的连接
    map.remove(dataSource1); // 将连接从resource ThreadLocal中移除

    // 清理事务
  • 事务管理器判断当前是否有事务的简化过程如下
Map map = resource.get(); // resource 为 ThreadLocal>
DataSource datasource = transactionManager.getDataSource();
Connection conn = map.get(datasource);
// 如果 conn 不为空,就表示当前有事务, 就不会再拿新连接
if(conn == null) {
  conn = jdbcTemplate1.datasource.getConnection();
}

所以,判断是否存在事务,主要和datasource有关,和事务管理器无关,即使是不同的事务管理器,只要事务管理器的datasource是一样的,那么就可以发现当前存在的事务.

  • 事务挂起操作: 对当前存在事务的现场生成一个快照,然后将事务现场清理干净,然后重新开启一个新事务,新事务执行完毕之后,将事务现场清理干净,然后再根据前面的快照恢复旧事务。
    例如, REQUIRED_NEW 表示不管当前事务管理器中是否有事务,都会重新开启一个事务. 如果当前事务管理器中有事务,会把当前事务挂起。

解决 Mybatis-Plus 多数据源 + 事务, 数据源切换失效问题

通过上面的事务管理器执行过程的分析, 我们就了解了开启事务时, 会从一开始事务下的数据库连接池获取数据库连接, 内层的service虽然使用了@DS切换数据源,但实质上并没有改变整个事务的连接, 而在事务内的所有数据库操作, 都是在事务连接建立之后进行的,所以会产生数据源没有切换的问题

如何保证数据源切换正常,事务使用也正常?

想要使内部调用切换@DS起作用,就必须替换数据库连接,也就是改变事务的传播机制,使其产生新的事务,获取新的数据库连接。

通过外层方法加上 @Transactional 注解, 内层方法加上 @Transactional(propagation = Propagation.REQUIRES_NEW) 注解解决

解决方法

@Transactional(propagation = Propagation.REQUIRES_NEW)

表示新建事务,如果当前存在事务,把当前事务挂起。

需要注意:添加了该注解的方法需放在业务的最后一步处理,确保挂起的事务方法均已执行成功,再去开启新事务。

因为内部事务方法异常会造成外部事务回滚, 但外部事务异常并不会造成内部事务回滚。

@Service
@DS("ds2")
public class Step2ServiceImpl extends ServiceImpl<XXXXMapper, XXXX> implements XXXXService {

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Boolean operate2() {
        return save();
    }
}
@Service
@DS("ds1")
public class Step1ServiceImpl extends ServiceImpl<XXXXMapper, XXXX> implements XXXXService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public R<?> combine() {
        // operate2 在 operate1 前调用, 若 operate1 方法异常,operate2 将不会回滚
        step1ServiceImpl.operate1();
        step2ServiceImpl.operate2();
    }
}

你可能感兴趣的:(Spring,java)