首页 > 数据库 >数据库事务详解

数据库事务详解

时间:2025-01-22 19:42:44浏览次数:1  
标签:account return 数据库 事务 回滚 详解 提交 public

事务-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. 通过消息的可靠投递和幂等性保证最终一致性。
  • 优点:解耦系统,性能较高。
  • 缺点:需要保证消息的可靠投递和幂等性。

见后续文章-----

标签:account,return,数据库,事务,回滚,详解,提交,public
From: https://www.cnblogs.com/jackjavacpp/p/18686689

相关文章

  • 学生管理系统C++版(简单版)详解
    有错请指出啊~,答应大家的来了头文件:#include<iostream>#include<stdlib.h>#include<windows.h>iostream是标准头文件,stdlib.h也可以写成cstdlib,windows.h,用Sleep数据定义:intx,y=0;//x是输入,y是xm的下标,初始化y为0详解见代码。 结构体类型:structStudent{  c......
  • nginx配置之斜杠详解
    配置location、proxy_pass时,加“/”与不加“/”的区别,今天我们通过实操去验证下。以下测试都通过nginx代理访问地址:http://127.0.0.1/v1/pt/apply/page:第一种:location、proxy_pass都不加斜杠location/v1{proxy_passhttp://127.0.0.1:8899;}实际访问代理地址:http://1......
  • (DM)达梦数据库基本操作(持续更新)
    1、连接达梦数据库./disql用户明/'"密码"'@IP+端口或者域名2、进入某个模式(数据库,因达梦数据库没有库的概念,只有模式,可以将模式等同于库)setschema库名;3、查表结构;SELECTCOLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULTFROMUSER_TAB_COLUMNSWHERETA......
  • 【信息系统项目管理师-选择真题】2019下半年综合知识答案和详解
    更多内容请见:备考系统架构设计师-专栏介绍和目录文章目录【第1题】【第2题】【第3题】【第4题】【第5题】【第6题】【第7题】【第8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16题】【第17题】【第18题】......
  • 深入探讨视图更新:提升数据库灵活性的关键技术
    title:深入探讨视图更新:提升数据库灵活性的关键技术date:2025/1/21updated:2025/1/21author:cmdragonexcerpt:在现代数据库的管理中,视图作为一种高级的抽象机制,为数据的管理提供了多种便利。它不仅简化了复杂查询的过程,还能用来增强数据的安全性,限制用户对基础......
  • 深入理解视图的创建与删除:数据库管理中的高级功能
    title:深入理解视图的创建与删除:数据库管理中的高级功能date:2025/1/21updated:2025/1/21author:cmdragonexcerpt:在现代数据库管理系统中,视图是一个重要的高级功能,可以为用户提供定制化的数据视图以满足特定需求。视图不仅能够简化复杂的查询,还能增强数据安全......
  • 22. C语言 输入与输出详解
    本章目录:前言1.输入输出的基础概念1.1标准输入输出流1.2输入输出函数2.格式化输出与输入2.1使用`printf()`进行输出示例1:输出字符串示例2:输出整数示例3:输出浮点数2.2使用`scanf()`进行输入示例4:读取整数和字符改进方案:使用`getchar()`清理缓冲......
  • 请问如何通过数据库修改网站的密码?
    通过数据库修改网站的密码需要谨慎操作,以下是详细的步骤:备份数据库:在进行任何修改之前,确保备份整个数据库。登录数据库管理工具:使用数据库管理工具(如phpMyAdmin)登录到数据库。选择数据库:选择需要修改的数据库。找到用户表:找到存储用户信息的表,通常命名为us......
  • 详解Redis的Zset类型及相关命令
    目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZPOPMAXBZPOPMAXZPOPMINBZPOPMINZRANKZREVRANKZSCOREZREMZREMRANGEBYRANKZREMRANGEBYSCOREZINCRBYZINTERSTORE内部编码应用场景Zset简介有序集合相对于字符串、列表、哈希、集合来说会有......
  • 如何优化数据库查询性能?请列举一些常见的优化方法。
    优化数据库查询性能是提升系统效率和用户体验的重要手段。以下是一些常见的优化方法,结合了多篇证据中的内容:1. 使用索引索引是提高查询速度的核心工具,应根据查询字段和表大小合理创建索引。例如,为主键、常用查询字段(如WHERE子句中的字段)创建索引可以显著提升查询效率。避免......