数据库事务详解

事务-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 {@Autowiredprivate AccountRepository accountRepository;@Transactionalpublic 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 {};//事务的传播行为,默认是REQUIREDPropagation 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@ConditionalOnMissingBeanpublic 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> {
.........@Overrideprotected 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@Nullablepublic Object invoke(MethodInvocation invocation) throws Throwable {Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {@Override@Nullablepublic Object proceedWithInvocation() throws Throwable {return invocation.proceed();}@Overridepublic Object getTarget() {return invocation.getThis();}@Overridepublic 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); // 将这里打上断点,debugreturn 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@Nullablepublic Object proceedWithInvocation() throws Throwable {return invocation.proceed();}@Overridepublic Object getTarget() {return invocation.getThis();}@Overridepublic 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方法-----starttry{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 {@Overridepublic boolean isCandidateClass(Class<?> targetClass) {return AnnotationUtils.isCandidateClass(targetClass, Transactional.class);}
}

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

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

// BeanFactoryTransactionAttributeSourceAdvisor
public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {@Nullableprivate TransactionAttributeSource transactionAttributeSource;private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {@Override@Nullableprotected TransactionAttributeSource getTransactionAttributeSource() {return transactionAttributeSource;}};
}// TransactionAttributeSourcePointcut
private static final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {@Overridepublic 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.rhkb.cn/news/5807.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

攻防世界GFSJ1012 pwnstack

题目编号&#xff1a;GFSJ1012 附件下载后是一个c和库文件&#xff1a; 获取在线场景是 1. 获取伪代码 Exeinfo打开pwn2&#xff0c;分析如图&#xff0c;64位。 IDA Pro(64-bit)打开pwn2&#xff0c;生成伪代码 2. 分析代码漏洞 /* This file was generated by the Hex-Rays …

最小距离和与带权最小距离和

1. 等权中位数 背景&#xff1a; 给定一系列整数&#xff0c;求一个整数x使得x在数轴上与所有整数在数轴上的距离和最小。 结论&#xff1a; 这一系列的整数按顺序排好后的中位数(偶数个整数的中位数取 n 2 或 n 2 1 \frac{n}{2}或\frac{n}{2}1 2n​或2n​1都可)一定是所求点…

【LeetCode 刷题】栈与队列-队列的应用

此博客为《代码随想录》栈与队列章节的学习笔记&#xff0c;主要内容为队列的应用相关的题目解析。 文章目录 239. 滑动窗口最大值347. 前 K 个高频元素 239. 滑动窗口最大值 题目链接 class Solution:def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]…

【优选算法】5----有效三角形个数

又是一篇算法题&#xff0c;今天早上刚做的热乎的~ 其实我是想写博客但不知道写些什么&#xff08;就水一下啦&#xff09; -------------------------------------begin----------------------------------------- 题目解析: 这道题的题目算是最近几道算法题里面题目最短的&a…

Golang:使用DuckDB查询Parquet文件数据

本文介绍DuckDB查询Parquet文件的典型应用场景&#xff0c;掌握DuckDB会让你的产品分析能力更强&#xff0c;相反系统运营成本相对较低。为了示例完整&#xff0c;我也提供了如何使用Python导出MongoDB数据。 Apache Parquet文件格式在存储和传输大型数据集方面变得非常流行。最…

HTTP 配置与应用(局域网)

想做一个自己学习的有关的csdn账号&#xff0c;努力奋斗......会更新我计算机网络实验课程的所有内容&#xff0c;还有其他的学习知识^_^&#xff0c;为自己巩固一下所学知识&#xff0c;下次更新HTTP 配置与应用&#xff08;不同网段&#xff09;。 我是一个萌新小白&#xf…

免费!无水印下载!

软件介绍 这个工具可方便啦&#xff0c;不管是小红书上那些时尚的美照&#xff0c;还是特别搞笑的视频&#xff0c;只要你想下载&#xff0c;轻轻一点就能保存。真的是实现了一键下载&#xff0c;完全没有复杂的操作。下载下来的内容会智能分类呢。这样的话&#xff0c;你的资源…

第二届国赛铁三wp

第二届国赛 缺东西去我blog找&#x1f447; 第二届长城杯/铁三 | DDLS BLOG web Safe_Proxy 源码题目 from flask import Flask, request, render_template_stringimport socketimport threadingimport htmlapp Flask(__name__)app.route(/, methods"GET"])de…

【深度学习】嘿马深度学习笔记第11篇:卷积神经网络,学习目标【附代码文档】

本教程的知识点为&#xff1a;深度学习介绍 1.1 深度学习与机器学习的区别 TensorFlow介绍 2.4 张量 2.4.1 张量(Tensor) 2.4.1.1 张量的类型 TensorFlow介绍 1.2 神经网络基础 1.2.1 Logistic回归 1.2.1.1 Logistic回归 TensorFlow介绍 总结 每日作业 神经网络与tf.keras 1.3 …

STranslate 中文绿色版即时翻译/ OCR 工具 v1.3.1.120

STranslate 是一款功能强大且用户友好的翻译工具&#xff0c;它支持多种语言的即时翻译&#xff0c;提供丰富的翻译功能和便捷的使用体验。STranslate 特别适合需要频繁进行多语言交流的个人用户、商务人士和翻译工作者。 软件功能 1. 即时翻译&#xff1a; 文本翻译&#xff…

缓存之美:万文详解 Caffeine 实现原理(下)

上篇文章&#xff1a;缓存之美&#xff1a;万文详解 Caffeine 实现原理&#xff08;上&#xff09; getIfPresent 现在我们对 put 方法有了基本了解&#xff0c;现在我们继续深入 getIfPresent 方法&#xff1a; public class TestReadSourceCode {Testpublic void doRead() …

GPT 结束语设计 以nanogpt为例

GPT 结束语设计 以nanogpt为例 目录 GPT 结束语设计 以nanogpt为例 1、简述 2、分词设计 3、结束语断点 1、简述 在手搓gpt的时候&#xff0c;可能会遇到一些性能问题&#xff0c;即关于是否需要全部输出或者怎么节约资源。 在输出语句被max_new_tokens 限制&#xff0c…

HackTheBox靶机:Sightless;NodeJS模板注入漏洞,盲XSS跨站脚本攻击漏洞实战

HackTheBox靶机&#xff1a;Sightless 渗透过程1. 信息收集常规探测深入分析 2. 漏洞利用&#xff08;CVE-2022-0944&#xff09;3. 从Docker中提权4. 信息收集&#xff08;michael用户&#xff09;5. 漏洞利用 Froxlor6. 解密Keepass文件 漏洞分析SQLPad CVE-2022-0944 靶机介…

XML外部实体注入--XML基础

一.XML基础 1.XML 基础概念 定义&#xff1a;XML 即可扩展标记语言&#xff08;Extensible Markup Language&#xff09;&#xff0c;用于标记电子文件&#xff0c;使其具有结构性。它是一种允许用户对自己的标记语言进行定义的源语言&#xff0c;可用来标记数据、定义数据类型…

YOLOv8改进,YOLOv8检测头融合DSConv(动态蛇形卷积),并添加小目标检测层(四头检测),适合目标检测、分割等

精确分割拓扑管状结构例如血管和道路,对各个领域至关重要,可确保下游任务的准确性和效率。然而,许多因素使任务变得复杂,包括细小脆弱的局部结构和复杂多变的全局形态。在这项工作中,注意到管状结构的特殊特征,并利用这一知识来引导 DSCNet 在三个阶段同时增强感知:特征…

Flutter:自定义Tab切换,订单列表页tab,tab吸顶

1、自定义tab切换 view <Widget>[// 好评<Widget>[TDImage(assetUrl: assets/img/order4.png,width: 36.w,height: 36.w,),SizedBox(width: 10.w,),TextWidget.body(好评,size: 24.sp,color: controller.tabIndex 0 ? AppTheme.colorfff : AppTheme.color999,),]…

深度学习笔记——循环神经网络RNN

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍面试过程中可能遇到的循环神经网络RNN知识点。 文章目录 文本特征提取的方法1. 基础方法1.1 词袋模型&#xff08;Bag of Words, BOW&#xff09;工作原…

nvm版本安装

安装 使用切换 MySQL5.7新安装 熟人命令 8.0 mysql -P3306 -uroot -p5.7 mysql -P3307 -uroot -p 记得用完关闭

人工智能之深度学习_[4]-神经网络入门

文章目录 神经网络基础1 神经网络1.1 神经网络概念1.1.1 什么是神经网络1.1.2 如何构建神经网络1.1.3 神经网络内部状态值和激活值 1.2 激活函数1.2.1 网络非线性因素理解1.2.2 常见激活函数1.2.2.1 Sigmoid 激活函数1.2.2.2 Tanh 激活函数1.2.2.3 ReLU 激活函数1.2.2.4 SoftMa…

一文大白话讲清楚webpack基本使用——11——chunkIds和runtimeChunk

文章目录 一文大白话讲清楚webpack基本使用——11——chunkIds和runtimeChunk1. 建议按文章顺序从头看&#xff0c;一看到底&#xff0c;豁然开朗2. 啥是chunkIds3.怎么使用chunkIds4. 啥是runtimeChunk5. 怎么使用runtimeChunk 一文大白话讲清楚webpack基本使用——11——chun…