欢迎光临
我们一直在努力

如果一乐拉面馆宕机了,鸣人的面钱去哪了?

当鸣人掏出20两银子购买拉面时,系统需要同时完成“扣钱”和“记账”两个操作。

如果其中一步执行失败,就可能出现钱没了、面没吃到的离谱事故。为了避免这种情况,数据库设计出了事务(Transaction)这一“S级忍术”,通过 ACID 四大特性保证数据操作的安全与一致。

本文将跟随鸣人的脚步,从一碗拉面出发,深入剖析事务的诞生原因、ACID 的核心思想以及 MySQL 的底层实现机制,带你彻底掌握数据库事务这一面试与实战中的高频知识点。

为什么要有事务?

核心痛点场景:

鸣人身上有 100 两银子,买一碗拉面需要 20 两。

整个购买过程实际上包含两步操作:

  • 鸣人的钱包扣除 20 两。
  • 一乐拉面馆的账本增加 20 两收入。
  • 如果第一步执行成功了,鸣人的钱被扣了;但第二步因为系统故障、网络异常等原因失败了,导致拉面馆账本没有记录这笔收入。

    此时就会出现问题:

    • 鸣人的钱少了 20 两;
    • 拉面馆却没收到这 20 两;
    • 整个系统的数据出现了不一致。

    为了解决这类问题,数据库引入了事务机制。


    事务是什么?

    定义:事务是一组 SQL 构成的不可分割的逻辑整体。

    核心规则:要么全部执行成功,要么全部失败回滚,永远不会停在部分成功部分失败的中间状态。

    事务的四大特性

    原子性(Atomicity)

    • 定义:事务是最小执行单元,要么全成功,要么全失败,不存在中间态。
    • 解决的问题:就是我们刚才转账场景的核心诉求:绝对不能出现「A 扣了钱,B 没加钱」的中间状态。
    • 底层实现:靠undo log(回滚日志)实现:事务执行的每一步修改,都会在 undo log 里记录对应的反向操作(比如「扣 100 元」就记录「加 100 元」),如果任意一步执行失败,就通过 undo log 把之前的所有操作全部撤销,回到初始状态。

    持久性(Durability)

    • 定义:事务一旦提交,对数据的修改就是永久的,哪怕数据库崩溃,修改也不会丢失。

    • 解决的问题:  原子性解决了「执行中失败回滚」的问题,那如果事务已经提交成功了,数据库突然崩溃断电,不能让刚提交的数据丢失。

    • 底层实现:通过redo log(重做日志)实现。事务提交时先把修改写入 redo log 持久化到磁盘,哪怕 MySQL 内存数据没刷到磁盘,崩溃后也能通过 redo log 恢复数据。

    一致性(Consistency)

    • 定义:事务执行前后,数据的业务逻辑保持一致(比如转账前后,两人的总金额永远不变)。

    隔离性(Isolation)

    有了原子性和持久性,单事务执行的问题解决了。

    但实际业务中,数据库永远是多个事务并发执行的:几百个用户同时下单、同时转账,如果多个事务互相干扰,就会出现各种诡异的数据异常。

    隔离性就是用来解决并发事务互相干扰的问题,保证多个事务独立执行,互不影响。

    隔离性要解决的 3 类并发异常

    如果没有隔离性,并发事务会出现 3 种严重的数据问题。

    脏读:读到了别人没提交的临时数据

    业务例子:商品库存本来是 0,事务 A 把库存改成 1,还没提交;事务 B 查询库存读到了 1,开始执行扣库存的逻辑。结果事务 A 因为其他逻辑失败回滚了,库存又变回 0,事务 B 扣库存就会出现超卖。

    → 核心问题:读到了其他事务未提交的临时数据,这些数据随时可能被回滚。

    不可重复读:同一个事务里,同一个数据读两次不一样

    业务例子:事务 A 查询用户余额是 100 元,准备做扣款;此时事务 B 给这个用户转了 200 元并提交;事务 A 第二次查询余额,发现变成了 300 元,同一个事务里两次读到的余额不一样,业务逻辑直接乱掉。

    → 核心问题:同一个事务内,其他事务的提交修改对当前事务可见,导致前后读取结果不一致。

    幻读:同一个事务里,范围查询的条数不一样

    业务例子:事务 A 统计 1 月份的订单,第一次查询有 100 条;此时事务 B 新增了一条 1 月份的订单并提交;事务 A 第二次统计,发现变成了 101 条,同一个事务里两次统计的订单数不一样,报表数据直接出错。

    核心区分:不可重复读是单条数据的值变了,幻读是查询到的数据条数变了,像出现了幻觉一样。

    问题解决:通过事务的隔离级别来解决。

    事务的隔离级别:

    我们知道了并发会带来 3 类数据异常,那怎么解决这些问题?数据库给我们提供了四种隔离级别,本质是数据一致性和并发性能的权衡:隔离级别越高,数据越安全,但并发性能越差。

    四个隔离级别,从低到高

    隔离级别能解决的问题核心特点实际使用场景
    读未提交 什么都解决不了 能读到其他事务所有未提交的修改 完全不用,性能最高但数据完全不可靠
    读已提交 解决脏读 只能读到其他事务已经提交的修改 Oracle、PostgreSQL 的默认级别,适合对一致性要求不高的业务
    可重复读 解决脏读、不可重复读 同一个事务内,永远读到第一次查询的快照数据 MySQL 的默认级别,生产环境最常用,平衡性能和一致性
    串行化 解决所有问题 事务完全串行执行,不允许并发 完全不用,性能极差,会导致大量锁等待和超时

    串行化底层实现:直接加锁。

    读已提交和可重复读的底层实现:MVCC

    现在我们知道了隔离级别的规则,那 MySQL 底层是怎么实现读已提交和可重复读这两个常用隔离级别的?总不能靠加锁吧?全加锁的话并发性能就太差了。

    这就引出了 MySQL 最核心的设计:MVCC 多版本并发控制—— 不用加锁,通过数据多版本实现读写不冲突,大大提升并发性能。

    MVCC 的基础:数据的版本链

    MySQL 的每一行数据,除了我们自己定义的字段,还有两个隐藏字段:

  • DB_TRX_ID:事务 ID,每次修改这行数据的事务 ID,都会记录在这里;
  • DB_ROLL_PTR:回滚指针,指向 undo log 里的历史版本,所有修改过的版本,通过这个指针形成一条「版本链」。
  • 比如一行数据的初始值是 1,事务 1 把它改成 2,事务 2 把它改成 3,就会形成这样的版本链: 当前值3(事务2修改)→ 版本2(事务1修改)→ 版本1(初始值)

    MVCC 的核心:ReadView(读快照)

    有了版本链,那事务读的时候,该读哪个版本?靠ReadView来判断可见性:

    每次执行 SELECT 查询的时候,会生成一个 ReadView,里面记录了当前所有「活跃的、未提交的事务 ID」,通过对比数据行的事务 ID 和 ReadView,就能判断这个版本的数据对当前事务是否可见:

    • 如果数据的事务 ID 已经提交,就可见;
    • 如果不可见,就顺着版本链往前找,直到找到可见的版本。

    两个隔离级别的实现差异

    读已提交和可重复读的唯一区别,就是「什么时候生成 ReadView」:

  • 可重复读(MySQL 默认):整个事务生命周期中,只在第一次执行 SELECT 的时候生成一次 ReadView,后面所有查询都复用这个 ReadView → 所以同一个事务里,所有查询看到的版本永远是第一次的快照,不管其他事务怎么修改提交,都看不到,自然就解决了不可重复读。

  • 读已提交:每次执行 SELECT,都会生成一个全新的 ReadView → 每次查询都能看到其他事务新提交的版本,所以能读到最新提交的数据,解决了脏读,但解决不了不可重复读。

  • 整个事务的体系就是一个完整的「问题→解决方案」链条,没有任何零散的知识点:

  • 业务有一组 SQL 要么全成要么全败的需求 → 诞生了事务;
  • 事务需要四层保障保证正确性 → 有了 ACID 四大特性;
  • 并发事务会出现数据异常 → 隔离性来解决,对应三类并发问题;
  • 为了平衡一致性和性能 → 设计了四个分层的隔离级别;
  • 为了实现隔离级别又不损失性能 → 诞生了 MVCC 多版本并发控制。
  • 赞(0)
    未经允许不得转载:171主机测评 » 如果一乐拉面馆宕机了,鸣人的面钱去哪了?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址