news 2026/10/10 17:04:34

InnoDB事务进阶:从MVCC、锁到redo/undo log的实战内功

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
InnoDB事务进阶:从MVCC、锁到redo/undo log的实战内功

先说个我碰到的真实经历。半夜被报警叫起来,说某条 update 卡了快十分钟没执行完,开发群里已经炸了。上去看的时候,锁等待提示显示的是另一笔已经跑了二十多分钟的长事务占着行锁不放。当时第一反应是“杀事务”,但真正让我后背发凉的其实不是这一单,而是如果当时没有这个长事务,这条 update 本身会不会也有问题。处理完现场之后,我花了挺长时间把 InnoDB 事务的底层原理过了一遍,越看越觉得想把事务用明白、查得清楚,光知道 begin、commit 远远不够。

这篇内容不是写给第一次接触事务的读者,也不是那种“ACID 四特性背一遍”的入门文章。它适合已经知道多版本并发控制(MVCC)、隔离级别、redo log、undo log 这些名词,但遇到实际问题时依然觉得隔着一层纱的同学。比如隔离级别到底怎么选、死锁日志怎么看、长事务怎么定位、为什么 RR 默认还要加 gap lock,我会从原理到实操,把这些点串起来讲明白。

1. 为什么说事务是 InnoDB 最值得进阶的内功

1.1 事务四特性背后的四个“执行机构”

很多教材喜欢把 ACID 放在一个章节里讲完,但落到 InnoDB 内部,这四个特性其实分别由不同的机制在支撑,不能混为一谈。我自己习惯用一张对应表来记:

事务特性InnoDB 内部承担者一句话职责
原子性(Atomicity)undo log记录“改前像”,事务出错时把数据回滚到起点
一致性(Consistency)约束、外键、事务逻辑本身保证事务从一个合法状态到另一个合法状态
隔离性(Isolation)MVCC + 行锁让并发事务互相“看不见”不该看见的中间状态
持久性(Durability)redo log + binlog提交后即使宕机,已提交的修改也不丢

真正决定你会不会踩坑的,是后面三行:隔离性相关的 MVCC 和锁,决定了并发场景下事务行为的边界;持久性相关的 redo log,决定了性能参数怎么调。而原子性的 undo log,是 MVCC 版本链的基础物料。可以说,InnoDB 事务的内功就藏在这三块里。

1.2 进阶到底“进”在哪里

事务的基础用法很简单:begin 开始、commit 提交、rollback 回滚。但线上问题从来不会按照这个顺序出现。你遇到的往往是这样:

  • 同一个 SQL 在 A 事务里查到 90 分,在 B 事务里却查到 85 分,到底哪个是“对的”?
  • 一个普通的范围 update,为什么把其他会话的 insert 也堵住了?
  • 死锁日志里的 “LATEST DETECTED DEADLOCK” 到底怎么读?
  • 事务已经提交了,为什么主从还是对不上?
  • 一条批量 update 把同一行改了 10 万次,undo 为什么膨胀到几个 G?

这些问题没有一个能用“begin、commit”回答。它们全都落在一个问题上:InnoDB 在并发和崩溃面前,是如何设计事务的执行路径的。把这层理解透了,你才能真正做到“看报错就知道根因”。

2. 隔离级别与并发下的三大“读异常”

2.1 三种读异常是什么时候发生的

在说隔离级别之前,必须先看懂三个经典的“读异常”场景。为了不绕口,我用一个具体例子说明。

假设有一张学生成绩表,某一行的分数初始是 90 分。现在有两个事务同时操作这行。

脏读:事务 A 把分数改成 80,还没提交;事务 B 这时读,读到了 80。如果 A 回头又回滚了,那 B 刚刚读到的是一个“从未真正存在过”的数据。写错一步棋又撤回来,旁边看棋的人已经把这一步当成了既定事实。脏读的本质是读到了未提交数据。

不可重复读:事务 B 先读了一次,分数是 90;随后事务 A 把分数改成了 80 并提交;B 在同一事务里再读一次,变成了 80。B 在两次读取之间数据发生了变化,这不符合“一个事务内多次读取结果应该一致”的直觉。

幻读:事务 B 查询某个范围的记录,第一次查出 10 条;事务 A 往这个范围插入了一条新记录并提交;B 再次查询时变成 11 条。“多出来一行”就像幻觉一样。注意,幻读针对的是“插入新行”,而不是修改已有行。

这三种异常,从后果严重性来看,脏读最不可接受,因为它会导致基于错误数据的后续决策;不可重复读会破坏事务内的一致性体验;幻读则更隐蔽,它往往影响范围统计、分页、批量操作这类 SQL。

2.2 四种隔离级别怎么选

标准 SQL 定义了四种隔离级别,InnoDB 也支持这四种。区别就在于允许哪种异常发生:

隔离级别脏读不可重复读幻读InnoDB 默认实现
读未提交允许允许允许基本不用,竞态极多
读提交禁止允许允许很多业务的实际选择
可重复读禁止禁止基本禁止(靠锁补)Java 生态默认,MySQL 默认
串行化禁止禁止禁止并发极低,仅少数场景

注意表格里“基本禁止”的措辞。在标准 SQL 的定义里,可重复读是允许幻读发生的。但 InnoDB 做了增强:在可重复读级别下,快照读靠 MVCC 保证一次快照内看不到新插入的数据,当前读靠 next-key lock 锁住范围,阻止其他事务往范围内插入。所以你看标准定义,幻读在 RR 下其实是被解决了的;但也因为这个增强,RR 下的范围和间隙锁带来了比读提交更大的锁竞争。

2.3 为什么 MySQL 的默认隔离级别偏偏是 RR

这个问题挺多人问过。从纯数据库语义角度讲,读提交(RC)往往更符合“读到的都是已提交数据”的直觉,也更好理解。那为什么 MySQL 默认选了 RR?

根源在历史包袱和主从复制的一致性设计上。早期 binlog 使用 statement 格式时,主库上执行的是一条 SQL 原文,从库也是按这条 SQL 原文重新在从库数据上执行。如果主库在 RC 级别下运行,某条范围更新在主库执行过程中,其他会话插入的新行是否被这条更新“扫到”,是随着执行时间的推进而变化的;到了从库重放时,数据分布可能已经不同,重放结果就和主库不一致。而 RR 级别下 next-key lock 会把范围锁死,保证主库执行期间范围内没有新记录进来,从而保证 statement 在从库重放时也能得到同样结果。

后来 binlog 逐渐以 row 格式为主流,row 格式只记录变更前后的行内容,不再依赖 SQL 语义,所以严格说,新版本 MySQL 把默认隔离级别改成 RC 也不会像早年那样容易出主从不一致。但出于兼容性和历史惯性,默认依然是 RR。了解这段背景,你在决定“要不要把系统切到 RC”时,就知道至少要确认 binlog 格式是 row,而不是盲目切换。

3. 多版本并发控制:事务隔离看不见的秘密

3.1 版本链是怎么搭起来的

MVCC 的核心思想,是给每一行保留多个历史版本,让读操作可以通过“历史版本”来避开未提交的修改。InnoDB 实现这一点,依赖的是每行记录的三个隐藏列:

隐藏列含义
DB_TRX_ID最近一次修改该行的事务 ID
DB_ROLL_PTR回滚指针,指向 undo log 里该行的上一个版本
DB_ROW_ID行 ID,没有主键时它充当隐藏主键

当你更新一条记录时,InnoDB 并不是直接改掉原值,而是先把当前行的旧值写入 undo log,然后修改当前行并更新 DB_TRX_ID 和 DB_ROLL_PTR。于是同一行的多个版本就从旧到新串成了一条链表,HEAD 是最新版本,往回走就能看到历史版本。这个“链”就是 undo 版本链。

这里有个疑问:既然已经有了 undo log,为什么 MVCC 还要顺着链查历史版本?因为 undo log 不仅要给事务回滚用,还要承担“查询时提供旧版本”的职责。两者只是同一份数据的两种消费场景。所以你会发现,回滚和快照读共享同一套链路。

3.2 ReadView:事务的快照“眼镜”

版本链一直摆在那里,但一个事务在查询时,哪些版本能看到,哪些版本不能看到,需要一个判断机制。这个判断机制就是 ReadView,你可以把它理解成事务在某个时间点给自己戴上的“眼镜”。

ReadView 的核心内容可以简化成四个字段:

  • m_creator_trx_id:创建 ReadView 的事务 ID,也就是“我自己”
  • m_up_limit_id:生成快照时,当前活跃事务列表中的最小事务 ID
  • m_low_limit_id:生成快照时,系统中下一个将要分配的事务 ID
  • m_active_ids:生成快照时,所有还在活跃(未提交)的事务 ID 集合

判断某个版本是否可见,我习惯用一个简化但方向正确的伪代码来记:

if trx_id == m_creator_trx_id: return visible # 自己改的必然可见 if trx_id < m_up_limit_id: return visible # 活跃列表之前已提交的事务,可见 if trx_id >= m_low_limit_id: return invisible # 生成快照后才新开启的事务,不可见 if trx_id in m_active_ids: return invisible # 生成快照时还在跑的事务,不可见 return visible # 剩下情况:非活跃但ID比up_limit大,可见

用一个类比更容易记:ReadView 就是你按下快门的一瞬间,系统里那些“正在写作业的人”名单。名单上的人,就算写完了你也不能看到他们的成果;名单外的人,已经交卷的可以看到;快门之后才入场的人,一律看不到。

3.3 RC 和 RR 的 ReadView 差异,决定了不可重复读

MVCC 的细节都是表象,真正影响你业务感受的是:ReadView 到底在什么时刻生成。

在 RC 隔离级别下,每次普通 SELECT 都会生成一个新的 ReadView。所以如果事务 A 提交了你正在读取的那行修改,你下一次 SELECT 时,新的 ReadView 里已经不再包含 A 的事务 ID,于是你就能看到新值。这就是不可重复读出现的直接原因——不是数据变了,而是你每次查询都换了副新眼镜。

在 RR 隔离级别下,ReadView 在事务第一次执行 SELECT 时生成,之后整个事务期间一直复用。哪怕其他事务提交了修改,你的 ReadView 里依然保留着当初的活跃事务列表,那些后面提交的内容对你看来说依然是“活跃状态”,查不到。这就是 RR 能避免不可重复读的原因。

我拿一个具体例子演示一下。假设成绩表一行分数是 90 分,事务 A(事务 ID=100)把它改成 85 分但未提交,事务 B(事务 ID=101)发起普通 SELECT。

  • B 生成 ReadView,m_active_ids = [100, 101](简化起见都算活跃),m_up_limit_id=100,m_low_limit_id=102。
  • 当前行最新版本是事务 100 改的,trx_id=100,在活跃列表内,不可见。
  • B 顺着回滚指针找到上一个版本,对应的事务 ID 很小,是已提交的,小于 m_up_limit_id,可见。
  • B 最终读到 90 分。

如果 A 随后提交,B 在 RC 下再查一次,新 ReadView 里已经没有 100 了,于是读到 85 分;B 在 RR 下再查,还是旧快照,依然读到 90 分。这个差异就是两个隔离级别的分水岭。

3.4 快照读与当前读:两条完全不同的执行路径

普通 SELECT 走的是快照读,不加锁,靠 MVCC 读历史版本。而 UPDATE、DELETE、INSERT,以及带 FOR UPDATE、LOCK IN SHARE MODE 的 SELECT,走的是当前读,必须读最新版本,并且对涉及的行加锁。

这个区分非常关键。你会发现一个现象:在 RR 下,一个事务里用 UPDATE 修改某行后,再普通 SELECT 查同一行,是可以看到自己修改后的值的。因为 trx_id == m_creator_trx_id,自己改的版本对自己永远可见。但如果让另一个事务来查,它看不到你这行未提交的新值,只能看到旧版本。两条路径各管各的,互不干扰。

理解快照读和当前读,也是后面理解锁机制的前提。快照读解决的是普通查询的并发能力,当前读解决的是写操作的一致性和互斥。

4. 锁机制:并发控制的下半场

4.1 从行锁到间隙锁,锁的粒度如何影响幻读

MVCC 解决了快照读的隔离问题,但当前读(UPDATE、DELETE、INSERT)不能靠快照解决,必须用锁。InnoDB 的锁有表级和行级,真正需要花心思的是行级锁。

行级锁按照对写操作的兼容性,分为共享锁(S Lock)和排他锁(X Lock),规则很简单:读读不互斥,读写互斥,写写互斥。但行锁并不是只有“锁住一行”这一种形态。InnoDB 的行锁一共分四种:

锁类型作用触发场景示例
记录锁锁定一条具体记录WHERE id = 10
间隙锁锁定一个区间,禁止区间内插入范围查询命中后,会锁住记录之间的空隙
next-key lock记录锁 + 间隙锁的组合,锁住记录本身及其前方的间隙范围查询在 RR 级别下默认走这种锁
插入意向锁多个事务想插入同一区间时的协调队列,表示“我准备向这个间隙插入”INSERT 碰到被锁的间隙时触发

在 RR 级别下,InnoDB 对普通索引的范围查询默认下 next-key lock。举个例子,你执行DELETE FROM orders WHERE id >= 100 AND id <= 130。除了把 id 在 100 到 130 之间的记录锁住外,它还会锁住 100 之前的间隙和 130 之后的间隙,让其他事务无法在 id=99、id=131 等相邻位置插入会落进这个范围内的新行。这样就能保证在执行期间,范围内不会“多出”一行。这正是解决幻读的手段。

而在 RC 级别下,InnoDB 只保留记录锁,不保留间隙锁。如果业务数据插入频繁,RC 的并发写入能力往往比 RR 好,代价是会出现幻读。标准 SQL 里的 RC 本来就不承诺防幻读,所以这个取舍是合理的。

4.2 当前读会等锁,等锁也可能等出死锁

锁为什么会导致死锁?死锁发生的四个必要条件在 InnoDB 里都能找到对应场景:互斥(同一行不能同时持有 X 锁)、持有并等待(事务拿着 A 行锁去要 B 行锁)、不可剥夺(锁不能强制抢走)、循环等待(T1 等 T2,T2 又等 T1)。

最常见的死锁场景是两条 SQL 以不同顺序更新同一组记录。比如 T1 先更新 id=1,再更新 id=2;T2 先更新 id=2,再更新 id=1。当两者各持一行的锁时,谁也没法继续,就形成了闭环。

查死锁信息,第一反应是执行SHOW ENGINE INNODB STATUS\G,在输出的 LATEST DETECTED DEADLOCK 段里会看到两个事务各自持有哪些锁、等待哪些锁、最终回滚了谁。不过我建议再配合两个操作:

  1. 把锁信息补全,开启参数后死锁日志里会显示具体锁对象:
SET GLOBAL innodb_status_output_locks = ON;
  1. 用下面这条 SQL 查当前正在发生的锁等待,比直接看 status 输出更快定位到具体线程:
SELECT r.trx_id AS waiting_trx_id, r.trx_mysql_thread_id AS waiting_thread, r.trx_query AS waiting_query, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, b.trx_query AS blocking_query FROM information_schema.innodb_trx r JOIN information_schema.innodb_lock_waits w ON r.trx_id = w.requesting_trx_id JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id;

不同 MySQL 版本的information_schema字段名会有点差异,建议在测试环境先跑一遍确认列名。定位到阻塞线程后,再结合应用日志看这个事务到底做了什么,而不是立刻 kill。误杀一个长时间跑批的事务,代价可能更大。

4.3 死锁复盘与预防的实操套路

死锁既然要“排”,更要“防”。把死锁率降下来,核心是控制事务在锁上的时间窗口和顺序:

  • 缩小事务粒度。一个事务里只放必须的操作,查询类操作尽量放到事务外。
  • 统一加锁顺序。批量更新时按主键 ID 排序后再执行,让所有事务都按同一个顺序拿锁。比如要更新 id in (1,2,3),更新语句写成:
UPDATE t SET col = 'new' WHERE id IN (1,2,3);

避免一个事务先更新 1 再更新 3,另一个事务先更新 3 再更新 1。

  • 减少当前读的范围。能用主键精确命中的不要用范围条件,少加间隙锁。
  • 关注innodb_lock_wait_timeout,超时时间设太大会延长故障感知,设太小会导致正常等待的事务大量报错。一般 10 到 30 秒之间比较合理。
  • 死锁发生后的处理不要只靠重试。应用层捕获死锁错误码(MySQL 错误码 1213)后,重试整个事务一到两次没问题,但如果频繁出现,说明不是运气问题,而是事务设计本身有缺陷,需要从上一步的锁顺序和事务粒度上找原因。

5. redo log 与 undo log:事务的“账本”和“后悔药”

5.1 WAL 为什么能让数据库更快

事务提交后要保证持久性,最简单粗暴的做法是每次提交都把脏页刷到磁盘。但随机 IO 太贵,InnoDB 选择了另一条路:先写日志,后落数据。这就是 WAL(Write-Ahead Logging)。

配合 WAL 的日志就是 redo log。redo log 记录的是“对某个页做了某个修改”这种物理层面的操作,比如把偏移量 8192 处写入一个数值。它本质上是为崩溃恢复准备的“重放账本”:只要账本上记了“提交”,就算数据页还没来得及写磁盘,启动时也能把账重演一遍,恢复到提交时的状态。

这个取舍非常划算。把每次提交的随机小 IO 换成日志的顺序写 IO,大批量写入时的性能提升可以到数量级。你可以理解成“先记账,后结账”:饭店里顾客点单,服务员不可能每点一桌就把账算完落库,而是先记在小本子上,打烊后统一结算;小本子就是 redo log,每天最后结账的结果才是最终的磁盘数据。

5.2 undo log 如何支撑回滚和 MVCC

undo log 和 redo log 完全相反,它记录的是“修改前”的数据。当事务需要回滚时,顺着 undo log 把旧值恢复回去即可。前面说过,undo log 还给 MVCC 提供了历史版本,回滚和快照读都靠它。

有个细节容易被忽略:undo log 本身也需要持久化。因为事务回滚可能发生在宕机重启之后,如果 undo 丢失,重启后没法判断一个还没提交的事务应该回滚到哪里。这也是整个 InnoDB 崩溃恢复流程的一部分:先恢复 redo,再根据情况处理未提交事务。

RR 隔离级别下如果长期存在大查询,旧的 undo 版本链一直无法被清理,undo log 文件会持续膨胀。这也是长事务治理里面一个很重要的指标,后面实操部分会再提。

5.3 redo log 与 binlog 的两阶段提交

redo log 是 InnoDB 层面的日志,binlog 是 MySQL Server 层面的日志。两者服务于不同目的:redo log 保证存储引擎崩溃后不丢已提交数据,binlog 负责主从复制和基于时间点的恢复。同一个事务要同时写这两份日志,就要保证它们的一致性,否则崩溃后主从数据就会错乱。

解决方式是两阶段提交。流程可以简化成三步:

  1. 事务提交时,先把 redo log 写入并处于 prepare 状态;
  2. 再写 binlog,binlog 写入成功后;
  3. 把 redo log 标记为 commit 状态。

崩溃恢复时的判断也很简洁:

redo 状态binlog 是否完整恢复动作
commit任意直接提交该事务
prepare已写入且完整提交该事务
prepare未写入或不完整回滚该事务

规则的本质是:binlog 完整了才允许提交,因为从库要靠 binlog 恢复,主库如果回滚而从库执行了,两边就不一致;binlog 缺失则必须回滚,因为从库没有这笔记录,主库提交了同样不一致。

5.4 三个日志刷盘参数,线上怎么配

日志写到内存之后什么时候落盘,取决于参数。最常调的一个是innodb_flush_log_at_trx_commit,取值范围 0、1、2:

取值行为可靠性适用场景
0每秒刷一次 redo log,事务提交不主动刷最弱,最多丢一秒提交记录能容忍丢数据的缓存类场景,不建议生产核心库用
1每次事务提交都刷盘最稳,不丢已提交事务默认值,金融/订单等核心业务
2每次事务提交写到 OS 缓存,每秒刷盘数据库进程崩溃不丢,主机断电可能丢一秒对性能要求高且能接受极端场景丢数据的业务

innodb_log_buffer_size决定 redo log buffer 大小,一般默认值足够,除非有大量并发大事务,需要调大。另一个容易忽略的是innodb_redo_log_capacity,MySQL 8.0.30 之后取代了原来的 log file 数量配置。日常维护要关注 redo log 的使用率,如果持续较高,说明日志产生速度大于刷盘能力,需要结合慢 SQL、大事务一起排查,而不是盲目调大容量。

6. 真实场景里的“事务素养”:三个很典型的开发与运维坑

6.1 统计报表把 RR 换成 RC,性能为什么提升明显

我之前帮一个统计类服务调整过隔离级别。服务本身只读历史数据,按时间范围做聚合,但因为它跑在默认的 RR 下,每条普通 SELECT 生成了快照后,历史版本链上积压了大量未清理的 undo 数据;同时范围查询在 RR 下要加间隙锁,多个定时任务一旦重叠执行,锁等待频繁。最直观的表现就是慢日志里全是几秒钟的大型 SELECT。

当时的处理是把这个只读服务调整为 RC。原因很直接:只读场景不需要 RR 那种多次读取结果不变的保证,每次查询只要读到“当前已提交的最新数据”就足够。RC 下不放间隙锁,写入端的并发能力和整体的查询 RT 都上来了。这里有个前提,binlog 必须是 row 格式。MySQL 5.7 以后默认就是 row,5.6 环境需要确认。

这不是说 RC 就比 RR 好。核心业务如果存在一个事务内对同一数据多次读取并做一致性判断的需求,比如“先查订单金额,再按这个金额做扣减”这种逻辑,那 RR 的稳定性价值就体现出来了。关键是根据场景选,不要一条配置走到黑。

6.2 库存扣减的防超卖,SQL 到底该怎么写

库存扣减是并发事务最典型的场景。最容易出问题的写法是先查再改:

SELECT stock FROM inventory WHERE sku_id = 'A'; -- 业务层判断 stock > 0 UPDATE inventory SET stock = stock - 1 WHERE sku_id = 'A';

两步之间如果有两个请求同时查到 stock=1,两个都判断“大于 0”,然后都执行扣减,库存就变成负数了。改成一条 SQL 就好很多:

UPDATE inventory SET stock = stock - 1 WHERE sku_id = 'A' AND stock > 0;

这里的 UPDATE 是当前读,会直接对命中的行加 X 锁。第二个事务执行同一条 SQL 时,必须等第一个事务提交后才能继续,并且重新判断stock > 0,所以不会超卖。执行后检查影响行数,等于 0 就说明没抢到。

更通用的做法是乐观锁:

UPDATE inventory SET stock = stock - 1 WHERE sku_id = 'A' AND version = 1;

先查 version,更新时把 version 作为条件。如果失败则重查重试。但注意乐观锁在超高并发下会产生不少无效重试,库存这类短事务场景,直接用条件更新 SQL 通常更合适。

6.3 大事务怎么识别,怎么拆分

大事务是 InnoDB 事务事故里最隐蔽的一类。事务启动后长时间不提交,可能表现为锁等待、undo 膨胀、从库延迟。判断一个事务是不是“大”,不能只看它修改了多少行,还要看它持续多久、持有锁多久。

查询当前所有未提交事务,最直接的方式:

SELECT trx_id, trx_state, trx_started, TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS run_seconds, trx_rows_modified, trx_query FROM information_schema.innodb_trx\G

看到run_seconds超过几秒甚至几十秒的,就要警惕。trx_rows_modified很大的,说明 undo 里积压了大量版本,提交后可能产生大段清理动作。在实际项目里,我遇到过因为一个循环里每轮迭代都开了事务但循环外层一直没 commit,导致总共改了 50 万行、事务跑了 40 分钟的情况。从库延迟超过几个小时,处理起来相当痛苦。

拆分的思路通常是:把一个大批量改成多个小批次,每个批次独立提交。比如要更新 100 万行,别在一条 UPDATE 里全干,可以按主键范围分段领取,每处理 5000 行提交一次。这样既不让单事务持有锁太久,也不会因为单个事务回滚造成长时间不可用。

6.4 一切锁问题的源头,多数还是事务边界不清晰

最后再提一个观察。很多锁等待和死锁,表面看是 SQL 写的不好,实际是事务边界没划清楚。最常见的问题是:在事务里写了耗时操作,比如调用外部 HTTP 接口、循环调用其他微服务,把未知延迟引进了数据库事务的锁生命周期里。一个事务里几毫秒的数据库操作本身没什么,但一个 HTTP 等待 3 秒,事务就持有了 3 秒的锁,这时候再来一个并发事务要改同一行,就只能等着。

我的习惯是:事务开始前先明确三个问题——这个事务要跑多久?要碰多少行?会不会和别的操作在锁上冲突?如果有任何一个问题是模糊的,就应该先把事务缩小,把耗时操作挪出去。把这些边界想清楚,大部分事务事故在开发阶段就能拦住。工具方法都放在上面了,遇到问题往这几个方向去查,基本都能找到根因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 17:01:38

Rufus制作U盘启动盘完整教程:从配置到绕过TPM检测

1. 为什么还要折腾U盘启动盘很多人觉得现在装系统已经不需要U盘了&#xff0c;直接在线升级或者用恢复分区就能搞定。但实际干过运维或者经常帮人处理电脑问题的人都知道&#xff0c;U盘启动盘依然是绕不开的刚需。系统崩溃进不去桌面、新买的硬盘是空的、需要给多台机器批量部…

作者头像 李华
网站建设 2026/10/10 16:49:24

8款降AI率工具实测与完整改写链路,论文检测从99%降到5%

去年有段时间我的论文审稿卡在了降AI率这一步&#xff0c;某主流检测系统反复给出90%以上结果&#xff0c;试过很多改法都没用。后来陆续花了半个多月&#xff0c;把市面上口碑比较热的工具挨个试了一遍&#xff0c;前前后后测了几十篇文本片段&#xff0c;最后总算摸清了每种工…

作者头像 李华
网站建设 2026/10/10 16:49:23

Unity跑酷小游戏源工程拆解:从跑通到性能优化

简介&#xff1a;这是一份可以直接在Unity编辑器中打开运行的跑酷小游戏完整工程&#xff0c;目标读者是刚接触Unity不久、希望从零理解横版跑酷玩法实现的初学者&#xff0c;也适合正在准备小型游戏作品集的开发者参考&#xff1b;工程覆盖了角色在前行道路上不断奔跑、跳跃、…

作者头像 李华
网站建设 2026/10/10 16:49:21

OpenHarmony上Flutter本地存储方案:从选型到避坑实践

1. 本地存储在资讯类App里的位置&#xff1a;比想象中更重要做“今日资讯”这类App做到第二十六期&#xff0c;功能层面基本已经齐了&#xff1a;列表、详情、分类、收藏、搜索都跑通了。这时候如果不做本地存储&#xff0c;用户每次冷启动都要等网络请求回来才能看到内容&…

作者头像 李华
网站建设 2026/10/10 16:48:57

进程间通信实战:从管道到共享内存的选型与避坑指南

进程间通信&#xff08;IPC&#xff09;这个题目&#xff0c;几乎每个写后台服务的开发者都会碰到。刚入行那会儿我觉得这玩意儿不就是几个API嘛&#xff0c;管道、共享内存、消息队列&#xff0c;背一背就能应付面试。直到在一个某跨平台系统的数据分发项目里被进程间的同步问…

作者头像 李华