【MySQL】第7节|Mysql锁机制与优化实践以及MVCC底层原理剖析

锁等待分析

我们通过检查InnoDB_row_lock相关的状态变量来分析系统上的行锁的争夺情况

示例场景

假设有两个用户同时操作账户表 accounts(主键为 id):

1. 用户A :执行转账,锁定账户 id=1 并等待3秒:

复制代码
BEGIN;
SELECT * FROM accounts WHERE id=1 FOR UPDATE;
-- 模拟业务逻辑耗时3秒
COMMIT;

2. 用户B:几乎同时尝试锁定同一账户:

复制代码
BEGIN;
SELECT * FROM accounts WHERE id=1 FOR UPDATE;  -- 被阻塞,等待用户A释放锁
COMMIT;

参数变化说明

  1. Innodb_row_lock_current_waits
    • 含义:当前正在等待锁的事务数量。
    • 示例 :当用户B被阻塞时,该值为 1;用户A提交后,该值变为 0
  2. Innodb_row_lock_waits
    • 含义:系统启动后锁等待的总次数。
    • 示例 :用户B等待一次后,该值增加 1
  3. Innodb_row_lock_time
    • 含义:系统启动后所有锁等待的总时长(毫秒)。
    • 示例 :用户B等待3秒(3000毫秒)后,该值增加 3000
  4. Innodb_row_lock_time_avg
    • 含义:每次锁等待的平均时长(毫秒)。
    • 示例 :若此前无等待,此次等待后该值为 3000;若已有1次等待(假设为2000毫秒),则平均值为 (2000 + 3000) / 2 = 2500
  5. Innodb_row_lock_time_max
    • 含义:系统启动后最长的单次锁等待时长。
    • 示例 :若此前最大值为2500毫秒,此次等待3000毫秒后,该值更新为 3000

关键指标解读

  • Innodb_row_lock_waits

表示频繁出现锁争用,可能是事务持有锁时间过长或并发度过高。

优化:缩短事务执行时间,避免长事务。

  • Innodb_row_lock_time_avg

表示锁等待时间过长,可能是锁粒度太大或索引缺失导致锁范围扩大。

优化:优化查询语句,确保使用索引减少锁范围。

  • Innodb_row_lock_time

累计等待时间过长,影响整体吞吐量。

优化:调整业务逻辑,减少锁竞争(如批量操作拆分为多次小事务)。

查看方法

复制代码
SHOW STATUS LIKE 'Innodb_row_lock%';

-- 输出示例:
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 3000  |
| Innodb_row_lock_time_avg      | 3000  |
| Innodb_row_lock_time_max      | 3000  |
| Innodb_row_lock_waits         | 1     |
+-------------------------------+-------+

这些指标是 动态累积值,重启MySQL后会重置为0。通过监控它们的变化趋势,可以定位数据库性能瓶颈。

INFORMATION_SCHEMA系统库锁相关数据表

以下是关于 InnoDB 事务和锁相关系统表的示例说明,结合常见场景演示如何查询和分析数据:

一、查看当前事务( INNODB_TRX**/** information_schema.INNODB_TRX**)**

场景:查询当前正在运行的事务
复制代码
SELECT
  trx_id,               -- 事务ID
  trx_state,            -- 事务状态(RUNNING、LOCK WAIT等)
  trx_started,          -- 事务开始时间
  trx_query,            -- 正在执行的SQL语句(可能为NULL)
  trx_mysql_thread_id   -- 事务对应的线程ID
FROM INFORMATION_SCHEMA.INNODB_TRX;
示例输出

|------------|---------------|---------------------|-----------------------------------------------|-------------------------|
| trx_id | trx_state | trx_started | trx_query | trx_mysql_thread_id |
| 12345 | LOCK WAIT | 2025-05-24 15:30:00 | SELECT * FROM accounts WHERE id=1 FOR UPDATE | 101 |
| 12346 | RUNNING | 2025-05-24 15:29:55 | UPDATE orders SET status='paid' | 102 |

说明
  • trx_state=LOCK WAIT:表示事务正在等待锁(如示例中线程101在等待锁定id=1的记录)。
  • trx_query:若事务未执行具体SQL(如处于空闲状态),则显示为NULL

二、查看锁信息( INNODB_LOCKS**→ 8.0+ 改为** performance_schema.data_locks**)**

场景:查询当前数据库中的行锁
复制代码
-- MySQL 5.7及之前版本
SELECT
  lock_id,          -- 锁ID
  lock_trx_id,      -- 持有锁的事务ID
  lock_mode,        -- 锁模式(如X锁、S锁、IS锁、IX锁)
  lock_table,       -- 锁定的表名
  lock_index,       -- 锁定的索引(若为行锁,通常为索引名)
  lock_space,       -- 表空间ID
  lock_page,        -- 数据页号
  lock_rec,         -- 数据行号
  lock_data          -- 锁定的行数据(如主键值)
FROM INFORMATION_SCHEMA.INNODB_LOCKS;

-- MySQL 8.0+ 版本(改用 performance_schema.data_locks)
SELECT
  ENGINE_LOCK_ID,    -- 锁ID
  THREAD_ID,         -- 持有锁的线程ID
  LOCK_MODE,         -- 锁模式
  LOCK_TYPE,         -- 锁类型(ROW、TABLE等)
  TABLE_SCHEMA,      -- 表所属数据库
  TABLE_NAME,        -- 表名
  INDEX_NAME,        -- 索引名
  LOCK_DATA           -- 锁定的行数据(如主键值)
FROM performance_schema.data_locks;
示例输出(MySQL 8.0+)

|--------------------|---------------|---------------|---------------|------------------|----------------|----------------|---------------|
| ENGINE_LOCK_ID | THREAD_ID | LOCK_MODE | LOCK_TYPE | TABLE_SCHEMA | TABLE_NAME | INDEX_NAME | LOCK_DATA |
| 12345:10:4:3 | 102 | X | ROW | test | accounts | PRIMARY | 1 |
| 12346:10:4:5 | 103 | S | ROW | test | orders | idx_status | 'paid' |

说明
  • LOCK_MODE=X:表示排他锁(示例中线程102对accountsid=1的记录加了X锁)。
  • LOCK_DATA:行锁通常显示主键值(如1),表锁或意向锁可能显示为NULL

三、查看锁等待( INNODB_LOCK_WAITS**→ 8.0+ 改为** performance_schema.data_lock_waits**)**

场景:查询当前锁等待的阻塞关系
复制代码
-- MySQL 5.7及之前版本
SELECT
  request_trx_id,     -- 请求锁的事务ID(等待者)
  request_lock_id,    -- 请求的锁ID
  block_trx_id,       -- 持有锁的事务ID(阻塞者)
  block_lock_id       -- 被持有的锁ID
FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

-- MySQL 8.0+ 版本(改用 performance_schema.data_lock_waits)
SELECT
  REQUESTING_THREAD_ID,  -- 请求锁的线程ID(等待者)
  BLOCKING_THREAD_ID,    -- 持有锁的线程ID(阻塞者)
  REQUESTED_LOCK_ID,     -- 请求的锁ID
  BLOCKING_LOCK_ID       -- 被持有的锁ID
FROM performance_schema.data_lock_waits;
示例输出

|--------------------|---------------------|------------------|-------------------|
| request_trx_id | request_lock_id | block_trx_id | block_lock_id |
| 12345 | 12345:10:4:3 | 12346 | 12346:10:4:3 |

说明
  • 事务12345(等待者) 请求锁定id=1的记录,但事务12346(阻塞者) 已持有该记录的X锁,导致阻塞。
  • 通过此表可快速定位死锁或锁竞争的源头。

四、释放锁( KILL THREAD**)**

场景:强制终止阻塞事务(需谨慎!)
  1. 通过 INNODB_TRX****找到阻塞事务的线程ID

    SELECT trx_mysql_thread_id FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_id=12346; -- 假设阻塞者事务ID为12346
    -- 输出:102

  2. 终止线程释放锁

    KILL 102; -- 杀死线程ID为102的事务(需SUPER权限)

注意
  • 直接终止事务可能导致数据不一致或未提交的业务逻辑中断,仅建议在紧急情况下使用(如死锁无法自动解决时)。

五、查看锁等待详细信息( SHOW ENGINE INNODB STATUS**)**

场景:获取更详细的锁等待日志(包含死锁检测信息)
复制代码
SHOW ENGINE INNODB STATUS\G
关键输出片段
复制代码
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-05-24 15:35:00 0x7f8a12345678
*** (1) TRANSACTION:
TRANSACTION 12347, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 103, OS thread handle 12345, query id 123456 test 192.168.1.1 root
INSERT INTO accounts (id, balance) VALUES (2, 1000)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 12347 lock_mode X insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000003031; asc    01;;
 2: len 7; hex b60000019d0110; asc        ;;

*** (2) TRANSACTION:
TRANSACTION 12348, ACTIVE 6 sec inserting
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 104, OS thread handle 12346, query id 123457 test 192.168.1.2 root
INSERT INTO accounts (id, balance) VALUES (1, 2000)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 10 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 12348 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000003030; asc    00;;
 2: len 7; hex b60000019d010a; asc        ;;

*** (1) AND (2) WAITING FOR EACH OTHER'S LOCKS!
说明
  • 输出显示两个事务(12347和12348)在插入数据时发生死锁,互相等待对方持有的锁。
  • SHOW ENGINE INNODB STATUS 会打印最近一次死锁的详细信息,包括锁定的表、索引、行数据及事务操作,用于分析死锁原因。

总结:如何通过系统表诊断锁问题

  1. 第一步 :通过 INNODB_TRX 查看是否有事务处于 LOCK WAIT 状态,定位等待者和阻塞者的线程ID。
  2. 第二步 :通过 data_locksINNODB_LOCKS 查看具体锁的类型、锁定的表和行数据。
  3. 第三步 :通过 data_lock_waitsINNODB_LOCK_WAITS 确认锁等待的阻塞关系。
  4. 第四步 :结合 SHOW ENGINE INNODB STATUS 的详细日志分析死锁或长时间等待的原因。

通过这些系统表的组合查询,可以快速定位数据库中的锁竞争、死锁等性能问题,并针对性优化事务逻辑或索引设计。

死锁问题分析

复制代码
set transcation_isolation='repeatable-read';
Session_1执行:select * from account where id=1 for update;
Session_2执行:select * from account where id=2 for update;
Session_1执行:select * from account where id=2 for update;
Session_2执行:select * from account where id=1 for update;
查看近期死锁日志信息:show engine innodb status; 

锁优化实践

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
  • 尽可能用低的事务隔离级别

MVCC多版本并发控制机制

MVCC(多版本并发控制)是MySQL中InnoDB存储引擎实现读一致性和并发控制 的核心机制,主要用于读已提交(RC)和可重复读(RR)隔离级别(默认是RR)。它通过版本链一致性读视图避免锁竞争,提升并发性能。

核心要素

  1. 版本链
    • 每行数据修改时会生成多个版本,通过隐藏字段(trx_idroll_ptr等)串联成版本链。
    • trx_id:修改数据的事务ID(唯一递增)。
    • roll_ptr:指向旧版本数据(存于回滚日志undo log)。
  2. 一致性读视图(Read View)
    • 事务执行普通查询(非锁定读,如SELECT)时,会生成一个快照(读视图),记录当前活跃事务ID列表。
    • 通过读视图判断数据版本是否可见,实现无锁读。

在MySQL中的应用场景

1. 不同隔离级别下的行为
  • 可重复读(RR)

    • 事务启动时 生成读视图,整个事务期间只读该视图,保证多次读取结果一致(避免不可重复读和幻读)。

    • -- 事务A(RR隔离级别)
      BEGIN;
      SELECT * FROM t WHERE id=1; -- 生成读视图V1,读取版本链中可见的数据
      -- 其他事务修改id=1的数据,生成新版本(trx_id=102)
      SELECT * FROM t WHERE id=1; -- 仍用V1,读取旧版本数据(不可重复读被禁止)
      COMMIT;

  • 读已提交(RC)

    • 每次查询时 重新生成读视图,保证每次读取的是最新已提交数据(可能出现不可重复读)。

    • -- 事务A(RC隔离级别)
      BEGIN;
      SELECT * FROM t WHERE id=1; -- 读视图V1,读取旧版本
      -- 其他事务提交修改(trx_id=102)
      SELECT * FROM t WHERE id=1; -- 重新生成V2,读取新版本(允许不可重复读)
      COMMIT;

2. 与锁的配合
  • 快照读(非阻塞读) :普通SELECT使用MVCC,不加锁,读旧版本数据(一致性读)。
  • 当前读(阻塞读) :加锁语句(如SELECT ... FOR UPDATE)直接读最新数据,需等待锁释放。
    • -- 事务A(当前读)
      SELECT * FROM t WHERE id=1 FOR UPDATE; -- 读取最新数据,加行锁,阻塞其他写事务

3. 提升并发性能
  • 写操作(如INSERT/UPDATE/DELETE)生成新版本,读操作通过MVCC访问旧版本,避免读写阻塞。
  • 场景:高并发读场景(如电商商品详情页),读不阻塞写,写不阻塞读。

关键特性

  • 无锁读 :普通查询不阻塞写事务,反之亦然(除了SERIALIZABLE隔离级别)。
  • 读一致性:根据隔离级别控制数据可见性,避免脏读、不可重复读等问题。
  • 回滚日志管理:旧版本数据存于undo log,由InnoDB自动清理(purge线程),避免日志膨胀。

总结

MVCC是InnoDB实现高并发的基石,通过版本链和读视图在一致性并发性间找到平衡。理解其原理有助于优化事务设计(如合理选择隔离级别)和排查锁问题(如长时间锁等待可能因MVCC版本链过长导致)。

相关推荐
Chasing__Dreams6 分钟前
Redis--基础知识点--28--慢查询相关
数据库·redis·缓存
C1829818257520 分钟前
小表驱动大表更快吗,不是
数据库
熙曦Sakura38 分钟前
【MySQL】用户管理
android·mysql·adb
cookqq2 小时前
mongodb源码分析session接受客户端find命令过程
数据库·sql·mongodb·nosql
小天努力学java2 小时前
Java -- 并发编程
java·数据库
江沉晚呤时2 小时前
深入了解 C# 异步编程库 AsyncEx
java·前端·数据库·c#·.netcore
有很多梦想要实现3 小时前
Nest全栈到失业(四):半小时图书管理系统-Book
开发语言·前端·javascript·数据库·typescript·webstorm
幼儿园口算大王3 小时前
Mysql中索引B+树、最左前缀匹配
数据库·b树·mysql
hixiaoyang3 小时前
MVCC 原理与并发控制实现
运维·服务器·数据库
大新新大浩浩3 小时前
k8s上运行的mysql、mariadb数据库的备份记录
数据库·mysql·kubernetes