@Transactional(rollbackFor = Exception.class)
RuntimeException
以外的异常,以及用户自定义的 Exception
异常 (java 编译器会强制要求对异常进行处理)https://m.php.cn/article/459597.html
脏读 :脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。
不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
幻读 : 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
不可重复读的重点是修改 :
同一事务,两次读取到的数据不一样。
幻读的重点在于新增或者删除
同样的条件 , 第 1 次和第 2 次读出来的记录数不一样
脏读:
强调的是第二个事务读到的不够新。
Isolation 属性一共支持五种事务隔离级别,具体介绍如下:
● DEFAULT 使用数据库设置的隔离级别 ( 默认 ) ,由 DBA 默认的设置来决定隔离级别 .
● READ_UNCOMMITTED 会出现脏读、不可重复读、幻读 ( 隔离级别最低,并发性能高 )
● READ_COMMITTED 会出现不可重复读、幻读问题(锁定正在读取的行)
● REPEATABLE_READ 会出幻读(锁定所读取的所有行)
● SERIALIZABLE 保证所有的情况不会发生(锁表)
打新
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
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
@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);
}
在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 ('李四')");
}
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
整个过程中, 有 2 个地方需要用到数据库连接 Connection 对象
保证事务管理器中的 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是一样的,那么就可以发现当前存在的事务.
通过上面的事务管理器执行过程的分析, 我们就了解了开启事务时, 会从一开始事务下的数据库连接池获取数据库连接, 内层的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();
}
}