外观
MySQL 锁等待分析
锁等待概述
版本差异
| 特性 | MySQL 5.6 | MySQL 5.7 | MySQL 8.0 |
|---|---|---|---|
| 锁等待监控视图 | INFORMATION_SCHEMA.INNODB_LOCKS/INNODB_LOCK_WAITS | INFORMATION_SCHEMA.INNODB_LOCKS/INNODB_LOCK_WAITS | INFORMATION_SCHEMA.INNODB_LOCKS/INNODB_LOCK_WAITS + performance_schema.data_locks |
| 死锁检测 | 支持 | 支持 | 支持(增强算法) |
| 锁超时机制 | innodb_lock_wait_timeout | innodb_lock_wait_timeout | innodb_lock_wait_timeout |
| Online DDL | 支持部分操作 | 支持大部分操作 | 支持几乎所有操作 |
| 间隙锁行为 | 严格 | 严格 | 可配置(innodb_strict_mode) |
| 锁内存管理 | 基于哈希表 | 基于哈希表 | 优化的内存管理 |
| 事务隔离级别默认值 | REPEATABLE READ | REPEATABLE READ | REPEATABLE READ |
| 锁监控工具 | 基本工具 | 增强工具 | 全面监控 |
锁的基本概念
| 概念 | 描述 |
|---|---|
| 锁 | 数据库用于控制并发访问的机制 |
| 锁等待 | 一个事务等待另一个事务释放锁的状态 |
| 死锁 | 两个或多个事务相互等待对方释放锁 |
| 锁超时 | 等待锁超过指定时间后失败 |
锁的类型
按锁粒度分类
| 锁粒度 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 全局锁 | 锁定整个数据库 | 实现简单 | 并发度极低 |
| 表级锁 | 锁定整张表 | 开销小,加锁快 | 并发度低 |
| 页级锁 | 锁定数据页(16KB) | 平衡粒度 | 开销较大 |
| 行级锁 | 锁定单行数据 | 并发度高 | 开销大,加锁慢 |
按锁模式分类
| 锁模式 | 描述 | 兼容关系 |
|---|---|---|
| 共享锁(S) | 允许读取,阻止写入 | 与S锁兼容,与X锁互斥 |
| 排他锁(X) | 允许写入,阻止其他所有锁 | 与所有锁互斥 |
| 意向共享锁(IS) | 表级锁,表示事务打算加S锁 | 与S、IS兼容,与X互斥 |
| 意向排他锁(IX) | 表级锁,表示事务打算加X锁 | 与S、X、IS、IX兼容 |
InnoDB特有锁
| 锁类型 | 描述 | 场景 |
|---|---|---|
| 记录锁 | 锁定单行记录 | 精准条件查询 |
| 间隙锁 | 锁定索引区间 | 防止幻读 |
| 临键锁 | 记录锁+间隙锁 | 范围查询 |
| 插入意向锁 | 插入操作时的间隙锁 | 并发插入同一区间 |
锁等待的影响
- 性能下降:锁等待导致查询延迟增加
- 吞吐量降低:并发事务数减少
- 死锁风险:锁等待可能升级为死锁
- 应用超时:客户端可能因为等待锁而超时
- 资源浪费:事务持有锁但不释放,占用连接资源
锁等待监控与分析
查看锁等待状态
使用 SHOW PROCESSLIST
sql
-- 查看当前所有进程
SHOW FULL PROCESSLIST;
-- 筛选出处于锁等待的进程
SHOW FULL PROCESSLIST WHERE Command = 'Query' AND State LIKE '%lock%';输出解读:
State列显示锁等待状态,如Waiting for table metadata lock、Waiting for table level lock、Waiting for row lockInfo列显示正在执行的SQL语句Time列显示等待时间(秒)
使用 INFORMATION_SCHEMA 表
sql
-- 查看正在等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-- 查看当前持有锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看当前事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
-- 关联查询锁等待详情
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query,
t.lock_mode,
t.lock_type,
t.lock_table,
t.lock_index,
t.lock_space,
t.lock_page,
t.lock_rec
FROM
INFORMATION_SCHEMA.INNODB_LOCK_WAITS w
JOIN
INFORMATION_SCHEMA.INNODB_TRX b ON w.blocking_trx_id = b.trx_id
JOIN
INFORMATION_SCHEMA.INNODB_TRX r ON w.requesting_trx_id = r.trx_id
JOIN
INFORMATION_SCHEMA.INNODB_LOCKS t ON w.requested_lock_id = t.lock_id;使用 SHOW ENGINE INNODB STATUS
sql
-- 查看InnoDB引擎状态,包含死锁和锁等待信息
SHOW ENGINE INNODB STATUS\G关键信息解读:
TRANSACTIONS部分:显示当前事务LATEST DETECTED DEADLOCK部分:显示最近的死锁信息TRX HAS BEEN WAITING X SEC FOR THIS LOCK TO BE GRANTED:显示锁等待详情
使用 Performance Schema
sql
-- 启用Performance Schema锁监控
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME LIKE '%lock%';
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME LIKE '%lock%';
-- 查看锁等待事件
SELECT
EVENT_NAME,
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
SOURCE,
TIMER_WAIT / 1000000000 AS WAIT_TIME_SEC
FROM
performance_schema.data_locks
WHERE
LOCK_STATUS = 'WAITING';
-- 查看锁等待统计
SELECT
EVENT_NAME,
COUNT_STAR AS WAIT_COUNT,
SUM_TIMER_WAIT / 1000000000 AS TOTAL_WAIT_SEC,
AVG_TIMER_WAIT / 1000000000 AS AVG_WAIT_SEC
FROM
performance_schema.events_waits_summary_global_by_event_name
WHERE
EVENT_NAME LIKE '%lock%'
ORDER BY
TOTAL_WAIT_SEC DESC;锁等待分析工具
命令行工具
| 工具 | 用途 | 命令示例 |
|---|---|---|
| mysqladmin | 查看进程列表 | mysqladmin -u root -p processlist |
| pt-deadlock-logger | Percona工具,记录死锁 | pt-deadlock-logger h=localhost,u=root,p=password |
| pt-query-digest | 分析锁相关的慢查询 | `pt-query-digest --filter '($event-> |
| innotop | 实时监控InnoDB事务和锁 | innotop |
可视化工具
| 工具 | 特点 | 适用场景 |
|---|---|---|
| MySQL Workbench | 图形化查看锁等待 | 开发测试环境 |
| Percona Monitoring and Management (PMM) | 集成锁等待监控 | 生产环境 |
| MySQL Enterprise Monitor | 官方工具,锁等待告警 | 企业版用户 |
| DataDog | 云原生监控,锁等待指标 | 云环境 |
常见锁等待场景分析
表级锁等待
场景描述
-- 会话1:执行ALTER TABLE,持有表级锁
ALTER TABLE users ADD COLUMN age INT;
-- 会话2:执行查询,等待表级锁
SELECT * FROM users WHERE id = 1;分析方法
sql
-- 查看进程列表
SHOW FULL PROCESSLIST;
-- 结果示例
+----+------+-----------+-------+---------+------+---------------------------------+--------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+-------+---------+------+---------------------------------+--------------------------+
| 1 | root | localhost | test | Query | 10 | Waiting for table metadata lock | SELECT * FROM users WHERE id = 1 |
| 2 | root | localhost | test | Query | 20 | NULL | ALTER TABLE users ADD COLUMN age INT |
+----+------+-----------+-------+---------+------+---------------------------------+--------------------------+解决方案
sql
-- 1. 查找并终止阻塞会话
KILL 2;
-- 2. 或等待DDL操作完成
-- 3. 优化DDL操作(使用Online DDL)
ALTER TABLE users ADD COLUMN age INT, ALGORITHM=INPLACE, LOCK=NONE;行级锁等待
场景描述
sql
-- 会话1:开启事务,更新行但不提交
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 会话2:更新同一行,等待行锁
UPDATE users SET balance = balance + 50 WHERE id = 1;分析方法
sql
-- 查看InnoDB事务状态
SHOW ENGINE INNODB STATUS\G
-- 关键输出
---TRANSACTION 12345, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 2, OS thread handle 140123456789012, query id 123 localhost root updating
UPDATE users SET balance = balance + 50 WHERE id = 1
------- TRX HAS BEEN WAITING 10 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 4 n bits 72 index PRIMARY of table `test`.`users` trx id 12345 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000001; asc ;; # id = 1
1: len 6; hex 000000003039; asc 09;;
2: len 7; hex 800000019e0110; asc ;; # 事务ID和回滚指针
3: len 5; hex 726f6f7400; asc root ;; # username
4: len 4; hex 800003e8; asc ;; # balance = 1000
---TRANSACTION 12344, ACTIVE 20 sec
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 1, OS thread handle 140123456789011, query id 122 localhost root
START TRANSACTION
mysql tables in use 1, locked 1
13072019 10:00:00 UPDATE users SET balance = balance - 100 WHERE id = 1解决方案
sql
-- 1. 提交或回滚阻塞事务(会话1)
COMMIT;
-- 或
ROLLBACK;
-- 2. 终止阻塞会话
KILL 1;
-- 3. 优化事务,减少锁持有时间
-- 优化前
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 执行其他耗时操作
SLEEP(10);
COMMIT;
-- 优化后
-- 先执行耗时操作
SLEEP(10);
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;死锁
场景描述
sql
-- 会话1
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 会话2
START TRANSACTION;
UPDATE users SET balance = balance - 50 WHERE id = 2;
-- 会话1继续
UPDATE users SET balance = balance + 50 WHERE id = 2;
-- 会话2继续
UPDATE users SET balance = balance + 100 WHERE id = 1;分析方法
sql
-- 查看InnoDB状态中的死锁信息
SHOW ENGINE INNODB STATUS\G
-- 关键输出
LATEST DETECTED DEADLOCK
------------------------
2023-01-01 10:00:00 0x7f1234567890
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1, OS thread handle 140123456789011, query id 123 localhost root updating
UPDATE users SET balance = balance + 50 WHERE id = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 5 n bits 72 index PRIMARY of table `test`.`users` trx id 12345 lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000002; asc ;; # id = 2
...
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 2, OS thread handle 140123456789012, query id 124 localhost root updating
UPDATE users SET balance = balance + 100 WHERE id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 123 page no 5 n bits 72 index PRIMARY of table `test`.`users` trx id 12346 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000002; asc ;; # id = 2
...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 4 n bits 72 index PRIMARY of table `test`.`users` trx id 12346 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000001; asc ;; # id = 1
...
*** WE ROLL BACK TRANSACTION (2)解决方案
优化事务顺序:
sql-- 两个会话都按相同顺序更新行 -- 会话1 START TRANSACTION; UPDATE users SET balance = balance - 100 WHERE id = 1; UPDATE users SET balance = balance + 50 WHERE id = 2; COMMIT; -- 会话2 START TRANSACTION; UPDATE users SET balance = balance - 50 WHERE id = 1; UPDATE users SET balance = balance + 100 WHERE id = 2; COMMIT;减少事务长度:
- 将大事务拆分为小事务
- 减少事务中的操作数量
使用更细粒度的锁:
- 使用行级锁而非表级锁
- 合理设计索引,减少锁范围
设置合理的锁超时:
sql-- 设置锁等待超时时间(秒) SET GLOBAL innodb_lock_wait_timeout = 30;
间隙锁导致的锁等待
场景描述
sql
-- 表结构
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
INDEX idx_order_no (order_no)
) ENGINE=InnoDB;
-- 插入测试数据
INSERT INTO orders (order_no, amount) VALUES ('202301010001', 100.00);
INSERT INTO orders (order_no, amount) VALUES ('202301010003', 200.00);
INSERT INTO orders (order_no, amount) VALUES ('202301010005', 300.00);
-- 会话1:范围查询,产生间隙锁
START TRANSACTION;
SELECT * FROM orders WHERE order_no BETWEEN '202301010002' AND '202301010004' FOR UPDATE;
-- 会话2:插入间隙中的数据,等待锁
INSERT INTO orders (order_no, amount) VALUES ('202301010002', 150.00);分析方法
sql
-- 查看锁等待
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS\G
-- 查看锁信息
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS\G
-- 关键输出
--- 会话1持有的锁
LOCK_ID: 12345:123:4:5
LOCK_TRX_ID: 12345
LOCK_MODE: X, GAP
LOCK_TYPE: RECORD
LOCK_TABLE: `test`.`orders`
LOCK_INDEX: idx_order_no
LOCK_SPACE: 123
LOCK_PAGE: 4
LOCK_REC: 5
LOCK_DATA: '202301010003', 2
--- 会话2等待的锁
LOCK_ID: 12346:123:4:5
LOCK_TRX_ID: 12346
LOCK_MODE: X, INSERT INTENTION
LOCK_TYPE: RECORD
LOCK_TABLE: `test`.`orders`
LOCK_INDEX: idx_order_no
LOCK_SPACE: 123
LOCK_PAGE: 4
LOCK_REC: 5
LOCK_DATA: '202301010003', 2解决方案
使用更精确的查询条件:
sql-- 避免范围查询,使用精确查询 SELECT * FROM orders WHERE order_no = '202301010003' FOR UPDATE;调整事务隔离级别:
sql-- 使用READ COMMITTED隔离级别,减少间隙锁 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;优化索引设计:
- 确保查询使用最精确的索引
- 避免在范围查询中使用非唯一索引
使用INSERT ... ON DUPLICATE KEY UPDATE:
sql-- 避免插入间隙,使用已存在的唯一键 INSERT INTO orders (order_no, amount) VALUES ('202301010003', 150.00) ON DUPLICATE KEY UPDATE amount = 150.00;
锁等待优化策略
应用层面优化
减少事务持有锁的时间:
- 事务中先执行查询操作,最后执行更新/删除操作
- 避免在事务中调用外部服务
- 避免在事务中进行复杂计算
优化事务设计:
- 将大事务拆分为小事务
- 使用更细粒度的事务
- 合理设置事务隔离级别
使用乐观锁:
sql-- 添加版本号字段 ALTER TABLE users ADD COLUMN version INT DEFAULT 1; -- 更新时检查版本号 UPDATE users SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1;合理设计应用架构:
- 实现读写分离
- 使用缓存减少数据库访问
- 异步处理非关键操作
数据库层面优化
优化索引设计:
- 为查询条件添加合适的索引
- 避免全表扫描
- 减少锁的范围
优化SQL语句:
- 避免使用SELECT *
- 减少JOIN操作
- 避免在WHERE子句中使用函数
调整InnoDB配置:
ini[mysqld] # 减少锁等待超时时间 innodb_lock_wait_timeout = 30 # 启用死锁检测 innodb_deadlock_detect = ON # 优化并发事务处理 innodb_concurrency_tickets = 5000 innodb_thread_concurrency = 0 # 优化锁内存管理 innodb_lock_wait_timeout = 30使用合适的存储引擎:
- 对于高并发场景,使用InnoDB存储引擎
- 避免使用MyISAM存储引擎
定期维护表:
sql-- 优化表,减少碎片 OPTIMIZE TABLE users; -- 重建索引 ALTER TABLE users ENGINE=InnoDB;
运维层面优化
监控锁等待:
- 设置锁等待告警阈值
- 定期分析锁等待日志
- 监控死锁发生频率
定期分析慢查询:
- 优化导致锁等待的慢查询
- 识别并修复长期运行的事务
实施读写分离:
- 减少主库的并发访问
- 分散读压力
使用连接池:
- 管理数据库连接
- 避免连接泄漏
- 控制并发连接数
锁等待监控与告警
基于脚本的监控
bash
#!/bin/bash
# 配置信息
MYSQL_USER="root"
MYSQL_PASSWORD="password"
ALERT_EMAIL="admin@example.com"
THRESHOLD=10
# 查询锁等待时间超过阈值的事务
LOCK_WAITS=$(mysql -u $MYSQL_USER -p$MYSQL_PASSWORD -e "
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) wait_time,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM
INFORMATION_SCHEMA.INNODB_LOCK_WAITS w
JOIN
INFORMATION_SCHEMA.INNODB_TRX b ON w.blocking_trx_id = b.trx_id
JOIN
INFORMATION_SCHEMA.INNODB_TRX r ON w.requesting_trx_id = r.trx_id
WHERE
TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) > $THRESHOLD;")
# 检查是否有锁等待
if [ -n "$LOCK_WAITS" ]; then
# 发送告警邮件
echo -e "Subject: MySQL Lock Wait Alert\n\nLock waits detected:\n\n$LOCK_WAITS" | sendmail $ALERT_EMAIL
fi集成到监控平台
Prometheus + Grafana
配置Prometheus监控锁等待指标:
yamlscrape_configs: - job_name: 'mysql' static_configs: - targets: ['localhost:9104'] metrics_path: /metricsGrafana面板配置:
- 指标:
mysql_global_status_innodb_row_lock_waits - 图表类型:Time series
- 告警规则:当5分钟内锁等待次数超过10次时触发告警
- 指标:
Alertmanager告警规则:
yamlgroups: - name: mysql_lock_alerts rules: - alert: MySQLHighLockWaitRate expr: rate(mysql_global_status_innodb_row_lock_waits[5m]) > 10 for: 1m labels: severity: warning annotations: summary: "High MySQL lock wait rate" description: "MySQL server {{ $labels.instance }} has a high lock wait rate ({{ $value }} waits per second)"
Zabbix监控
创建监控项:
- 类型:Zabbix agent (active)
- 键值:
mysql.status[Innodb_row_lock_waits] - 数据类型:数字(无正负号)
创建触发器:
- 名称:MySQL High Lock Waits
- 表达式:
{mysql-server:mysql.status[Innodb_row_lock_waits].diff(5m)} > 50 - 严重性:警告
案例分析:电商系统锁等待优化
问题描述
某电商系统在促销活动期间出现大量锁等待,导致订单处理延迟,用户体验下降。
分析过程
查看锁等待状态:
sqlSHOW ENGINE INNODB STATUS\G识别锁等待热点:
sqlSELECT r.trx_query waiting_query, COUNT(*) wait_count, AVG(TIMESTAMPDIFF(SECOND, r.trx_started, NOW())) avg_wait_time FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS w JOIN INFORMATION_SCHEMA.INNODB_TRX r ON w.requesting_trx_id = r.trx_id GROUP BY r.trx_query ORDER BY wait_count DESC;发现主要问题:
- 订单表的UPDATE操作导致大量行锁等待
- 订单状态更新的事务持有锁时间过长
- 库存扣减和订单创建在同一个长事务中
优化方案
优化事务设计:
- 将长事务拆分为小事务
- 先扣减库存,再创建订单
- 减少事务持有锁的时间
优化SQL语句:
- 为订单表的状态字段添加索引
- 优化订单查询条件
- 减少SELECT *的使用
调整应用架构:
- 实现异步订单处理
- 使用缓存减少数据库访问
- 优化库存扣减逻辑
调整数据库配置:
ini[mysqld] innodb_lock_wait_timeout = 15 innodb_deadlock_detect = ON innodb_concurrency_tickets = 10000
优化效果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 锁等待次数 | 100+次/分钟 | <10次/分钟 |
| 平均锁等待时间 | 10+秒 | <1秒 |
| 订单处理延迟 | 5-10秒 | <1秒 |
| 系统吞吐量 | 100订单/秒 | 500订单/秒 |
总结
锁等待是MySQL高并发环境中常见的性能问题,通过合理的分析和优化,可以有效减少锁等待对系统性能的影响。作为DBA,应该:
- 定期分析锁等待:使用各种工具和方法监控锁等待状态
- 优化应用设计:减少事务持有锁的时间,优化SQL语句
- 优化数据库配置:调整InnoDB参数,优化索引设计
- 实施监控告警:及时发现和处理锁等待问题
- 持续优化:根据业务发展和系统负载变化,持续优化锁等待问题
通过综合运用这些方法,可以提高MySQL数据库的并发处理能力,确保系统在高负载下的稳定运行。
