自学内容网 自学内容网

MySQL 记录锁+间隙锁防幻读方案

1. 什么是幻读?

定义
        幻读是指在事务过程中,一个事务执行了某些查询操作后,另一事务插入、删除或更新了满足查询条件的新数据,当第一个事务再次执行相同的查询操作时,结果集出现了“幻觉”,即读取到(或缺少)先前查询中不存在的数据。

幻读与脏读、不可重复读的区别

  1. 脏读:事务读取了另一个事务未提交的数据,可能会读到错误结果。
  2. 不可重复读:事务在两次读取中,读到的数据被其他事务修改了,数据值不同。
  3. 幻读:事务在两次读取中,结果集的记录条数或范围变化了,比如多了新记录或少了某些记录。

举例说明
假设有一个数据库表 employees,内容如下:

idnamesalary
1Alice5000
2Bob4000
3Charlie3000

场景
事务A执行如下查询:

SELECT * FROM employees WHERE salary > 3000;

结果为:

idnamesalary
1Alice5000
2Bob4000

这时,事务B插入了一条新记录:

INSERT INTO employees (id, name, salary) VALUES (4, 'David', 4500);

如果事务A再次执行相同的查询操作,结果会变为:

idnamesalary
1Alice5000
2Bob4000
4David4500

事务A“感知”到了新数据的插入,这种现象就是幻读。

2. MySQL 是如何解决幻读的?

MySQL 通过两种方式解决幻读问题:

  1. 针对快照读:使用 MVCC(多版本并发控制)
  2. 针对当前读:通过加锁机制(记录锁和间隙锁)

(1)针对快照读(Snapshot Read)

快照读的定义
        快照读是一种基于 MVCC 的非阻塞读取方式。执行普通 SELECT 语句时,InnoDB 存储引擎会基于当前事务启动时的数据库版本生成一个数据快照,事务读取的内容固定为快照中的数据,忽略其他事务的更新、插入或删除操作。

如何实现快照读

  • MVCC 原理:InnoDB 为每行记录维护两个隐藏的时间戳字段:trx_id(创建该记录的事务ID)和 roll_pointer(指向回滚日志的指针)。
    • trx_id 用于标记这条记录由哪个事务创建。
    • roll_pointer 用于回滚到旧版本数据,从而形成多个版本链。
  • ReadView
    • 每个事务启动时会生成一个 ReadView,其中包含当前正在活跃事务的 ID 列表。
    • 事务读取数据时,会根据 trx_idReadView 判断数据版本是否可见。

快照读的优点

  1. 高性能:读取操作不需要等待锁释放,可以并发执行。
  2. 一致性:避免了其他事务导致的数据不可重复读或幻读现象。

快照读的限制
快照读只能用于非锁定查询,例如:

SELECT * FROM table_name WHERE ...;

(2)针对当前读(Current Read)

当前读的定义
        当前读是一种锁定读取方式,事务在读取数据的同时,会对涉及的数据范围加锁,防止其他事务对数据进行修改或插入。

当前读的典型语句
以下语句属于当前读:

  • SELECT ... FOR UPDATE;
  • SELECT ... LOCK IN SHARE MODE;
  • INSERT, UPDATE, DELETE

加锁机制:记录锁和间隙锁(Next-Key Lock)

为了防止当前读场景下的幻读,MySQL 使用以下两种锁:

  1. 记录锁(Record Lock)

    • 锁定单条记录。
    • 防止其他事务修改或删除这条记录。
  2. 间隙锁(Gap Lock)

    • 锁定一个记录范围之间的“间隙”,即不存在记录的区域。
    • 防止其他事务向这个间隙中插入新记录。
  3. Next-Key Lock(记录锁 + 间隙锁)

    • 锁定一个范围,同时包含范围内的所有记录和间隙。
    • Next-Key Lock 是记录锁和间隙锁的组合,用于防止当前读下的幻读。

3. 通过记录锁和间隙锁防止删除操作导致的幻读的示例

        我们以一个具体场景为例,说明 MySQL 如何通过记录锁和间隙锁防止幻读。

场景描述

假设有一个名为 orders 的表,存储如下数据:

idorder_nameamount
1Order_A100
2Order_B200
4Order_D400

事务过程:

步骤 1:事务A执行当前读
事务A读取表中 amount > 100 的记录并对这些记录加锁:

SELECT * FROM orders WHERE amount > 100 FOR UPDATE;

事务A读取到:

idorder_nameamount
2Order_B200
4Order_D400

加锁分析:

  • 记录锁:锁住符合条件的记录 id=2id=4
  • 间隙锁:锁住 (100, 200)(200, 400) 之间的间隙,以及 (400, +∞) 的间隙。

步骤 2:事务B尝试插入新记录
事务B尝试插入一条新记录:

INSERT INTO orders (id, order_name, amount) VALUES (3, 'Order_C', 300);

事务B会被阻塞,原因如下:

  • 插入的 amount=300 落在 (200, 400) 的间隙范围,而事务A持有该范围的间隙锁,禁止其他事务在此区间插入记录。

步骤 3:事务B尝试删除或修改记录
事务B尝试删除或修改已存在的记录:

DELETE FROM orders WHERE id = 2;

事务B再次被阻塞,原因如下:

  • id=2 的记录已被事务A加了记录锁,其他事务无法删除或修改这条记录。

步骤 4:事务A提交后,事务B执行成功
当事务A完成操作并提交后,锁释放:

  • 事务B的插入操作 id=3, amount=300 可以成功执行。
  • 事务B的删除操作 id=2 也可以正常进行。

总结加锁机制防幻读的过程:

  1. 记录锁:防止其他事务修改或删除当前事务查询到的记录。
  2. 间隙锁:防止其他事务在查询范围内插入新记录。
  3. Next-Key Lock(记录锁 + 间隙锁):通过同时锁定记录和范围,有效防止幻读。

4. 总结

幻读的定义

幻读是指在事务中,某次查询范围内新增了数据,导致后续查询结果不一致。例如,第一次查询到的结果集中没有一条记录,但后续由于其他事务的插入操作,查询结果发生变化。

MySQL 解决幻读的两种方式:

  1. 快照读(Snapshot Read):使用 MVCC 技术

    • 基于多版本并发控制 (MVCC),通过维护数据的多个版本及事务的 ReadView,实现一致性读取。
    • 快照读适用于非阻塞读取操作,如普通的 SELECT 查询。
  2. 当前读(Current Read):使用加锁机制

    • 对读取范围内的记录和间隙加锁,通过记录锁(Record Lock)和间隙锁(Gap Lock)防止插入或修改,避免幻读。
    • 当前读适用于需要锁定数据的操作,例如 SELECT ... FOR UPDATEUPDATEDELETE

加锁机制:记录锁和间隙锁

  • 记录锁:锁定查询范围内的记录,防止其他事务修改或删除这些记录。
  • 间隙锁:锁定查询范围内的“间隙”,防止其他事务向这些间隙插入新记录。
  • Next-Key Lock:记录锁和间隙锁的结合,既锁定记录,又锁定范围,是防止幻读的关键机制。

示例回顾:通过记录锁和间隙锁防止删除操作导致的幻读

在示例中,我们通过以下步骤,详细分析了 MySQL 如何防止幻读:

  1. 事务A对查询范围内的记录和间隙加锁(Next-Key Lock)。
  2. 事务B试图插入新记录或修改已锁定的记录时被阻塞。
  3. 事务A提交后,锁释放,事务B的操作得以继续。

通过 Next-Key Lock 机制,MySQL 确保了事务A的查询范围内的数据不被其他事务破坏,从而避免了幻读现象。

总结价值与启发

  1. 事务隔离级别和加锁机制密切相关:了解幻读的解决方式有助于更好地设计事务逻辑,避免数据一致性问题。
  2. 性能与一致性的权衡:MVCC 提高了读取性能,但需要额外的存储和计算;锁机制确保了当前读的一致性,但可能引发锁争用问题。
  3. 应用场景选择:根据具体需求选择快照读或当前读策略。例如,在频繁更新的场景中,通过加锁防止幻读更为可靠。


原文地址:https://blog.csdn.net/m0_53926113/article/details/144428365

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!