当前位置: 首页 > article >正文

数据库事务详解

事务-1-数据库事务

今天聊一聊数据库的事务,这里以MySQL为例子。

在MySQL中,事务(Transaction)是一组SQL操作的集合,这些操作要么全部成功执行,要么全部失败回滚,确保数据的一致性和完整性。事务具有以下四个关键特性,通常称为ACID特性:

  1. 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的任何操作失败,整个事务将回滚到最初状态。事务中的所有元素必须作为一个整体提交或回滚,如果事务中的任何元素失败,则整个事务将失败。

  2. 一致性(Consistency):事务确保数据库从一个一致状态转换到另一个一致状态。即使在事务执行过程中出现错误,数据库也不会处于不一致的状态。大白话来说就是最终结果是我们预期的那样,比如A给B转钱100块,如果转成功了那么就是A少一百块,B多一百块;如果失败了,那么A和B账户里面的钱还是原封不动的。

  3. 隔离性(Isolation):多个事务并发执行时,每个事务的操作与其他事务隔离,互不干扰。MySQL提供了不同的隔离级别(如读未提交、读已提交、可重复读(innodb默认隔离级别)、串行化)来控制事务之间的可见性。

  4. 持久性(Durability):一旦事务提交,其对数据库的修改就是永久性的,即使系统崩溃也不会丢失。

还有一点值得注意的那就是隐式事务显式事务

执行单条SQL语句的时候,例如insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。

Mysql默认是提交事务的。MySQL 默认开启事务自动提交模式,每条 SOL 语句都会被当做一个单独的事务自动执行。

1.MySQL中的事务操作

  • 开始事务:使用START TRANSACTIONBEGIN语句开始一个新事务。
  • 提交事务:使用COMMIT语句提交事务,使所有修改永久生效。
  • 回滚事务:使用ROLLBACK语句回滚事务,撤销所有未提交的修改。

2.具体分析

数据库名字,trans_db, 这里假设有两个数据库表,如下sql建表语句

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `account_id` int NOT NULL AUTO_INCREMENT COMMENT '账户ID',
  `uid` int NULL DEFAULT NULL COMMENT '用户ID,该账户属于哪个用户的',
  `money` int NULL DEFAULT NULL COMMENT '账户金额,测试所以用int',
  PRIMARY KEY (`account_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of account
-- ----------------------------

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `age` int NULL DEFAULT NULL,
  PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;

上面是一个很简单的用户账户的两张表。

有三个账户。

在这里插入图片描述
在这里插入图片描述

每个账户归属于各自的用户,里面都有1000块钱。

①简单事务操作

**事务操作:**用户1给用户2转100块钱,那么1账户-100,2账户+100。

在这里插入图片描述

从上图看出,start transaction还有begin都可以开启一个事务,在事务提交之前,对数据库所做的操作,我们在右边是看不到的。只有在commit之后才会看到数据库中的修改。

在这里插入图片描述

可以看到commit提交之后,可以在数据库中看到对应的操作。

**回滚操作:**用户2给用户1转100块钱,但是我们在事务中回滚了,然后再提交,故会回到事务开始前的状态。

在这里插入图片描述

然后提交后,发现数据是原封不动 的。

savepoint操作:1给2转500块钱,2给1转100块钱,但是第二步操作是失败了。需要回到第一步操作结束的时候。

在这里插入图片描述

commit之前,中间的sql语句执行,对于我们而言都是不可见的。

上图中,savepoint保存了一个点,然后我们可以通过rollback to savepoint 名字来让数据回到那个保存点。

②隔离级别

select @@transaction_isolation;查看当前的隔离级别

事务的隔离性是通过数据库锁的机制实现的。

产生的问题概览:

  • 事务 A、B 交替执行,事务 A 读取到事务 B 未提交的数据,这就是脏读
  • 在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读
  • 事务 A 查询一个范围的结果集,另一个并发事务 B 往这个范围中插入 / 删除了数据,并静悄悄地提交,然后事务 A 再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读

在这里插入图片描述

原文链接:https://blog.csdn.net/zch981964/article/details/128099297

并发性能从上到下依次递减

不可重复读 vs 幻读:(引用知乎大佬的话https://www.zhihu.com/question/392569386,下面暖猫Suki的回答)

“脏读”指读到了未提交的数据,然后基于这个数据做了一些事情,结果做完发现数据被回滚了。可以理解为领导还没下达正式任务你就凭着自己的揣摩开始干活,结果活干完了,任务的内容被改了。
    
“不可重复读”好一点,读到的是已提交的数据,比如某个读事务持续时间比较长,期间多次读取某个元组,每次读到的都是被别人改过并已提交的不同数据。可以理解为在执行任务的过程中,领导的指令一直在变。但好歹是正式下达的指令。
    
“幻读”是指读的过程中,某些元组被增加或删除,这样进行一些集合操作,比如算总数,平均值等等,就会每次算出不一样的数。

    
所以“不可重复读”和“幻读”都是读的过程中数据前后不一致,只是前者侧重于修改,后者侧重于增删。个人认为,严格来讲“幻读”可以被称为“不可重复读”的一种特殊情况。但是从数据库管理的角度来看二者是有区别的。解决“不可重复读”只要加行级锁就可以了。而解决“幻读”则需要加表级锁,或者采用其他更复杂的技术,总之代价要大许多。这是搞数据库的那帮家伙非要把这两者区分开的动机吧。
// 这里解决幻读需要加表级锁这里我不是很清楚,下面评论有说 不需要锁表,MVCC配合索引上的next key lock的,这个本文章就不深究了,在后续文章中分析
    
作者:暖猫Suki
链接:https://www.zhihu.com/question/392569386/answer/1434210648
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.读未提交

所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,虽然拥有不错的并发处理能力较低的系统开销,但很少用于实际应用。因为一个最大的问题就是会读取到脏数据。

set session transaction isolation level read uncommitted;这句话,可以设置当前会话事务隔离级别为读未提交

下面举一个例子,开启两个命令行窗口,左边开启的事务负责修改数据,右边的窗口事务负责查询。

在这里插入图片描述

可以看到,左边是账户1扣50块钱,左边还未提交事务呢,右边的黑窗口里面已经查到了。但是navicat里面为啥看不到呢?因为navicat还是用的默认的隔离级别啊,我只是设置了当前会话的隔离级别,也就是那俩黑窗口是读未提交的。最后提交就可以更新到数据库了。

很显然哦,假如有这样一个例子,还是关于转账的,1有1000块,2有1000块,3有1000块。有如下两个事务

银行系统给用户1添加100块钱

begin; -- 1
update account set money = money + 100 where uid = 1; -- 2
commit; -- 3

银行系统查询用户1账户里面的余额

begin; -- 4
select * from account; -- 5
commit; -- 6

这两个事务操作是并发执行的。

但是,并发啊!!各位懂吗?

假如MySQL此时执行到上面的语句2的时候,还没来得及执行语句3【提交事务】,这个时候恰好执行了语句5,由于是读未提交,故此时读取到uid为1的账户里面有1100块钱.

在这里插入图片描述

可以看到,上图中,右边navicat里面,提交之前是看不到的。如果提交之后

在这里插入图片描述

就可以看到了。

如果,假如说如果,事务B失败回滚了,用户1账户应该还是1000块钱;但是在事务A中,读取到的数据是1100块钱,还有其他的业务操作的话,那么,事务A中用的就是脏数据了!!

2.读已提交

set session transaction isolation level read committed

这个可以避免脏读,但是会导致不可重复读

脏读由于是读已提交的,故在事务里面读取的都是别的事务提交过的数据,故不会出现脏读了,这里就不详细演示了。

在这里插入图片描述

从上面图中看出,绿色框框是在左边事务提交之前读取的,可以看到都是1000块钱,在左边事务提交之后,红色框框查询的就是900块钱了。

不可重复读:同一事务里面,不同时刻读取到的同一个值,他是不一样的,下面就来演示一下。

现在有两个事务并发,事务1需要查询一次账户1的余额,过一会又想查询一次账户1的余额;事务2在事务1查询第一次之后,修改了账户1的余额,然后事务2提交了事务。

在这里插入图片描述

可以看到在事务1里面,两次同样的查询是不一样的。

3.可重复读(default)

set session transaction isolation level repeatable read;

在这里插入图片描述

按照上面标注的顺序执行。可以看到在事务1里面,读取到的结果都是一样的。

扩展一下,死锁问题

思考一下,假如有两个事务,隔离级别是可重复读,事务1给账户1扣五百块钱,事务2也给账户1扣五百块钱。假如在事务1里面先读取到账户1,是1000块钱,执行update语句,不提交,此时在事务2里面也执行update扣五百的语句,会怎么样呢?

在这里插入图片描述

上面的图答案很明显,会锁住!如果事务1一直不提交的话,甚至会出现锁超时的情况【如果没有超时,在等待期间如果事务1提交了,会看到事务2会马上输出执行结果的】

mysql> update account set money = money - 500 where uid = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
-- MySQL 提供了锁超时机制(innodb_lock_wait_timeout),如果一个事务等待锁的时间超过设定的阈值,会自动回滚并释放锁。

在这里插入图片描述

上面的图给出了完整流程。

事务并发会导致有死锁的问题!在日常中,死锁是不可能百分之百避免的

4.串行化

SERIALIZABLE

set session transaction isolation level serializable;

这个就不详细演示了。

串行化的实现采用的是读写都加锁的原理。

在这里插入图片描述

/*
按照上面的顺序,语句4加了共享锁【读锁】,语句5执行的时候需要加上排它锁【写锁】,产生了死锁条件了。故左边窗口会在语句5这里卡住了,这个时候把右边窗口事务提交掉,释放了读锁,故此时左边窗口的线程就可以继续向下执行了。降低事务的隔离级别,上面的操作就不会出现这个问题。
*/

串行化的情况下,对于同一行事务,**写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。**这避免了所有的并发问题,但是锁的更多了。。。

使用了串行化隔离级别时,在这个事务没有被提交之前,其他的线程,只能等到当前事务提交完成之后,才能进行操作,这样会非常耗时,非常影响数据库的性能,通常情况下,不会使用这种隔离级别。

3.SpringBoot和事务

搭建好SpringBoot项目,配置好数据库的连接后。。。

①声明式

@Transactional注解:

@Transactional 是 Spring 框架中用于管理事务的核心注解。它可以应用于类或方法级别,用于声明事务的边界、传播行为、隔离级别、超时时间等属性。

@Transactional 可以标注在类或方法上:

  • 标注在类上:表示该类的所有公共方法都启用事务管理。
  • 标注在方法上:表示该方法启用事务管理。
@Service
public class AccountService {
    @Autowired
    private AccountRepository accountRepository;
    @Transactional
    public void transfer(Long fromId, Long toId, Double amount) {
        // 逻辑
    }
}

@Transactional 注解仅在以下条件下生效:

  1. 方法必须是 public:Spring 默认只对公共方法启用事务代理。
  2. 方法必须通过代理调用:如果方法在同一个类中直接调用,事务不会生效(因为 Spring 使用代理模式)。
  3. Spring 事务管理器已配置:确保在 Spring 配置中启用了事务管理。
// 先来看看该注解长啥样
@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 {};
    //事务的传播行为,默认是REQUIRED
	Propagation propagation() default Propagation.REQUIRED;
    //事务的隔离级别,默认值采用Default,即基于当前数据库事务的隔离级别
	Isolation isolation() default Isolation.DEFAULT;
    //事务的超时时间
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
	String timeoutString() default "";
	boolean readOnly() default false;
    //用于指定会触发事务回滚的异常类型
	Class<? extends Throwable>[] rollbackFor() default {};
	String[] rollbackForClassName() default {};
	Class<? extends Throwable>[] noRollbackFor() default {};
	String[] noRollbackForClassName() default {};
}
// 上文中关于@Transactional也有介绍

@Transactional核心属性

propagation(传播行为)

定义事务的传播行为,即当前方法如何与已有事务交互。默认值为 Propagation.REQUIRED

  • REQUIRED:如果当前存在事务,则加入该事务;否则创建一个新事务。【默认的】
  • REQUIRES_NEW:总是创建一个新事务,如果当前存在事务,则挂起当前事务。
  • NESTED:如果当前存在事务,则在嵌套事务中执行。
  • SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务。
  • MANDATORY:如果当前存在事务,则加入该事务;否则抛出异常。
  • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
isolation(隔离级别)

定义事务的隔离级别,用于控制事务之间的可见性。默认值为 Isolation.DEFAULT(使用数据库的默认隔离级别)。

  • DEFAULT:使用数据库的默认隔离级别。
  • READ_UNCOMMITTED:读未提交,最低隔离级别。
  • READ_COMMITTED:读已提交。
  • REPEATABLE_READ:可重复读。
  • SERIALIZABLE:串行化,最高隔离级别。
timeout(超时时间)

定义事务的超时时间(以秒为单位)。如果事务在指定时间内未完成,则自动回滚。默认值为 -1(不超时)。长事务会有对数据库有较长的锁定,长时间会占用数据库资源。

readOnly(只读事务)

定义事务是否为只读。只读事务可以优化数据库性能,避免不必要的写操作。默认值为 false

rollbackFornoRollbackFor

定义哪些异常触发回滚,哪些异常不触发回滚。

  • rollbackFor:指定触发回滚的异常类型(默认为 RuntimeExceptionError)。
  • noRollbackFor:指定不触发回滚的异常类型。
用法演示
// 1.最基本的用法--声明式事务
@Override
@Transactional
public void saveSimple1(Account account) {
    System.out.println("【需要插入的数据】:" + account);
    accountDao.insert(account);
    throw new RuntimeException("抛出异常额~~~");
}

在方法上加上这个@Transactional注解,即可保证该方法内为一个事务,上面例子,抛出异常后会rollback,即数据库数据不变。

// 2.外层函数声明式事务, 内层没有
@Override
@Transactional
public void saveSimple2(Account account) {
    System.out.println("【需要插入的数据】:" + account);
    fun1(account);
    throw new RuntimeException("抛出异常额~~~");
}

//@Transactional  这个注解加上了,在默认情况下,效果是一样的
public void fun1(Account account) {
    accountDao.insert(account);
}

很显然嘛,可以想象默认情况下,最外层的把内层所有的包起来了,形成整个事务。

// 3.同类中,外层没有,内层有
@Override
public void saveSimple3(Account account) {
    System.out.println("【需要插入的数据】:" + account);
    fun2(account);
    throw new RuntimeException("抛出异常额~~~");
}
@Transactional
public void fun2(Account account) {
    accountDao.insert(account);
}

**事务会失效!**为啥啊。

// 4.不同类中的方法声明式事务, 外层没有注解
// AccountServiceImpl.class
@Override
public void saveSimple4(Account account) {
   accountDao.insert(account);
   userService.updateByUserId(account.getUid()); // 调用UserServiceImpl的事务方法
   throw new RuntimeException("抛出异常额~~~不同类中");
}
// UserServiceImpl.class
@Override
@Transactional
public void updateByUserId(Integer uid) {
    User user = userDao.selectById(uid);
    user.setName(user.getName() + "t");
    userDao.updateById(user);
	// throw new RuntimeException("抛出异常额~~~不同类中");  //注释点【1】
}

都没有回滚。为啥啊。

这个先说结论,accountDao.insert(account);这一句不会滚很正常。userService.updateByUserId(account.getUid());是另一个类的事务里面,但是那个正常执行了呀。抛异常又不是在事务里面抛的,所以都不会回滚呐。

假如说,将上面注释点【1】解开,那么,就是accountDao.insert(account);不会滚,userService.updateByUserId(account.getUid());回滚了。!!!

事务为什么会失效?
SpringBoot事务自动配置:

Spring 声明式事务基于 AOP 实现。

与数据库打交道,我们需要引入jdbc的依赖,导入这个场景

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!--有时候我们引入的mybatis的-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>
<!--从mybatis依赖往上看,发现它其实也引入了spring-boot-starter-jdbc-->

<!--最后,追根溯源,就是这个包-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>

在SpringBoot原理分析-1中,我们知道导入这个场景,就有了事务支持的话,肯定是用了自动装配了。

那么,我们去spring-boot-autoconfigure中看一下,会发现其有transaction这个包!

在这里插入图片描述

.....
public class TransactionAutoConfiguration {

    //=================== 显示定义了以下的三个bean
	@Bean
	@ConditionalOnMissingBean
	public TransactionManagerCustomizers platformTransactionManagerCustomizers(
			ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
		return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
	}
	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
	public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
		return TransactionalOperator.create(transactionManager);
	}
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
	public static class TransactionTemplateConfiguration {
		@Bean
		@ConditionalOnMissingBean(TransactionOperations.class)
		public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
			return new TransactionTemplate(transactionManager);
		}
	}
    //================================================

    // 重点配置是在内部类EnableTransactionManagementConfiguration中
    // 该类对Jdk动态代理和CGlib动态代理两种方式分别作了配置
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(TransactionManager.class)
	@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
	public static class EnableTransactionManagementConfiguration {
		@Configuration(proxyBeanMethods = false)
		@EnableTransactionManagement(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
		public static class JdkDynamicAutoProxyConfiguration {}

		@Configuration(proxyBeanMethods = false)
        // 指示是否创建基于子类 (CGLIB) 的代理 (true) 而不是基于标准 Java 接口的代理 (false)。
		@EnableTransactionManagement(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		public static class CglibAutoProxyConfiguration {}

	}
.........................

}

这个内部类中并没有配置事务相关的bean,那么关键是在@EnableTransactionManagement注解中

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//使用@Import注解导入了一个TransactionManagementConfigurationSelector
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {...}

下面来看一下TransactionManagementConfigurationSelector

public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
.........
	@Override
	protected String[] selectImports(AdviceMode adviceMode) {
    	// 如果adviceMode是代理模式,那么就走其对应分支,这里仅分析动态代理的情况咯
		switch (adviceMode) {
			case PROXY:
				return new String[] {AutoProxyRegistrar.class.getName(),
						ProxyTransactionManagementConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {determineTransactionAspectClass()};
			default:
				return null;
		}
	}
..........

}

也就是说,ProxyTransactionManagementConfiguration才是真正的配置类!

public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
	@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) // 1
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    /*
    这是一个 Spring AOP 的 Advisor,它将事务拦截器(TransactionInterceptor)与事务属性源(TransactionAttributeSource)结合起来,用于在方法调用时应用事务管理。
    */
	public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
			TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {

		BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
		advisor.setTransactionAttributeSource(transactionAttributeSource);
		advisor.setAdvice(transactionInterceptor);
		if (this.enableTx != null) {
			advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
		}
		return advisor;
	}
    
	@Bean // 2 -------transactionAttributeSource bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public TransactionAttributeSource transactionAttributeSource() {
        /*
        AnnotationTransactionAttributeSource:这是一个具体的实现类,它从方法或类上的 @Transactional 注解中解析事务属性。
        */
		return new AnnotationTransactionAttributeSource();
	}

	@Bean // 3 ---
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    //这是一个 Spring AOP 的拦截器,用于在方法调用时执行事务管理逻辑。
	public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
		TransactionInterceptor interceptor = new TransactionInterceptor();
        // 将事务属性源设置到拦截器中。
		interceptor.setTransactionAttributeSource(transactionAttributeSource);
		if (this.txManager != null) {
            // 如果 txManager 属性不为空,则将事务管理器设置到拦截器中。
			interceptor.setTransactionManager(this.txManager);
		}
		return interceptor;
	}
}

上面有三个Bean,第一个Bean将第二个,第三个Bean结合起来。第三个Bean用到了第二个Bean,用来设置事务属性源。这里是注解

接下来看看这个TransactionInterceptor

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    // 里面有一个invoke方法
    @Override
	@Nullable
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
		return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
			@Override
			@Nullable
			public Object proceedWithInvocation() throws Throwable {
				return invocation.proceed();
			}
			@Override
			public Object getTarget() {
				return invocation.getThis();
			}
			@Override
			public Object[] getArguments() {
				return invocation.getArguments();
			}
		});
	}
}
SpringBoot事务执行过程

首先思考一下,如果要我们手动实现基于aop的事务,该怎么做呢。

// service
public Object getUser(Integer uid) {
 	return userMapper.getById(uid);
}

无非就是环绕通知around来实现嘛,最后达成这样一个效果

public .....  invoke ( 真实对象 o ) {
 	before操作;
    o.getUser(uid);
    after操作
}

但是Spring的会自动回滚耶,那我们就多加一点儿东西嘛

public .....  invoke ( 真实对象 o ) {
    连接 Connect connection;
    try{
        before操作;
        得到连接connection;
    	o.getUser(uid);
    	after操作
        事务提交connection.commit();
    } catch( 异常 ) {
        connection.rollback()
    }
}

这样不就可以了吗,Spring是这样做的吗?解下来通过一个例子来追踪一下调用过程。

// Controller
// 7.源码追踪
@PostMapping("/add7")
public R add7(@RequestBody Account account) {
    accountService.saveSimple7(account); // 将这里打上断点,debug
    return R.success();
}
// service
@Override
@Transactional
public void saveSimple7(Account account) {
    Integer uid = account.getUid();
    int i = 1 / uid;
    accountDao.insert(account); // 将这里也打上断点,debug
}

在这里插入图片描述

这个accountService怎么是这个样子??

然后我们点击进入方法,来到了CglibAopProxy类中的下面的方法了。

在这里插入图片描述

retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();

执行到,这一行代码了,创建方法代理对象,然后执行proceed方法

@Override
@Nullable
public Object proceed() throws Throwable {
    try {
        // 主要是这一行,调用父类的方法
        return super.proceed(); 
    }
    catch (RuntimeException ex) {
        throw ex;
    }
    catch (Exception ex) {
        ..............
    }
}

ReflectiveMethodInvocation是它的父类:用来处理方法调用的拦截器链,因为可能不只有事务@Transactional,还可能会有我们自定义的其他方法拦截器,比如说日志记录aop之类的

@Override
@Nullable
public Object proceed() throws Throwable {
    // 检查是否到达拦截器链的末尾--责任链设计模式?
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }
    // 获取下一个拦截器或动态方法匹配器
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    // 处理动态方法匹配器,这里不清楚
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
       ........
    }
    else {
        //如果当前对象是一个普通的拦截器(MethodInterceptor),则直接调用其 invoke() 方法。
        // 执行到这里了====================
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); // 进入这里
    }
}
  • 拦截器链的执行
    • Spring AOP 使用拦截器链来实现方法调用的增强(如事务管理、日志记录等)。
    • 每个拦截器都可以在目标方法执行前后插入自定义逻辑。
  • 递归终止条件
    • 这段代码是递归调用 proceed() 的终止条件。
    • 当所有拦截器都执行完毕后,最终调用目标方法。
  • 责任链模式
    • Spring AOP 使用了责任链模式(Chain of Responsibility),每个拦截器都可以决定是否继续调用下一个拦截器。
    • 通过递归调用 proceed(),控制权在拦截器链中逐级传递。

假设有以下拦截器链:

  1. 拦截器 A
  2. 拦截器 B
  3. 目标方法

调用流程如下:

  1. 调用 proceed()currentInterceptorIndex-1 变为 0,执行拦截器 A 的 invoke()
  2. 在拦截器 A 的 invoke() 中,调用 proceed()currentInterceptorIndex0 变为 1,执行拦截器 B 的 invoke()
  3. 在拦截器 B 的 invoke() 中,调用 proceed()currentInterceptorIndex1 变为 2
  4. 此时,currentInterceptorIndex == interceptorsAndDynamicMethodMatchers.size() - 1,调用 invokeJoinpoint(),执行目标方法。
  5. 目标方法执行完毕后,逐级返回结果,最终返回给调用方。

之后我们来到了TransactionInterceptor,它继承自TransactionAspectSupport

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
    ..................... // 执行这个,是父类TransactionAspectSupport的方法
    return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
        @Override
        @Nullable
        public Object proceedWithInvocation() throws Throwable {
            return invocation.proceed();
        }
        @Override
        public Object getTarget() {
            return invocation.getThis();
        }
        @Override
        public Object[] getArguments() {
            return invocation.getArguments();
        }
    });
}

TransactionAspectSupport

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {
	// 这个方法有点长。。。只截取重要部分
    //  处理普通事务
    // 平台事务管理器,用于管理传统的事务。
    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 创建事务
        // 根据事务属性创建事务。如果当前方法需要事务,则开启一个新事务;否则,可能加入现有事务。
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
        Object retVal;
        try {
            //invocation.proceedWithInvocation():实际执行目标方法。这是 AOP 拦截器链的一部分,确保在方法执行前后可以插入其他逻辑。
            retVal = invocation.proceedWithInvocation(); // 执行的是上面new CoroutinesInvocationCallback()里面的proceed方法!!!形成递归调用链!!!!!!!!
        }
        catch (Throwable ex) {
            //调用 completeTransactionAfterThrowing 方法,根据事务属性决定是否回滚事务。
            completeTransactionAfterThrowing(txInfo, ex);//见下面
            throw ex;
        }
        finally {
            //无论方法是否成功执行,都会清理当前线程的事务信息,确保不会影响后续操作。
            cleanupTransactionInfo(txInfo);
        }
........................
		// 如果方法成功执行且没有异常,则提交事务。
        commitTransactionAfterReturning(txInfo); //见下面
        return retVal;
    }
}

***retVal = invocation.proceedWithInvocation(); // 执行的是上面new CoroutinesInvocationCallback()里面的proceed方法!!!形成递归调用链!!!!!!!!***这一段我感觉挺重要的!!!

上面的代码结构有点儿熟悉哦,在这一小节最开始的时候,我们说了,如果我们要自定义实现这样的功能是不是和这个结构有点儿相似?

获取事务管理器
创建事务
try{
    执行我们service的方法 // ==============
} catch (异常 e) {
    回滚
} finally{
    清理当前线程的事务信息
}
提交事务

completeTransactionAfterThrowing(txInfo, ex); 出现异常,被捕获到了。

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        ......
        if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
            ....
            // rollback吧
            txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            ....
        }
        .......
    }
}

如果正常执行:commitTransactionAfterReturning(txInfo);

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        // commit吧
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}
小结一下!!
  • 故如果在@Transactional方法里面,如果手动将抛出的异常给捕获了,
@Transactional
// 代码块test1方法-----start
public void test1() {
    try{
        mapper.insert()
    } catch ( Exception e ) {
        // 捕获了异常,没有继续往外抛
        log.info("asdsddsasdss");
    }
}
// 代码块test1方法-----end

那么,按照Spring源码的结构,就相当于这样的了

获取事务管理器
创建事务
try{
    // 代码块test1方法-----start
    try{
        mapper.insert()
    } catch ( Exception e ) {
        // 捕获了异常,没有继续往外抛
        log.info("asdsddsasdss");
    }
    // 代码块test1方法-----end
} catch (异常 e) {
    回滚
} finally{
    清理当前线程的事务信息
}
提交事务

Spring捕获不到异常,就不会回滚了。

  • 同类方法调用,如果最外层的没有加@Transactional注解,内层调用有的话,不生效

Spring 在启动时会扫描所有被 @Component@Service@Repository 等注解标记的类。如果类或方法上标注了 @Transactional 注解,Spring 会将这些方法标记为需要事务管理。

Spring 通过 TransactionAttributeSource 接口解析 @Transactional 注解中的属性(如传播行为、隔离级别、超时时间等)。

默认实现类是 AnnotationTransactionAttributeSource,它负责从 @Transactional 注解中提取事务属性。

各位还记得在上一小节SpringBoot事务自动配置里面吗,ProxyTransactionManagementConfiguration这个真正的配置类,他配置了一个Bean

new AnnotationTransactionAttributeSource();

//AnnotationTransactionAttributeSource.java
@Override
public boolean isCandidateClass(Class<?> targetClass) {
    //遍历所有的 TransactionAnnotationParser,解析方法或类上的 @Transactional 注解。
    for (TransactionAnnotationParser parser : this.annotationParsers) {
        if (parser.isCandidateClass(targetClass)) {
            return true;
        }
    }
    return false;
}
//SpringTransactionAnnotationParser.java
public class SpringTransactionAnnotationParser implements TransactionAnnotationParser, Serializable {
	@Override
	public boolean isCandidateClass(Class<?> targetClass) {
		return AnnotationUtils.isCandidateClass(targetClass, Transactional.class);
	}
}

BeanFactoryTransactionAttributeSourceAdvisor 是一个 AOP Advisor,它决定了哪些方法需要被事务拦截器拦截【见上一小节】

BeanFactoryTransactionAttributeSourceAdvisor 依赖于 TransactionAttributeSource 来解析方法或类上的 @Transactional 注解。 内部使用了一个 TransactionAttributeSourcePointcut,它的 matches() 方法决定了哪些方法需要被拦截:

// BeanFactoryTransactionAttributeSourceAdvisor
public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
	@Nullable
	private TransactionAttributeSource transactionAttributeSource;

	private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
		@Override
		@Nullable
		protected TransactionAttributeSource getTransactionAttributeSource() {
			return transactionAttributeSource;
		}
	};
}

// TransactionAttributeSourcePointcut
private static final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        TransactionAttributeSource tas = getTransactionAttributeSource();
        return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
    }
}

matches() 方法

调用 TransactionAttributeSourcegetTransactionAttribute() 方法,获取当前方法的事务属性。如果事务属性不为 null,则表示该方法需要被拦截;否则,不需要被拦截。

由于是同类方法之间的调用,最外层没有@Transactional注解,拦截器链就不会有transactionInterceptor,所以就没有事务咯!

②编程式

@Resource
private TransactionTemplate transactionTemplate; // 需要这个。
// 5.编程式事务-1
@Override
public void saveSimple5(Account account) {
    Boolean execute = transactionTemplate.execute((status) -> {
        try {
            accountDao.insert(account);
            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("手动捕获异常~~~");
            status.setRollbackOnly(); // 回滚--手动控制
            return false;
        }
        return true;
    });
    System.out.println("execute = " + execute);
}

完全手动控制了。就不会有那种嵌套的问题了。但是每个流程都要考虑到哦

// UserServiceImpl.class
@Override
public void updateByUserId1(Integer uid) {
    transactionTemplate.execute((status)->{
        try {
            User user = userDao.selectById(uid);
            user.setName(user.getName() + "t");
            userDao.updateById(user);
            int i = 1 / 0; 
        } catch (Exception e) {
            status.setRollbackOnly();
        }
        return null;
    });
}
// 6.编程式事务嵌套问题
// AccountServiceImpl.class
@Override
public void saveSimple6(Account account) {
    Boolean execute = transactionTemplate.execute((status) -> {
        try {
            accountDao.insert(account);
            // 调用了上面的方法
            userService.updateByUserId1(account.getUid());
            int i = 1 / 0; //=============这里【注释点1】
        } catch (Exception e) {
            System.out.println("手动捕获异常~~~");
            status.setRollbackOnly(); // 回滚--手动控制
            return false;
        }
        return true;
    });
    System.out.println("execute = " + execute);
}

上面的代码是不会往数据库插入和修改数据的。

思考题!!!!

如果上面的代码中,我将“【注释点1】”的那一行代码注释掉了,会发生什么呢?然后分析说,accountDao.insert(account);执行成功,userService.updateByUserId1(account.getUid()里面有异常手动回滚了,最外层没有异常,故account会插入一条记录,对于user的修改会回滚!。

那这样你就错了,事务默认的传播行为是REQUIRED,如果当前存在事务,则加入该事务;否则创建一个新事务。不管有几个事务存在,都**合并成一个事务来处理,只要有一个事务抛出异常,所有事务都会回滚;**案例6里面,编程式事务,默认情况下,TransactionTemplate 使用 PROPAGATION_REQUIRED传播机制,故二者合并成一个事务了。

也就是说saveSimple6这个方法里面的东西,都被包裹在一个事务里面了,最外层我们暂且称之为外部事务,但是在内部调用的updateByUserId1方法里面,内部事务已经抛出异常了,此时,整个事务已经被标记为rollback-only,最外层事务还commit的话,这就有问题了。会报错"Transaction rolled back because it has been marked as rollback-only"。

4.分布式事务

1.相关概念

分布式事务是指跨越多个分布式系统或服务的事务操作,需要保证这些操作要么全部成功,要么全部失败。在分布式系统中,由于数据和服务分散在不同的节点上,传统单机事务的 ACID 特性(原子性、一致性、隔离性、持久性)难以直接实现,因此需要引入分布式事务解决方案。

在单体应用中,事务通常由数据库管理系统(如 MySQL)直接支持,通过本地事务即可保证 ACID 特性。但在分布式系统中:

  • 数据存储在不同的数据库或服务中。
  • 服务之间通过网络通信,可能存在延迟、故障或分区。
  • 无法直接使用本地事务来保证跨服务或跨数据库的一致性。

场景:

  • 跨数据库事务:例如,订单服务需要同时更新订单数据库和库存数据库。
  • 跨服务事务:例如,支付服务需要调用订单服务和库存服务,完成支付、更新订单状态和扣减库存。
  • 跨系统事务:例如,银行转账需要同时更新两个不同银行的账户余额。

分布式事务的解决方案可以分为两类:

  • 强一致性方案:保证事务的 ACID 特性,但性能较低。
  • 最终一致性方案:通过异步补偿或消息队列实现最终一致性,性能较高。

2.解决方法

以下是常见的分布式事务解决方案:【来自于gpt】

(1)两阶段提交(2PC,Two-Phase Commit)
  • 原理
    1. 准备阶段:协调者(Coordinator)询问所有参与者(Participant)是否可以提交事务。
    2. 提交阶段:如果所有参与者都同意提交,协调者通知所有参与者提交事务;否则,通知所有参与者回滚事务。
  • 优点:强一致性,保证事务的原子性。
  • 缺点
    • 性能较低,同步阻塞。
    • 协调者单点故障。
    • 网络分区时可能导致数据不一致。
(2)三阶段提交(3PC,Three-Phase Commit)
  • 原理:在 2PC 的基础上增加了一个预提交阶段,减少阻塞时间。
  • 优点:比 2PC 更容错。
  • 缺点:实现复杂,性能仍然较低。
(3)TCC(Try-Confirm-Cancel)
  • 原理
    1. Try 阶段:尝试执行业务操作,预留资源。
    2. Confirm 阶段:确认执行业务操作,提交资源。
    3. Cancel 阶段:取消执行业务操作,释放资源。
  • 优点:性能较高,适用于高并发场景。
  • 缺点:需要业务代码实现 Try、Confirm、Cancel 逻辑,开发成本较高。
(4)本地消息表(Local Message Table)
  • 原理
    1. 在本地事务中插入一条消息记录。
    2. 通过消息队列异步通知其他服务。
    3. 其他服务消费消息并执行业务操作。
  • 优点:实现简单,性能较高。
  • 缺点:需要保证消息的可靠投递和幂等性。
(5)Saga 模式
  • 原理
    1. 将分布式事务拆分为多个本地事务。
    2. 每个本地事务执行后发布事件,触发下一个本地事务。
    3. 如果某个本地事务失败,则执行补偿操作回滚之前的操作。
  • 优点:适用于长事务,性能较高。
  • 缺点:需要实现补偿逻辑,开发成本较高。
(6)消息队列(MQ)
  • 原理
    1. 生产者发送消息到消息队列。
    2. 消费者消费消息并执行业务操作。
    3. 通过消息的可靠投递和幂等性保证最终一致性。
  • 优点:解耦系统,性能较高。
  • 缺点:需要保证消息的可靠投递和幂等性。

见后续文章-----

文章中的示例项目见gitee:https://gitee.com/quercus-sp204/sourcecode-and-tools


http://www.kler.cn/a/514798.html

相关文章:

  • 【前端知识】简单易懂的vue前端页面元素权限控制
  • 京华春梦,守岁这方烟火人间
  • k8s集群换IP
  • 深度学习 · 手撕 DeepLearning4J ,用Java实现手写数字识别 (附UI效果展示)
  • pytest+playwright落地实战大纲
  • 利用R计算一般配合力(GCA)和特殊配合力(SCA)
  • 分支与循环(下)
  • 汽车制造行业案例 | 发动机在制造品管理全解析(附解决方案模板)
  • fastapi 博客系统模型分析
  • 考研408笔记之数据结构(六)——查找
  • go语言gui窗口应用之fyne框架-动态添加、删除一行控件(逐行注释)
  • Django的models.model如何使用
  • LoRA面试篇
  • AIGC浪潮下,图文内容社区数据指标体系如何构建?
  • nodeJS 系统学习(package-包-章节2)
  • 2025牛客寒假算法营1
  • C++并发编程之线程中断异常的捕捉与信息显示
  • Groovy语言的安全开发
  • PAT甲级-1014 Waiting in Line
  • 【软件】解决奥林巴斯生物显微镜软件OlyVIA提示“不支持您使用的操作系统”安装中止的问题
  • 【思科】NAT配置
  • macos app签名和公证
  • PHP教育系统小程序
  • Python网络自动化运维---用户交互模块
  • Vue3组件重构实战:从Geeker-Admin拆解DataTable的最佳实践
  • 场馆预定平台高并发时间段预定实现V2