【计算基础|MySQL11】事务与隔离级别
转账这个例子谁都听过:A 给 B 转 100 块,要扣 A 的钱、加 B 的钱,两步要么都成、要么都不成------不能扣了 A 的钱、B 却没收到。这就是事务。但真正难的是:两个事务同时改同一条数据时会发生什么?脏读、不可重复读、幻读到底怎么区分?为什么 MySQL 默认隔离级别下,别的事务都提交了,你在事务里还能读到旧值?
这一篇把这些问题在 MySQL 8.0 上用两个真实会话跑给你看。机制图用 Mermaid,结尾配面试高频。
环境:Docker MySQL 8.0.46,InnoDB,默认隔离级别
REPEATABLE-READ,自动提交autocommit=1。
一、什么是事务
事务就是一组 SQL 语句的执行单元,要么全成功、要么全失败回滚。
sql
START TRANSACTION; -- 或 BEGIN,开启事务
UPDATE account SET balance=balance-100 WHERE id=1; -- A扣100
UPDATE account SET balance=balance+100 WHERE id=2; -- B加100
COMMIT; -- 全成功才提交
-- 中间任何一步出错,执行 ROLLBACK; 撤销整个事务
⚠️ 两个新手必踩的点:
- MySQL 默认
autocommit=ON,意味着你每条 SQL 自动是一个事务,写完就提交。不显式开事务,多条语句是不原子的。 CREATE/ALTER/DROP这类 DDL 语句会隐式提交 当前事务,且 DDL 本身不能回滚。别指望BEGIN; DROP TABLE t; ROLLBACK;能救回来。
sql
SELECT @@autocommit; -- 1 = 自动提交开
SELECT @@transaction_isolation; -- REPEATABLE-READ(默认)
二、ACID:四个特性各自靠什么实现
事务的四个特性是面试必问,但更值得问的是每个特性底层靠什么实现。结论先列:
#mermaid-svg-nK0qybgMqb0Tudy8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nK0qybgMqb0Tudy8 .error-icon{fill:#552222;}#mermaid-svg-nK0qybgMqb0Tudy8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nK0qybgMqb0Tudy8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nK0qybgMqb0Tudy8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nK0qybgMqb0Tudy8 .marker.cross{stroke:#333333;}#mermaid-svg-nK0qybgMqb0Tudy8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nK0qybgMqb0Tudy8 p{margin:0;}#mermaid-svg-nK0qybgMqb0Tudy8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 .cluster-label text{fill:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 .cluster-label span{color:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 .cluster-label span p{background-color:transparent;}#mermaid-svg-nK0qybgMqb0Tudy8 .label text,#mermaid-svg-nK0qybgMqb0Tudy8 span{fill:#333;color:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 .node rect,#mermaid-svg-nK0qybgMqb0Tudy8 .node circle,#mermaid-svg-nK0qybgMqb0Tudy8 .node ellipse,#mermaid-svg-nK0qybgMqb0Tudy8 .node polygon,#mermaid-svg-nK0qybgMqb0Tudy8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nK0qybgMqb0Tudy8 .rough-node .label text,#mermaid-svg-nK0qybgMqb0Tudy8 .node .label text,#mermaid-svg-nK0qybgMqb0Tudy8 .image-shape .label,#mermaid-svg-nK0qybgMqb0Tudy8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-nK0qybgMqb0Tudy8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nK0qybgMqb0Tudy8 .rough-node .label,#mermaid-svg-nK0qybgMqb0Tudy8 .node .label,#mermaid-svg-nK0qybgMqb0Tudy8 .image-shape .label,#mermaid-svg-nK0qybgMqb0Tudy8 .icon-shape .label{text-align:center;}#mermaid-svg-nK0qybgMqb0Tudy8 .node.clickable{cursor:pointer;}#mermaid-svg-nK0qybgMqb0Tudy8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nK0qybgMqb0Tudy8 .arrowheadPath{fill:#333333;}#mermaid-svg-nK0qybgMqb0Tudy8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nK0qybgMqb0Tudy8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nK0qybgMqb0Tudy8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nK0qybgMqb0Tudy8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nK0qybgMqb0Tudy8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nK0qybgMqb0Tudy8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nK0qybgMqb0Tudy8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nK0qybgMqb0Tudy8 .cluster text{fill:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 .cluster span{color:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nK0qybgMqb0Tudy8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nK0qybgMqb0Tudy8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-nK0qybgMqb0Tudy8 .icon-shape,#mermaid-svg-nK0qybgMqb0Tudy8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nK0qybgMqb0Tudy8 .icon-shape p,#mermaid-svg-nK0qybgMqb0Tudy8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nK0qybgMqb0Tudy8 .icon-shape .label rect,#mermaid-svg-nK0qybgMqb0Tudy8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nK0qybgMqb0Tudy8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nK0qybgMqb0Tudy8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nK0qybgMqb0Tudy8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原子性 Atomicity
undo log
回滚日志
一致性 Consistency
由 A + I + D 共同保证
隔离性 Isolation
锁 + MVCC
持久性 Durability
redo log
重做日志
| 特性 | 含义 | 实现机制 |
|---|---|---|
| 原子性 Atomicity | 事务里的 SQL 要么全做要么全不做 | undo log:记录修改前的旧值,回滚时用它复原 |
| 一致性 Consistency | 数据从一个一致状态转到另一个一致状态 | 目的,由原子性/隔离性/持久性共同保障 |
| 隔离性 Isolation | 并发事务互不干扰 | 锁 + MVCC(下一篇详讲 MVCC) |
| 持久性 Durability | 提交后即使宕机也不丢 | redo log:提交前先把修改记到重做日志,崩溃后重放 |
🔴 一句话记:原子性靠 undo log(后悔药),持久性靠 redo log(存档点),隔离性靠锁+MVCC,一致性是最终目标。undo/redo 这两对日志后续日志篇会专门展开。
三、并发事务会出什么问题
两个事务同时读写,按严重程度有三种现象。用账户表 account(id, balance) 举例,初始 id=1 余额 100。
3.1 脏读(Dirty Read)
事务 A 读到了事务 B还没提交的修改,结果 B 回滚了,A 读到的就是"脏数据"。
事务B 事务A 事务B 事务A #mermaid-svg-jbJc0juNL8PEspLW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jbJc0juNL8PEspLW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jbJc0juNL8PEspLW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jbJc0juNL8PEspLW .error-icon{fill:#552222;}#mermaid-svg-jbJc0juNL8PEspLW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jbJc0juNL8PEspLW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jbJc0juNL8PEspLW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jbJc0juNL8PEspLW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jbJc0juNL8PEspLW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jbJc0juNL8PEspLW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jbJc0juNL8PEspLW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jbJc0juNL8PEspLW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jbJc0juNL8PEspLW .marker.cross{stroke:#333333;}#mermaid-svg-jbJc0juNL8PEspLW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jbJc0juNL8PEspLW p{margin:0;}#mermaid-svg-jbJc0juNL8PEspLW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jbJc0juNL8PEspLW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jbJc0juNL8PEspLW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jbJc0juNL8PEspLW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jbJc0juNL8PEspLW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jbJc0juNL8PEspLW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jbJc0juNL8PEspLW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jbJc0juNL8PEspLW .sequenceNumber{fill:white;}#mermaid-svg-jbJc0juNL8PEspLW #sequencenumber{fill:#333;}#mermaid-svg-jbJc0juNL8PEspLW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jbJc0juNL8PEspLW .messageText{fill:#333;stroke:none;}#mermaid-svg-jbJc0juNL8PEspLW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jbJc0juNL8PEspLW .labelText,#mermaid-svg-jbJc0juNL8PEspLW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jbJc0juNL8PEspLW .loopText,#mermaid-svg-jbJc0juNL8PEspLW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jbJc0juNL8PEspLW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jbJc0juNL8PEspLW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jbJc0juNL8PEspLW .noteText,#mermaid-svg-jbJc0juNL8PEspLW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jbJc0juNL8PEspLW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jbJc0juNL8PEspLW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jbJc0juNL8PEspLW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jbJc0juNL8PEspLW .actorPopupMenu{position:absolute;}#mermaid-svg-jbJc0juNL8PEspLW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jbJc0juNL8PEspLW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jbJc0juNL8PEspLW .actor-man circle,#mermaid-svg-jbJc0juNL8PEspLW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jbJc0juNL8PEspLW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 真实值其实是100,A刚才读的200是脏数据 UPDATE account SET balance=200 (未提交)SELECT balance 读到 200 (脏)ROLLBACK
3.2 不可重复读(Non-Repeatable Read)
事务 A 内两次读同一行,中间事务 B 修改并提交了,A 两次读到的值不一样。重点是同一行被 UPDATE 了。
事务B 事务A 事务B 事务A #mermaid-svg-IiPwpDTMgCBAcbzx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IiPwpDTMgCBAcbzx .error-icon{fill:#552222;}#mermaid-svg-IiPwpDTMgCBAcbzx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IiPwpDTMgCBAcbzx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IiPwpDTMgCBAcbzx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IiPwpDTMgCBAcbzx .marker.cross{stroke:#333333;}#mermaid-svg-IiPwpDTMgCBAcbzx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IiPwpDTMgCBAcbzx p{margin:0;}#mermaid-svg-IiPwpDTMgCBAcbzx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IiPwpDTMgCBAcbzx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IiPwpDTMgCBAcbzx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IiPwpDTMgCBAcbzx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IiPwpDTMgCBAcbzx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IiPwpDTMgCBAcbzx .sequenceNumber{fill:white;}#mermaid-svg-IiPwpDTMgCBAcbzx #sequencenumber{fill:#333;}#mermaid-svg-IiPwpDTMgCBAcbzx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IiPwpDTMgCBAcbzx .messageText{fill:#333;stroke:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IiPwpDTMgCBAcbzx .labelText,#mermaid-svg-IiPwpDTMgCBAcbzx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .loopText,#mermaid-svg-IiPwpDTMgCBAcbzx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IiPwpDTMgCBAcbzx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IiPwpDTMgCBAcbzx .noteText,#mermaid-svg-IiPwpDTMgCBAcbzx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IiPwpDTMgCBAcbzx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IiPwpDTMgCBAcbzx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IiPwpDTMgCBAcbzx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IiPwpDTMgCBAcbzx .actorPopupMenu{position:absolute;}#mermaid-svg-IiPwpDTMgCBAcbzx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IiPwpDTMgCBAcbzx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IiPwpDTMgCBAcbzx .actor-man circle,#mermaid-svg-IiPwpDTMgCBAcbzx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IiPwpDTMgCBAcbzx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一次读 balance = 100UPDATE balance=200 并 COMMIT第二次读 balance = 200 (两次不一样)
3.3 幻读(Phantom Read)
事务 A 内两次范围查询 ,中间事务 B 插入/删除了符合条件的行,A 第二次查到的行数变了。重点是行数"幻影般"多了或少了。
事务B 事务A 事务B 事务A #mermaid-svg-owL5ftk4Ys9MmdfP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-owL5ftk4Ys9MmdfP .error-icon{fill:#552222;}#mermaid-svg-owL5ftk4Ys9MmdfP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-owL5ftk4Ys9MmdfP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-owL5ftk4Ys9MmdfP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-owL5ftk4Ys9MmdfP .marker.cross{stroke:#333333;}#mermaid-svg-owL5ftk4Ys9MmdfP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-owL5ftk4Ys9MmdfP p{margin:0;}#mermaid-svg-owL5ftk4Ys9MmdfP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-owL5ftk4Ys9MmdfP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-owL5ftk4Ys9MmdfP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-owL5ftk4Ys9MmdfP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-owL5ftk4Ys9MmdfP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-owL5ftk4Ys9MmdfP .sequenceNumber{fill:white;}#mermaid-svg-owL5ftk4Ys9MmdfP #sequencenumber{fill:#333;}#mermaid-svg-owL5ftk4Ys9MmdfP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-owL5ftk4Ys9MmdfP .messageText{fill:#333;stroke:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-owL5ftk4Ys9MmdfP .labelText,#mermaid-svg-owL5ftk4Ys9MmdfP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .loopText,#mermaid-svg-owL5ftk4Ys9MmdfP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-owL5ftk4Ys9MmdfP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-owL5ftk4Ys9MmdfP .noteText,#mermaid-svg-owL5ftk4Ys9MmdfP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-owL5ftk4Ys9MmdfP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-owL5ftk4Ys9MmdfP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-owL5ftk4Ys9MmdfP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-owL5ftk4Ys9MmdfP .actorPopupMenu{position:absolute;}#mermaid-svg-owL5ftk4Ys9MmdfP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-owL5ftk4Ys9MmdfP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-owL5ftk4Ys9MmdfP .actor-man circle,#mermaid-svg-owL5ftk4Ys9MmdfP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-owL5ftk4Ys9MmdfP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SELECT * WHERE id>10 第一次: 5行INSERT id=15 并 COMMITSELECT * WHERE id>10 第二次: 6行 (多了一行幻影)
🔴 三者区别一句话 :脏读 = 读到别人没提交 的;不可重复读 = 同一行被别人改过提交了 ;幻读 = 范围里多/少了行。幻读下一篇用 MVCC 专门演示。
四、四种隔离级别
SQL 标准定义了四种隔离级别,从上到下隔离越来越强、性能越来越低、能解决的问题越来越多:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| 读未提交 Read Uncommitted | 可能 | 可能 | 可能 | 基本没人用 |
| 读已提交 Read Committed (RC) | ✅解决 | 可能 | 可能 | Oracle/PostgreSQL 默认 |
| 可重复读 Repeatable Read (RR) | ✅解决 | ✅解决 | ✅基本解决 | MySQL/InnoDB 默认 |
| 串行化 Serializable | ✅解决 | ✅解决 | ✅解决 | 全加锁,性能最差 |
sql
-- 查看
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
-- REPEATABLE-READ REPEATABLE-READ
-- 设置会话级
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局级(对新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 配置文件写死:my.cnf 里 transaction-isolation = READ-COMMITTED
⚠️ MySQL 8 变量名是 transaction_isolation,老版本(5.7 及以前)叫 tx_isolation,别照着老文章抄错。
4.1 实测:RR 下的可重复读
我开两个会话。会话 A 在 RR 下开事务读余额,会话 B 趁机把 id=1 余额改成 200 并提交。会话 A 的输出:
text
A-第一次读(事务开始后) 100
B-更新后提交前自己看 200
A-第二次读(B已提交,RR应仍为100) 100 ← 还是100!
A-当前读FOR UPDATE(应看到最新200) 200 ← 当前读才看到200
🔴 这就是 RR 的可重复读 :会话 A 第二次普通读,明明 B 已经提交了改动,A 还是读到 100。它靠的是 MVCC 的一致性视图(下一篇细讲)。而 SELECT ... FOR UPDATE 这种"当前读"会读最新值,所以看到 200。
4.2 实测:RC 下的不可重复读
同样的实验,把会话 A 设成 RC:
text
A(RC)-第一次读 100
A(RC)-第二次读(B已提交,RC应看到200) 200 ← 变了,不可重复读
✅ 对照清楚了:RC 下每次 SELECT 都生成新的一致性视图,所以能读到别人刚提交的修改(不可重复读);RR 下视图复用,所以读一致(可重复读)。
五、隔离级别到底怎么实现的
为什么 RR 和 RC 行为不同?核心是 ReadView(一致性视图)的创建时机不同:
| 隔离级别 | 普通 SELECT 怎么读 | ReadView 创建时机 |
|---|---|---|
| RU 读未提交 | 不加锁,直接读最新 | 不生成 ReadView |
| RC 读已提交 | 每次 SELECT 都生成新 ReadView | 每条 SELECT 都新建 |
| RR 可重复读 | 复用第一次的 ReadView | 事务第一次 SELECT 时创建,后续复用 |
| Serializable | 所有读加共享锁、写加排他锁 | 不依赖 MVCC,全靠锁 |
🔴 关键冷知识 :BEGIN/START TRANSACTION 不会立刻创建 ReadView ,而是事务里第一条 SELECT 才创建 。这就是为什么你 BEGIN 之后,别的事务改了数据提交了,你接着 SELECT 仍读到旧值------你的 ReadView 在第一次查询那一刻就定格了。
想立刻定格?用 START TRANSACTION WITH CONSISTENT SNAPSHOT;,它会马上建 ReadView。
六、事务的其他关键点
6.1 自动提交与隐式提交
autocommit=ON 时,每条独立 SQL 都是一个自动提交的小事务。执行 LOCK TABLES、START TRANSACTION、DDL 等语句时,会隐式提交当前未提交的事务。
6.2 保存点 SAVEPOINT
事务太大想部分回滚?用保存点:
sql
BEGIN;
INSERT INTO t VALUES (1);
SAVEPOINT sp1;
INSERT INTO t VALUES (2);
ROLLBACK TO sp1; -- 撤销到 sp1,只回滚第二条插入
RELEASE SAVEPOINT sp1;
COMMIT;
6.3 长事务的危害
一个事务开着不提交,会带来一堆问题:
- 持有锁不释放,阻塞别的事务。
- undo log 堆积:老版本可能被这个长事务的 ReadView 引用,purge 线程不敢清理,磁盘暴涨。
- 主从延迟:从库回放慢。
监控长事务:
sql
SELECT trx_id, trx_started, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started; -- trx_started 越老越是长事务
可以用 SET MAX_EXECUTION_TIME 限制查询时长,超时主动打断。
七、面试怎么考
| 面试题 | 一句话答 |
|---|---|
| ACID 及实现 | 原子性 undo log、持久性 redo log、隔离性 锁+MVCC、一致性是目的 |
| 四种隔离级别 | RU/RC/RR/Serializable,逐级解决脏读、不可重复读、幻读 |
| MySQL 默认隔离级别 | 可重复读 RR(InnoDB) |
| 脏读 vs 不可重复读 vs 幻读 | 读未提交 / 同行被改 / 范围多了行 |
| RR 怎么解决幻读 | 快照读靠 MVCC 复用 ReadView,当前读靠 Next-Key Lock |
| RC 和 RR 区别 | ReadView 创建时机:RC 每条 SELECT 新建,RR 第一次 SELECT 后复用 |
| 长事务危害 | 锁不释放、undo log 堆积、主从延迟 |
总结:你真正需要记住的 7 件事
- 事务是一组 SQL 的原子单元,
BEGIN/COMMIT/ROLLBACK;DDL 会隐式提交不能回滚。 - ACID:原子性靠 undo log,持久性靠 redo log,隔离性靠锁+MVCC,一致性是目标。
- 三种并发问题:脏读(读未提交)、不可重复读(同行被改)、幻读(范围多行)。
- 四种隔离级别逐级加强,MySQL InnoDB 默认 RR,RC 是 Oracle/PG 默认。
- RR 和 RC 的本质区别是 ReadView 创建时机------RR 复用,RC 每次新建。
BEGIN不立刻建 ReadView,第一条 SELECT 才建;想立刻定格用START TRANSACTION WITH CONSISTENT SNAPSHOT。- 长事务要监控(
innodb_trx),它会撑大 undo log、阻塞锁、拖慢主从。
验证清单
- 用
SELECT @@transaction_isolation确认默认是 REPEATABLE-READ - 亲手复现 RR 下"两次读同一行都是旧值"的实验
- 切到 RC 复现不可重复读,对比两者 ReadView 时机差异
- 写出脏读/不可重复读/幻读的区别并各举一个例子
- 解释 undo log 和 redo log 分别保障 ACID 的哪一项
- 查一次
information_schema.innodb_trx认识长事务监控视图 - 说出为什么
BEGIN后别的事务提交了你还读到旧值
参考资源
- MySQL 8.0 Reference Manual --- InnoDB Transaction Model: https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-model.html
- MySQL 8.0 Reference Manual --- Transaction Isolation Levels: https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
下篇预告:《MVCC 原理与幻读》------上一篇反复提到"ReadView""快照读",这一篇就把 MVCC 拆到底:每行记录背后那三个隐藏列、串联历史版本的 undo log 版本链、ReadView 四个字段的可见性判断规则,以及为什么 RR 下快照读不幻读、当前读却可能幻读。全部用真实事务 ID 走一遍判断流程。