事务-1-数据库事务
今天聊一聊数据库的事务,这里以MySQL为例子。
在MySQL中,事务(Transaction)是一组SQL操作的集合,这些操作要么全部成功执行,要么全部失败回滚,确保数据的一致性和完整性。事务具有以下四个关键特性,通常称为ACID特性:
-
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的任何操作失败,整个事务将回滚到最初状态。事务中的所有元素必须作为一个整体提交或回滚,如果事务中的任何元素失败,则整个事务将失败。
-
一致性(Consistency):事务确保数据库从一个一致状态转换到另一个一致状态。即使在事务执行过程中出现错误,数据库也不会处于不一致的状态。大白话来说就是最终结果是我们预期的那样,比如A给B转钱100块,如果转成功了那么就是A少一百块,B多一百块;如果失败了,那么A和B账户里面的钱还是原封不动的。
-
隔离性(Isolation):多个事务并发执行时,每个事务的操作与其他事务隔离,互不干扰。MySQL提供了不同的隔离级别(如读未提交、读已提交、可重复读(innodb默认隔离级别)、串行化)来控制事务之间的可见性。
-
持久性(Durability):一旦事务提交,其对数据库的修改就是永久性的,即使系统崩溃也不会丢失。
还有一点值得注意的那就是隐式事务和显式事务
执行单条SQL语句的时候,例如insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。
Mysql默认是提交事务的。MySQL 默认开启事务自动提交模式
,每条 SOL 语句都会被当做一个单独的事务自动执行。
1.MySQL中的事务操作
- 开始事务:使用
START TRANSACTION
或BEGIN
语句开始一个新事务。 - 提交事务:使用
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
注解仅在以下条件下生效:
- 方法必须是
public
:Spring 默认只对公共方法启用事务代理。 - 方法必须通过代理调用:如果方法在同一个类中直接调用,事务不会生效(因为 Spring 使用代理模式)。
- 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
。
rollbackFor
和 noRollbackFor
定义哪些异常触发回滚,哪些异常不触发回滚。
rollbackFor
:指定触发回滚的异常类型(默认为RuntimeException
和Error
)。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()
,控制权在拦截器链中逐级传递。
假设有以下拦截器链:
- 拦截器 A
- 拦截器 B
- 目标方法
调用流程如下:
- 调用
proceed()
,currentInterceptorIndex
从-1
变为0
,执行拦截器 A 的invoke()
。 - 在拦截器 A 的
invoke()
中,调用proceed()
,currentInterceptorIndex
从0
变为1
,执行拦截器 B 的invoke()
。 - 在拦截器 B 的
invoke()
中,调用proceed()
,currentInterceptorIndex
从1
变为2
。 - 此时,
currentInterceptorIndex == interceptorsAndDynamicMethodMatchers.size() - 1
,调用invokeJoinpoint()
,执行目标方法。 - 目标方法执行完毕后,逐级返回结果,最终返回给调用方。
之后我们来到了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() 方法:
调用 TransactionAttributeSource
的 getTransactionAttribute()
方法,获取当前方法的事务属性。如果事务属性不为 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)
- 原理:
- 准备阶段:协调者(Coordinator)询问所有参与者(Participant)是否可以提交事务。
- 提交阶段:如果所有参与者都同意提交,协调者通知所有参与者提交事务;否则,通知所有参与者回滚事务。
- 优点:强一致性,保证事务的原子性。
- 缺点:
- 性能较低,同步阻塞。
- 协调者单点故障。
- 网络分区时可能导致数据不一致。
(2)三阶段提交(3PC,Three-Phase Commit)
- 原理:在 2PC 的基础上增加了一个预提交阶段,减少阻塞时间。
- 优点:比 2PC 更容错。
- 缺点:实现复杂,性能仍然较低。
(3)TCC(Try-Confirm-Cancel)
- 原理:
- Try 阶段:尝试执行业务操作,预留资源。
- Confirm 阶段:确认执行业务操作,提交资源。
- Cancel 阶段:取消执行业务操作,释放资源。
- 优点:性能较高,适用于高并发场景。
- 缺点:需要业务代码实现 Try、Confirm、Cancel 逻辑,开发成本较高。
(4)本地消息表(Local Message Table)
- 原理:
- 在本地事务中插入一条消息记录。
- 通过消息队列异步通知其他服务。
- 其他服务消费消息并执行业务操作。
- 优点:实现简单,性能较高。
- 缺点:需要保证消息的可靠投递和幂等性。
(5)Saga 模式
- 原理:
- 将分布式事务拆分为多个本地事务。
- 每个本地事务执行后发布事件,触发下一个本地事务。
- 如果某个本地事务失败,则执行补偿操作回滚之前的操作。
- 优点:适用于长事务,性能较高。
- 缺点:需要实现补偿逻辑,开发成本较高。
(6)消息队列(MQ)
- 原理:
- 生产者发送消息到消息队列。
- 消费者消费消息并执行业务操作。
- 通过消息的可靠投递和幂等性保证最终一致性。
- 优点:解耦系统,性能较高。
- 缺点:需要保证消息的可靠投递和幂等性。
见后续文章-----
标签:account,return,数据库,事务,回滚,详解,提交,public From: https://www.cnblogs.com/jackjavacpp/p/18686689