找回密码
 立即注册
首页 业界区 安全 MySQL的并发访问机制

MySQL的并发访问机制

肿抢 4 天前
在MySQL中,锁是用于解决并发访问冲突的核心机制。当多个事务同时操作数据库中的数据时(如读取、修改、删除),可能会出现数据不一致(如脏读、不可重复读、幻读)或操作冲突(如同时修改同一行),锁的作用就是通过合理限制不同事务的操作权限,保证数据的一致性和并发操作的正确性。本文只讨论InnoDB引擎下并发访问控制。
锁的粒度

MySQL从锁的粒度上分为表级锁和行级锁,MySQL会根据不同情况判断是对表上锁还是对行上锁。
表级锁

表级锁有四种类型:

  • 共享锁

    • 显示上锁 : LOCK TABLES 表名 READ;  UNLOCK TABLES;

  • 排他锁

    • 显示上锁 : LOCK TABLES 表名 WRITE;  UNLOCK TABLES;
    • 隐式上锁:如果行级锁需要加锁的数据条数太多,会直接加表锁,减少维护行锁的消耗。

  • 意向共享锁

    • 给行加共享锁会尝试给表加意向共享锁

  • 意向排他锁

    • 给行加排他锁会尝试给表加意向排他锁

如果没有意向锁,当一个事务想要给整个表加表级排他锁(X 锁)时,需要逐行检查是否有行级锁(S 锁或 X 锁),这在大表中会非常低效。而意向锁通过 “预先声明” 的方式,让表级锁的检查只需判断意向锁的类型,无需扫描每行,大幅提升性能。
行级锁

行级锁粒度:

  • 行锁 对每一行数据加锁
  • 间隙锁  区域锁,防止其他事务数据插入导致的幻读。
  • next-key 同时给行锁和其前面的间隙加锁。
间隙锁和Next-Key锁(行级+间隙)只存在不小于RR(可重复读)隔离级别下,用于解决幻读,下面有论述。
行级锁类型:

  • 共享锁

    • 显示加锁:SELECT ... LOCK IN SHARE MODE
    • 隐式加锁:串行化隔离级别时候,读取数据会加共享锁,确保整个事务内读取的数据不会变化

  • 排他锁

    • 显示加锁:SELECT ... FOR UPDATE
    • 隐式加锁:尝试更新数据的时候

共享锁之间不冲突,多个事务可以对同一个表或数据加共享锁。共享锁和排他锁之间冲突,A事务对一个表或数据加了共享锁,B事务就无法再在这个表或数据上加排他锁。显示的加共享锁,可以防止某些数据被其他事务更新,可以在可重复读隔离级别下实现串行化。
不同隔离级别的上锁逻辑

不同隔离级别的时候的上锁逻辑不一样的。
读取数据

隔离级别是否默认加S锁显式加锁方式(强制加锁)主要目的READ UNCOMMITTED否无(通常无需)允许脏读,追求极致并发READ COMMITTED否SELECT ... FOR SHARE避免脏读,不阻塞读写REPEATABLE READ否(依赖 MVCC)SELECT ... FOR SHARE默认无锁,显式加锁用于特殊场景SERIALIZABLE是(隐式加 S 锁)无需显式,自动加锁完全串行化,保证最高一致性修改数据

隔离级别是否加行锁(X 锁)是否加间隙锁 / Next-Key Lock主要目的READ UNCOMMITTED是否防止并发修改冲突READ COMMITTED是否防止并发修改冲突REPEATABLE READ是可能(范围 / 未命中时)防止并发修改 + 幻读SERIALIZABLE是是(范围 / 未命中时)最高一致性,彻底防幻读MVCC

MVCC 是 InnoDB 存储引擎实现非阻塞读的关键机制,其核心思想是:为数据维护多个版本,事务读取时无需加锁,而是通过 “版本链” 找到符合自身可见性规则的数据版本,从而避免读操作阻塞写操作(反之亦然),提升并发性能。
MVCC 在不同隔离级别的表现


  • 读已提交(RC)隔离级别
MVCC 生效:读操作(SELECT)会通过 MVCC 读取 “已提交的最新版本” 数据。
具体行为:

  • 每次执行 SELECT 时,都会生成一个新的 Read View(读视图,用于判断数据版本的可见性)。
  • 因此,同一事务中两次执行相同的 SELECT,可能读到其他事务已提交的新数据(即 “不可重复读”)。
  • 例如:事务 A 第一次查询某行值为 1,事务 B 修改为 2 并提交,事务 A 再次查询会读到 2。
  • MVCC 的作用:避免读操作加锁,同时保证不会读到未提交的脏数据(符合 “读已提交” 的要求)。

  • 可重复读(RR)隔离级别
MVCC 生效:读操作通过 MVCC 读取 “事务启动时的一致性版本” 数据。
具体行为:

  • 事务启动时生成一个 Read View,并在整个事务期间复用该视图,不再重新生成。
  • 因此,同一事务中多次执行 SELECT 会读到相同的数据(即使其他事务已提交修改),实现 “可重复读”。
  • 例如:事务 A 启动时查询某行值为 1,事务 B 修改为 2 并提交,事务 A 再次查询仍读到 1。
  • MVCC 的作用:在无锁的情况下,保证事务内读取数据的一致性,同时避免 “不可重复读”。
不同隔离级别的实现原理

事务级别描述实现READ UNCOMMITTED能够读取到其他事务未提交的数据直接读取数据的最新版本。READ COMMITTED不能读取到未提交数据,同一事务里多次读取可能查询的数据不一样通过MVCC快照实现,排除掉正在执行的事务,读取当前事务之前提交的版本。但是重新读取的时候会重新生成readview,所以会读取到已提交的数据。REPEATABLE READ同一事务下每次读取的数据保持一致读取的时候通过mvvc创建一个readview,再次读取的时候使用之前readview,所以不会查询到事务执行期间提交的数据。特殊情况下使用间隙锁保证,下面有描述。SERIALIZABLE同一事务下每次读取的数据保持一致通过select加读锁保证数据不被修改。事务隔离级别是用于控制不同事务级别下同一事务读取数据的逻辑。更新数据的时候因为需要更新最新版本的数据,无法使用MVCC,还是需要靠锁进行数据隔离。
MVCC和间隙锁

MVCC已经能解决幻读的问题了,为什么还要有间隙锁。具体场景分析:
假设表user的id(主键)存在记录10、20,初始数据如下:
idname10Alice20Bob步骤 1:事务 A 启动,执行快照读(普通 SELECT)
事务 A 开始后,执行第一次范围查询(快照读,依赖 MVCC):
sql
  1. -- 事务A
  2. START TRANSACTION;
  3. -- 快照读:基于MVCC的Read View,只能看到事务启动前已提交的数据
  4. SELECT * FROM user WHERE id BETWEEN 10 AND 20;
  5. -- 结果:仅能看到id=10和id=20的记录
复制代码
步骤 2:事务 B 插入新记录并提交
此时事务 B 插入一条在[10,20]范围内的新记录,并提交:
sql
  1. -- 事务B
  2. START TRANSACTION;
  3. INSERT INTO user (id, name) VALUES (15, 'Charlie'); -- 插入新记录
  4. COMMIT;
复制代码
由于没有间隙锁,事务 B 的插入操作不会被阻塞(间隙锁的作用就是阻止这种插入)。
步骤 3:事务 A 执行当前读(如 UPDATE / 加锁查询)
事务 A 接着执行一个 “当前读” 操作(如更新或加锁查询,会读取最新数据,而非 MVCC 快照):
sql
  1. -- 事务A
  2. -- 当前读:读取最新数据,而非快照
  3. UPDATE user SET name = 'Updated' WHERE id BETWEEN 10 AND 20;
  4. -- 此时会更新id=10、20(原有记录)和id=15(事务B插入的新记录)
复制代码
步骤 4:事务 A 再次执行快照读,出现幻读
事务 A 再次执行快照读时:
sql
  1. -- 事务A
  2. SELECT * FROM user WHERE id BETWEEN 10 AND 20;
复制代码
此时结果会包含id=15的记录(因为事务 A 自己更新过这条记录,MVCC 规则中 “事务可以看到自己修改的内容”)。
但这条记录在事务 A 第一次查询时并不存在,因此出现了 “幻读”—— 同一事务内,两次相同的范围查询,第二次出现了第一次未见过的新记录。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册