数据库线上故障里,最难受的一类问题往往不是单条慢 SQL,而是锁冲突把请求链路拖成排队系统。它的典型表现是:
这类问题必须结合事务边界、索引命中、加锁顺序和业务流量一起看,不能只盯 SQL 语法。
数据库锁从不同维度可以分为多种类型:
表级锁(Table Lock)
行级锁(Row Lock)
页面锁(Page Lock)
共享锁(Shared Lock,S锁)
SELECT ... LOCK IN SHARE MODE(MySQL 8.0 后推荐使用 FOR SHARE)-- 事务 A
BEGIN;
SELECT * FROM orders WHERE id = 1001 LOCK IN SHARE MODE;
-- 此时可以读取,但不能修改
-- 事务 B(同时执行)
BEGIN;
SELECT * FROM orders WHERE id = 1001 LOCK IN SHARE MODE; -- 成功,共享锁兼容
UPDATE orders SET status = 'PAID' WHERE id = 1001; -- 阻塞,需要排他锁
COMMIT;排他锁(Exclusive Lock,X锁)
INSERT、UPDATE、DELETE 操作自动加排他锁SELECT ... FOR UPDATE-- 事务 A
BEGIN;
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 此时其他事务不能读也不能写该行
-- 事务 B(同时执行)
BEGIN;
SELECT * FROM orders WHERE id = 1001 FOR UPDATE; -- 阻塞,等待 A 释放锁
COMMIT;InnoDB 通过给索引上的索引项加锁来实现行锁,这意味着:
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
INDEX idx_age (age)
);
-- 情况1:通过主键更新,只锁定一行
UPDATE users SET name = 'Alice' WHERE id = 1; -- 行锁
-- 情况2:通过索引更新,锁定索引记录
UPDATE users SET name = 'Bob' WHERE age = 25; -- 行锁(锁定满足条件的索引项)
-- 情况3:无索引更新,锁住所有扫描到的行
UPDATE users SET name = 'Charlie' WHERE name = 'David'; -- name 列无索引,全表扫描,近似锁全表注意: 在实际开发中,务必确保 UPDATE、DELETE 语句的 WHERE 条件有合适的索引,否则会导致表锁,严重影响并发性能。
间隙锁(Gap Lock) 是 InnoDB 在可重复读隔离级别下,为了解决幻读问题而引入的锁机制。
什么是间隙?
间隙锁的作用:
-- 表数据:id = 1, 5, 10
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id > 3 AND id < 8 FOR UPDATE;
-- 锁定间隙 (1, 5) 和 (5, 10),以及 id=5 这一行
-- 事务 B(同时执行)
BEGIN;
INSERT INTO users VALUES(2, 'test'); -- 阻塞,因为 id=2 在间隙 (1, 5) 内
INSERT INTO users VALUES(8, 'test'); -- 阻塞,因为 id=8 在间隙 (5, 10) 内
COMMIT;间隙锁的特点:
Next-Key Lock 是行锁和间隙锁的组合,锁定一个索引记录以及该记录之前的间隙。
示例:
-- 表数据:id = 1, 5, 10
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id <= 5 FOR UPDATE;
-- Next-Key Lock 锁定:(-∞, 1]、(1, 5]
-- 包括:间隙 (-∞, 1)、行 1、间隙 (1, 5)、行 5
-- 事务 B
BEGIN;
INSERT INTO users VALUES(0, 'test'); -- 阻塞
INSERT INTO users VALUES(2, 'test'); -- 阻塞
INSERT INTO users VALUES(5, 'test'); -- 阻塞(行锁冲突)
INSERT INTO users VALUES(10, 'test'); -- 成功
COMMIT;Next-Key Lock 的退化:
SELECT * FROM users WHERE id = 5 FOR UPDATE(id 是主键),只锁定 id=5 这一行意向锁(Intention Lock) 是表级锁,用于协调行锁和表锁的关系。
为什么需要意向锁?
意向锁的类型:
意向锁的兼容性:
| 锁类型 | IS | IX | S | X |
|---|---|---|---|---|
| IS | √ | √ | √ | × |
| IX | √ | √ | × | × |
| S | √ | × | √ | × |
| X | × | × | × | × |
意向锁的自动获取:
-- 事务 A
BEGIN;
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 自动获取:意向排他锁(表级) + 排他行锁(行级)
-- 事务 B(同时执行)
BEGIN;
LOCK TABLE orders WRITE; -- 阻塞,因为表有意向排他锁
COMMIT;读未提交(READ UNCOMMITTED):
读已提交(READ COMMITTED):
可重复读(REPEATABLE READ) - InnoDB 默认:
串行化(SERIALIZABLE):
-- 不同隔离级别下的锁行为示例
-- 读已提交(RC)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 只锁定 id > 5 的已存在记录(id=10)
-- 其他事务可以在 (5, 10) 和 (10, +∞) 插入新记录
-- 可重复读(RR)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 锁定 (5, +∞) 的所有间隙和记录
-- 其他事务不能在 (5, +∞) 范围内插入新记录InnoDB 为了保证事务隔离,会在读写过程中加不同类型的锁。实际排障里最常遇到的是:
所以"看起来只更新一行"的 SQL,如果索引没命中,或者命中了范围条件,最后锁住的可能不是一行,而是一段索引范围。
锁冲突的本质是资源竞争: 多个事务同时请求同一资源的锁,且锁类型不兼容。
锁兼容性矩阵:
| 锁类型 | 共享锁(S) | 排他锁(X) |
|---|---|---|
| 共享锁(S) | 兼容 | 冲突 |
| 排他锁(X) | 冲突 | 冲突 |
当一个事务请求锁但被阻塞时,会等待锁释放。MySQL 提供了参数控制等待时间:
-- 查看锁等待超时时间(默认 50 秒)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 设置锁等待超时时间
SET innodb_lock_wait_timeout = 30; -- 30 秒
-- 超时后报错
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction优化建议:
锁冲突:
死锁:
事务 A: 持有锁(资源 1) → 等待锁(资源 2)
↑
|
事务 B: 持有锁(资源 2) ← 等待锁(资源 1)死锁的四个必要条件:
MySQL 提供了参数控制死锁检测:
-- 查看死锁检测开关(默认开启)
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- 关闭死锁检测(不推荐)
SET GLOBAL innodb_deadlock_detect = OFF;死锁检测的工作原理:
为什么有人关闭死锁检测?
一个事务里同时包含查询、更新、远程调用、循环处理、消息发送,导致锁持有时间被业务逻辑拉长。
典型症状:
SHOW PROCESSLIST 出现大量 Waiting for lock危险写法示例:
BEGIN;
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 这里夹杂 RPC、库存检查、优惠计算、发送消息
-- 耗时可能长达数秒
UPDATE orders SET status = 'PAID' WHERE id = 1001;
COMMIT;问题分析:
FOR UPDATE 本身,而在于事务里放了太多非数据库动作优化方案:
-- 方案1:缩小事务范围,只包含必要的数据库操作
BEGIN;
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
UPDATE orders SET status = 'PAID' WHERE id = 1001;
COMMIT;
-- 将 RPC、消息发送等移到事务外
-- 方案2:使用乐观锁,减少锁持有时间
SELECT version, status FROM orders WHERE id = 1001;
-- 执行业务逻辑(无锁)
BEGIN;
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = 1001 AND version = 查询到的version;
COMMIT;UPDATE orders
SET status = 'CANCELLED'
WHERE user_id = 1001 AND status = 'INIT';问题分析:
如果 user_id, status 没有合适的联合索引,数据库可能需要扫描大量记录,最终锁住更大范围。结果是:
示例:
-- 表结构
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
status VARCHAR(20),
amount DECIMAL(10, 2),
INDEX idx_user (user_id) -- 只有 user_id 的单列索引
);
-- 数据:用户 1001 有 1000 个订单
-- 事务 A
BEGIN;
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 1001 AND status = 'INIT';
-- 需要扫描 user_id=1001 的所有订单
-- 在可重复读隔离级别下,可能锁定大量记录和间隙
-- 事务 B(同时执行)
BEGIN;
UPDATE orders SET status = 'PAID'
WHERE user_id = 1001 AND id = 500;
-- 阻塞,因为被事务 A 的锁覆盖
COMMIT;优化方案:
-- 方案1:创建联合索引
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 方案2:分批更新
-- 每次只更新一小批,减少锁范围
UPDATE orders
SET status = 'CANCELLED'
WHERE user_id = 1001 AND status = 'INIT'
LIMIT 100;
-- 方案3:使用主键范围更新
-- 先查询出需要更新的主键
SELECT id FROM orders
WHERE user_id = 1001 AND status = 'INIT';
-- 然后按主键更新
UPDATE orders SET status = 'CANCELLED' WHERE id IN (1001, 1002, ...);库存、账户余额、优惠券计数、全局序号等数据经常会落到单行高频更新:
UPDATE stock
SET available = available - 1
WHERE sku_id = 9001 AND available > 0;在秒杀、抢券、扣库存场景下,大量事务会竞争同一行,导致:
性能瓶颈分析:
优化方案:
方案1:库存分桶
-- 创建多个库存桶
CREATE TABLE stock_bucket (
id INT PRIMARY KEY,
sku_id INT,
bucket_no INT, -- 桶编号
available INT,
INDEX idx_sku_bucket (sku_id, bucket_no)
);
-- 初始化:sku_id=9001 的库存分散到 10 个桶
INSERT INTO stock_bucket VALUES
(1, 9001, 1, 100),
(2, 9001, 2, 100),
(3, 9001, 3, 100),
(4, 9001, 4, 100),
(5, 9001, 5, 100),
(6, 9001, 6, 100),
(7, 9001, 7, 100),
(8, 9001, 8, 100),
(9, 9001, 9, 100),
(10, 9001, 10, 100);
-- 扣库存时随机选择一个桶
UPDATE stock_bucket
SET available = available - 1
WHERE sku_id = 9001 AND bucket_no = FLOOR(1 + RAND() * 10)
AND available > 0;
-- 查询总库存时汇总
SELECT SUM(available) FROM stock_bucket WHERE sku_id = 9001;方案2:使用缓存预扣减
// Redis 预扣库存
public boolean deductStock(Long skuId) {
String key = "stock:" + skuId;
Long remaining = redisTemplate.opsForValue().decrement(key);
if (remaining < 0) {
redisTemplate.opsForValue().increment(key);
return false; // 库存不足
}
// 异步同步到数据库
asyncUpdateDatabase(skuId);
return true;
}
// 定期异步同步到数据库
@Scheduled(fixedRate = 1000)
public void syncStockToDatabase() {
// 批量更新数据库
batchUpdateStock();
}方案3:乐观锁
-- 查询当前库存
SELECT id, available, version FROM stock WHERE sku_id = 9001;
-- 扣减库存(使用版本号)
UPDATE stock
SET available = available - 1, version = version + 1
WHERE sku_id = 9001 AND version = 查询到的version AND available > 0;
-- 如果影响行数为 0,说明并发冲突,业务重试方案4:令牌桶限流
// 使用 Guava RateLimiter 限流
RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒 1000 次
public boolean deductStock(Long skuId) {
if (!rateLimiter.tryAcquire()) {
return false; // 被限流
}
// 执行数据库扣减
return doDeductStock(skuId);
}-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE,
email VARCHAR(100)
);
-- 事务 A
BEGIN;
INSERT INTO users(username, email) VALUES('alice', 'alice@example.com');
-- 未提交
-- 事务 B(同时执行)
BEGIN;
INSERT INTO users(username, email) VALUES('alice', 'bob@example.com');
-- 阻塞,等待唯一索引锁
COMMIT;问题分析:
优化方案:
INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 处理冲突-- 使用 INSERT IGNORE 忽略重复
INSERT IGNORE INTO users(username, email) VALUES('alice', 'bob@example.com');
-- 使用 ON DUPLICATE KEY UPDATE 更新
INSERT INTO users(username, email) VALUES('alice', 'bob@example.com')
ON DUPLICATE KEY UPDATE email = VALUES(email);场景: 转账业务,两个用户互相转账
事务 A:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 执行其他业务逻辑
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;事务 B(同时执行):
BEGIN;
UPDATE account SET balance = balance - 50 WHERE id = 2;
-- 执行其他业务逻辑
UPDATE account SET balance = balance + 50 WHERE id = 1;
COMMIT;死锁分析:
id = 1id = 2id = 2,等待 B 释放id = 1,等待 A 释放解决方案:
-- 统一加锁顺序:都按 id 从小到大加锁
BEGIN;
-- 先获取两个账户的锁,按 id 排序
SELECT * FROM account WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- 然后执行转账
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;优化建议:
事务 A:
BEGIN;
SELECT id FROM orders
WHERE status = 'INIT'
ORDER BY id
LIMIT 100
FOR UPDATE;
-- 处理这批订单
UPDATE orders SET status = 'PROCESSING' WHERE id IN (选中的id);
COMMIT;事务 B(同时执行):
BEGIN;
SELECT id FROM orders
WHERE status = 'INIT'
ORDER BY create_time -- 不同的排序字段
LIMIT 100
FOR UPDATE;
-- 处理这批订单
UPDATE orders SET status = 'PROCESSING' WHERE id IN (选中的id);
COMMIT;死锁分析:
解决方案:
-- 方案1:统一排序规则
-- 所有批量查询都使用相同的排序字段(如主键)
SELECT id FROM orders
WHERE status = 'INIT'
ORDER BY id
LIMIT 100
FOR UPDATE;
-- 方案2:使用分布式锁
-- 在应用层加分布式锁,保证同一时间只有一个批量任务执行String lockKey = "batch_process_orders";
if (redisLock.tryLock(lockKey, 30, TimeUnit.SECONDS)) {
try {
// 执行批量处理
} finally {
redisLock.unlock(lockKey);
}
}-- 方案3:跳过锁策略
-- 使用 SKIP LOCKED 跳过已被锁定的记录(MySQL 8.0+)
SELECT id FROM orders
WHERE status = 'INIT'
ORDER BY id
LIMIT 100
FOR UPDATE SKIP LOCKED;场景: 订单插入与状态更新并发执行
事务 A(插入订单):
BEGIN;
INSERT INTO orders(id, user_id, status, amount)
VALUES(1001, 100, 'INIT', 100.00);
COMMIT;事务 B(更新订单状态):
BEGIN;
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 100 AND status = 'INIT';
COMMIT;死锁分析:
解决方案:
-- 方案1:缩小更新范围
-- 查询出具体的订单 id,然后按主键更新
SELECT id FROM orders
WHERE user_id = 100 AND status = 'INIT';
UPDATE orders SET status = 'CANCELLED' WHERE id IN (具体id列表);
-- 方案2:调整隔离级别
-- 使用读已提交隔离级别,避免间隙锁
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 方案3:业务层面控制
-- 插入和批量更新错峰执行场景: 父子表关联更新
-- 表结构
CREATE TABLE orders (
id INT PRIMARY KEY,
status VARCHAR(20),
KEY idx_status (status)
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT,
amount DECIMAL(10, 2),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- 事务 A
BEGIN;
UPDATE orders SET status = 'CANCELLED' WHERE id = 1001;
-- 需要检查子表 order_items 是否有关联记录
COMMIT;
-- 事务 B
BEGIN;
INSERT INTO order_items(id, order_id, amount) VALUES(1, 1001, 100.00);
-- 需要检查父表 orders 对应记录
COMMIT;死锁分析:
解决方案:
-- 方案1:避免使用外键约束
-- 在应用层维护数据一致性
-- 适合高并发场景
-- 方案2:统一操作顺序
-- 先操作父表,再操作子表
-- 所有涉及父子表的事务都遵循这个顺序
-- 方案3:使用存储过程封装操作
-- 在存储过程中使用事务和锁保证顺序
DELIMITER //
CREATE PROCEDURE update_order_with_items(
IN p_order_id INT,
IN p_status VARCHAR(20)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
UPDATE orders SET status = p_status WHERE id = p_order_id;
-- 其他操作
COMMIT;
END //
DELIMITER ;场景: 锁住不存在的记录后并发插入
-- 表结构
CREATE TABLE logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
action VARCHAR(50),
KEY idx_user (user_id)
);
-- 数据:id = 1, 5, 10
-- 事务 A
BEGIN;
SELECT * FROM logs WHERE id = 3 FOR UPDATE;
-- id=3 不存在,可重复读下对间隙 (1, 5) 加间隙锁
-- 事务 B(同时执行)
BEGIN;
INSERT INTO logs(id, user_id, action) VALUES(3, 100, 'login');
-- 插入位置落在间隙 (1, 5) 内,插入意向锁被 A 的间隙锁阻塞,等待
-- 事务 A 继续
INSERT INTO logs(id, user_id, action) VALUES(3, 100, 'login');
-- A 也向间隙 (1, 5) 插入,该插入请求排在 B 的请求之后
-- B 在等 A 的间隙锁,A 在等排在队首的 B → 循环等待,死锁!
-- ERROR 1213 (40001): Deadlock found when trying to get lock死锁分析:
解决方案:
-- 方案1:降低隔离级别到 READ COMMITTED(无间隙锁,插入不会被间隙锁阻塞)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 方案2:需要"锁住不存在的记录"时,先插入占位行,再对其加行锁
-- 把间隙锁问题变成普通行锁
INSERT INTO logs(id, user_id, action) VALUES(3, 100, 'init');
COMMIT;
BEGIN;
SELECT * FROM logs WHERE id = 3 FOR UPDATE; -- 行锁,不影响其他间隙的插入
-- 方案3:控制插入并发
-- 避免大量事务同时向同一索引范围插入,可按业务键分片路由死锁特征:
Deadlock found when trying to get lock// Java 应用捕获死锁异常
try {
orderService.processOrder(orderId);
} catch (DeadlockLoserDataAccessException e) {
// 死锁异常,可以重试
log.warn("Deadlock detected, retrying...", e);
orderService.processOrder(orderId); // 重试
}锁等待特征:
Lock wait timeout exceeded// Java 应用捕获锁等待超时异常(Spring 将 MySQL 1205 翻译为 CannotAcquireLockException)
try {
orderService.processOrder(orderId);
} catch (CannotAcquireLockException e) {
// 锁等待超时
log.error("Lock wait timeout", e);
throw new BusinessException("系统繁忙,请稍后重试");
}排查优先级:
排查死锁时先执行:
SHOW ENGINE INNODB STATUS\G重点关注信息:
LATEST DETECTED DEADLOCK
------------------------
2026-03-30 10:30:15 0x700012345000
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 2 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 100, OS thread handle 123456, query id 1000 localhost root updating
UPDATE account SET balance = balance - 100 WHERE id = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 4 n bits 72 index PRIMARY of table `test`.`account`
trx id 12345 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 1 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 101, OS thread handle 123457, query id 1001 localhost root updating
UPDATE account SET balance = balance - 50 WHERE id = 2
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 4 n bits 72 index PRIMARY of table `test`.`account`
trx id 12346 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 5 n bits 72 index PRIMARY of table `test`.`account`
trx id 12346 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
*** WE ROLL BACK TRANSACTION (1)解读关键信息:
事务信息:
TRANSACTION 12345: 事务 IDACTIVE 2 sec: 事务已执行时间starting index read: 当前正在执行的操作锁信息:
lock_mode X: 排他锁locks rec but not gap: 记录锁,不是间隙锁waiting: 正在等待锁关键问题:
实际阅读顺序:
查询锁等待关系:
-- MySQL 8.0+:使用 sys.innodb_lock_waits 视图(底层来自 performance_schema.data_lock_waits)
SELECT waiting_pid, waiting_query, blocking_pid, blocking_query, wait_age_secs
FROM sys.innodb_lock_waits;
-- MySQL 5.7 及以下:使用 information_schema.innodb_lock_waits
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
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;查看长时间运行的事务:
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds,
trx_mysql_thread_id,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;查看当前活跃的锁:
-- MySQL 8.0+
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
THREAD_ID,
PROCESSLIST_ID
FROM performance_schema.data_locks
WHERE OBJECT_SCHEMA = 'your_database';
-- MySQL 5.7 及以下
SELECT * FROM information_schema.innodb_locks;除了 SHOW ENGINE INNODB STATUS,还要一起看:
1. SHOW PROCESSLIST
SHOW FULL PROCESSLIST;
-- 关注以下状态:
-- Waiting for table metadata lock: 等待元数据锁
-- Waiting for table level lock: 等待表锁
-- Waiting for lock: 等待行锁
-- Sending data: 可能是大查询
-- Statistics: 可能在计算执行计划2. 慢查询日志
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 超过 2 秒记录
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录未使用索引的查询
-- 查看慢查询日志
-- Linux: /var/log/mysql/slow.log
-- 或使用 mysqldumpslow 工具分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log3. 业务日志
// 在业务代码中记录事务执行时间
@Around("execution(* com.example.service.*.*(..))")
public Object logTransactionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
if (duration > 1000) { // 超过 1 秒记录
log.warn("Slow transaction: {} took {} ms",
joinPoint.getSignature(), duration);
}
}
}4. 索引设计检查
-- 检查表的索引
SHOW INDEX FROM orders;
-- 使用 EXPLAIN 分析 SQL
EXPLAIN UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 1001 AND status = 'INIT';
-- 检查索引使用情况
SELECT
TABLE_SCHEMA,
TABLE_NAME,
INDEX_NAME,
CARDINALITY,
SEQ_IN_INDEX
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database'
ORDER BY TABLE_NAME, INDEX_NAME;MySQL Enterprise Monitor
Percona Toolkit
# 安装 Percona Toolkit
yum install percona-toolkit
# 使用 pt-deadlock-logger 持续监控死锁
pt-deadlock-logger h=localhost,u=root,p=password --daemonize --log /var/log/deadlock.log
# 使用 pt-query-digest 分析慢查询
pt-query-digest /var/log/mysql/slow.log监控脚本示例:
#!/bin/bash
# monitor_locks.sh - 监控锁等待情况
while true; do
echo "=== $(date) ==="
mysql -uroot -ppassword -e "
SELECT
r.trx_id waiting_trx_id,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_query blocking_query,
TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) wait_seconds
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
WHERE TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) > 5;
"
sleep 10
done核心原则: 事务内只保留必要的数据库操作,其他操作移到事务外。
优化示例:
// × 错误示例:长事务
@Transactional
public void processOrder(Long orderId) {
// 1. 查询订单
Order order = orderRepository.findById(orderId);
// 2. 检查库存(可能耗时)
boolean stockAvailable = inventoryService.checkStock(order.getSkuId());
if (!stockAvailable) {
throw new BusinessException("库存不足");
}
// 3. 调用支付接口(可能耗时,可能失败)
PaymentResult result = paymentService.pay(order);
// 4. 发送消息通知(可能耗时)
notificationService.sendNotification(order);
// 5. 更新订单状态
order.setStatus("PAID");
orderRepository.save(order);
}
// √ 正确示例:缩短事务
public void processOrder(Long orderId) {
// 事务外操作
Order order = orderRepository.findById(orderId);
boolean stockAvailable = inventoryService.checkStock(order.getSkuId());
if (!stockAvailable) {
throw new BusinessException("库存不足");
}
PaymentResult result = paymentService.pay(order);
// 只在事务内执行必要的数据库操作
updateOrderStatus(orderId, "PAID");
// 异步发送通知
notificationService.sendNotificationAsync(order);
}
@Transactional
public void updateOrderStatus(Long orderId, String status) {
orderRepository.updateStatus(orderId, status);
}批量处理优化:
// × 错误示例:大批量一次性处理
@Transactional
public void batchUpdateOrders(List<Long> orderIds) {
List<Order> orders = orderRepository.findByIds(orderIds);
for (Order order : orders) {
order.setStatus("PROCESSING");
// 其他复杂逻辑
}
orderRepository.saveAll(orders);
}
// √ 正确示例:分批处理
public void batchUpdateOrders(List<Long> orderIds) {
// 分批处理,每批 100 条
Lists.partition(orderIds, 100).forEach(batch -> {
processBatch(batch);
});
}
@Transactional
public void processBatch(List<Long> batch) {
List<Order> orders = orderRepository.findByIds(batch);
orders.forEach(order -> order.setStatus("PROCESSING"));
orderRepository.saveAll(orders);
}核心原则: 涉及多行、多表资源时,统一按照相同顺序访问。
实现方案:
// × 错误示例:不同业务逻辑,加锁顺序不一致
// 业务 A:从账户 1 转账到账户 2
public void transferA(Long fromId, Long toId, BigDecimal amount) {
accountRepository.deductBalance(fromId, amount);
accountRepository.addBalance(toId, amount);
}
// 业务 B:从账户 2 转账到账户 1
public void transferB(Long fromId, Long toId, BigDecimal amount) {
accountRepository.deductBalance(fromId, amount);
accountRepository.addBalance(toId, amount);
}
// √ 正确示例:统一加锁顺序
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 按 ID 排序,保证加锁顺序一致
Long firstId = Math.min(fromId, toId);
Long secondId = Math.max(fromId, toId);
// 先锁定 ID 小的账户
accountRepository.lockById(firstId);
// 再锁定 ID 大的账户
accountRepository.lockById(secondId);
// 执行转账逻辑
if (fromId < toId) {
accountRepository.deductBalance(fromId, amount);
accountRepository.addBalance(toId, amount);
} else {
accountRepository.addBalance(toId, amount);
accountRepository.deductBalance(fromId, amount);
}
}使用框架强制顺序:
// 使用 AOP 强制按顺序加锁
@Aspect
@Component
public class LockOrderAspect {
@Around("@annotation(orderedLock)")
public Object enforceLockOrder(ProceedingJoinPoint joinPoint, OrderedLock orderedLock) throws Throwable {
// 获取方法参数中的 ID 列表
Object[] args = joinPoint.getArgs();
List<Long> ids = extractIds(args);
// 排序后按顺序加锁
Collections.sort(ids);
for (Long id : ids) {
lockManager.lock(id);
}
try {
return joinPoint.proceed();
} finally {
// 按相反顺序释放锁
Collections.reverse(ids);
for (Long id : ids) {
lockManager.unlock(id);
}
}
}
}
// 使用注解标记需要按顺序加锁的方法
@OrderedLock
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 业务逻辑
}核心原则: 确保更新条件有合适的索引,避免全表扫描和大规模锁。
索引设计最佳实践:
-- × 错误示例:更新条件无索引
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
status VARCHAR(20),
create_time DATETIME
);
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 1001 AND status = 'INIT';
-- 需要全表扫描,锁住大量记录
-- √ 正确示例:创建合适的索引
CREATE INDEX idx_user_status ON orders(user_id, status);
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 1001 AND status = 'INIT';
-- 使用索引,只锁住满足条件的记录索引选择原则:
区分度高的列放在前面
覆盖索引减少回表
-- 创建覆盖索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
-- 查询可以使用索引覆盖
SELECT id, create_time FROM orders
WHERE user_id = 1001 AND status = 'INIT';
-- 不需要回表,性能更高-- × 索引失效示例
-- 使用函数
UPDATE orders SET status = 'CANCELLED'
WHERE DATE(create_time) = '2026-03-30';
-- 隐式类型转换
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = '1001'; -- user_id 是 INT,字符串会导致索引失效
-- 使用 OR
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 1001 OR status = 'INIT';
-- √ 正确示例
-- 使用范围查询
UPDATE orders SET status = 'CANCELLED'
WHERE create_time >= '2026-03-30 00:00:00'
AND create_time < '2026-03-31 00:00:00';
-- 类型匹配
UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 1001;
-- OR 两侧列都有索引时可能走 index_merge,否则拆成两条语句分别执行
UPDATE orders SET status = 'CANCELLED' WHERE user_id = 1001;
UPDATE orders SET status = 'CANCELLED' WHERE status = 'INIT' AND user_id != 1001;核心策略: 降低单行热点并发,分散锁竞争。
方案对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 库存分桶 | 库存、计数器 | 简单有效,提升并发 | 需要定期同步桶数据 |
| 缓存预扣 | 秒杀、抢购 | 性能极高,减少数据库压力 | 需要保证数据一致性 |
| 乐观锁 | 低并发冲突 | 无锁等待,实现简单 | 高冲突时重试频繁 |
| 令牌桶 | 流量削峰 | 简单易用,保护系统 | 不解决根本问题 |
库存分桶完整示例:
-- 创建库存桶表
CREATE TABLE stock_bucket (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sku_id BIGINT NOT NULL,
bucket_no INT NOT NULL,
available INT NOT NULL DEFAULT 0,
total INT NOT NULL DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_sku_bucket (sku_id, bucket_no),
KEY idx_sku (sku_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 初始化库存(分 10 个桶)
DELIMITER //
CREATE PROCEDURE init_stock_buckets(
IN p_sku_id BIGINT,
IN p_total_stock INT,
IN p_bucket_count INT
)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE per_bucket INT;
SET per_bucket = CEIL(p_total_stock / p_bucket_count);
WHILE i <= p_bucket_count DO
INSERT INTO stock_bucket(sku_id, bucket_no, available, total)
VALUES(p_sku_id, i, per_bucket, per_bucket);
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
-- 初始化 sku_id=9001,总库存 1000,分 10 个桶
CALL init_stock_buckets(9001, 1000, 10);
-- 扣减库存(随机选择桶)
DELIMITER //
CREATE FUNCTION deduct_stock(
p_sku_id BIGINT,
p_quantity INT
) RETURNS BOOLEAN
NOT DETERMINISTIC
MODIFIES SQL DATA
BEGIN
DECLARE v_bucket_no INT;
DECLARE v_available INT;
DECLARE v_affected INT DEFAULT 0;
DECLARE retry_count INT DEFAULT 0;
DECLARE max_retry INT DEFAULT 3;
retry_loop: LOOP
-- 随机选择一个桶
SET v_bucket_no = FLOOR(1 + RAND() * 10);
-- 检查库存
SELECT available INTO v_available
FROM stock_bucket
WHERE sku_id = p_sku_id AND bucket_no = v_bucket_no;
IF v_available >= p_quantity THEN
-- 扣减库存
UPDATE stock_bucket
SET available = available - p_quantity
WHERE sku_id = p_sku_id
AND bucket_no = v_bucket_no
AND available >= p_quantity;
SET v_affected = ROW_COUNT();
IF v_affected > 0 THEN
RETURN TRUE;
END IF;
END IF;
SET retry_count = retry_count + 1;
IF retry_count >= max_retry THEN
RETURN FALSE;
END IF;
END LOOP;
RETURN FALSE;
END //
DELIMITER ;
-- 查询总库存
SELECT sku_id, SUM(available) AS total_available
FROM stock_bucket
GROUP BY sku_id;
-- 库存同步(定期将各桶库存同步到主表)
DELIMITER //
CREATE PROCEDURE sync_stock_to_main()
BEGIN
-- 同步到主库存表
INSERT INTO stock(sku_id, available, total)
SELECT sku_id, SUM(available), SUM(total)
FROM stock_bucket
GROUP BY sku_id
ON DUPLICATE KEY UPDATE
available = VALUES(available),
total = VALUES(total);
END //
DELIMITER ;核心原则: 死锁被回滚后可以重试,但前提是业务具备幂等性。
重试实现:
// 使用 Spring Retry 实现自动重试
@Retryable(
value = {DeadlockLoserDataAccessException.class, CannotAcquireLockException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus("PROCESSING");
orderRepository.save(order);
}
// 重试失败后的兜底处理
@Recover
public void recover(DeadlockLoserDataAccessException e, Long orderId) {
log.error("Deadlock retry failed for order: {}", orderId, e);
// 发送告警
alertService.sendAlert("Deadlock retry failed: " + orderId);
// 记录到待处理队列
pendingQueueService.add(orderId);
}幂等性保证:
// 使用幂等键保证重试安全
@Transactional
public void processOrder(String idempotentKey, Long orderId) {
// 检查幂等键是否已处理
if (idempotentKeyRepository.existsByKey(idempotentKey)) {
log.info("Order already processed: {}", idempotentKey);
return;
}
// 执行业务逻辑
Order order = orderRepository.findById(orderId);
order.setStatus("PROCESSING");
orderRepository.save(order);
// 记录幂等键
idempotentKeyRepository.save(new IdempotentKey(idempotentKey, orderId));
}
// 或者使用数据库唯一约束
@Transactional
public void processOrder(String idempotentKey, Long orderId) {
try {
// 插入幂等键(唯一约束)
idempotentKeyRepository.insert(idempotentKey, orderId);
// 执行业务逻辑
Order order = orderRepository.findById(orderId);
order.setStatus("PROCESSING");
orderRepository.save(order);
} catch (DuplicateKeyException e) {
log.info("Order already processed: {}", idempotentKey);
// 重复请求,直接返回
}
}重试最佳实践:
设置合理的重试次数
只重试可重试的异常
记录重试日志
设置兜底机制
// 完整的重试和兜底机制
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PendingQueueService pendingQueueService;
@Autowired
private AlertService alertService;
private static final int MAX_RETRY = 3;
public void processOrderWithRetry(Long orderId) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
try {
processOrder(orderId);
return; // 成功,直接返回
} catch (DeadlockLoserDataAccessException e) {
retryCount++;
log.warn("Deadlock detected for order: {}, retry count: {}", orderId, retryCount);
if (retryCount >= MAX_RETRY) {
// 重试失败,记录到待处理队列
log.error("Max retry reached for order: {}", orderId);
pendingQueueService.add(orderId, "DEADLOCK_RETRY_FAILED");
alertService.sendAlert("Order process failed after retry: " + orderId);
return;
}
// 指数退避
try {
Thread.sleep(100 * (1 << retryCount));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
}
} catch (Exception e) {
// 其他异常,不重试
log.error("Unexpected error for order: {}", orderId, e);
throw new BusinessException("处理失败");
}
}
}
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus("PROCESSING");
orderRepository.save(order);
}
}错误认知:
正确理解:
合理目标:
错误认知:
UPDATE ... WHERE id = 100 只锁 id=100 这一行正确理解:
-- 情况1:唯一索引等值查询,只锁一行
UPDATE users SET name = 'Alice' WHERE id = 100;
-- 只锁 id=100 这一行
-- 情况2:范围查询,锁定多行和间隙
UPDATE users SET status = 'ACTIVE' WHERE id > 100 AND id < 200;
-- 锁定 id 在 (100, 200) 的所有行和间隙
-- 情况3:无索引,近似锁全表
UPDATE users SET status = 'ACTIVE' WHERE name = 'Alice';
-- name 列无索引,全表扫描,锁住所有行
-- 情况4:间隙锁,锁定间隙
SELECT * FROM users WHERE id > 100 FOR UPDATE;
-- 锁定 id > 100 的所有行和间隙排查方法:
-- 查看当前事务持有的锁
SELECT * FROM performance_schema.data_locks
WHERE THREAD_ID = CONNECTION_ID();错误认知:
正确理解:
重试适用场景:
重试不适用场景:
重试的风险:
最佳实践:
// √ 正确示例:有限重试 + 幂等保证
@Retryable(
value = {DeadlockLoserDataAccessException.class},
maxAttempts = 3, // 最多 3 次
backoff = @Backoff(delay = 100, multiplier = 2) // 指数退避
)
@Transactional
public void processOrder(String idempotentKey, Long orderId) {
// 幂等性检查
if (isProcessed(idempotentKey)) {
return;
}
// 业务逻辑
doProcess(orderId);
// 记录幂等键
markAsProcessed(idempotentKey);
}错误认知:
正确理解:
大事务的问题:
合理的事务范围:
// × 错误示例:事务包含所有操作
@Transactional
public void processOrder(Long orderId) {
// 1. 查询订单
Order order = orderRepository.findById(orderId);
// 2. 调用第三方接口(可能耗时数秒)
PaymentResult result = paymentService.pay(order);
// 3. 发送邮件通知
emailService.sendNotification(order);
// 4. 更新订单状态
order.setStatus("PAID");
orderRepository.save(order);
}
// √ 正确示例:缩小事务范围
public void processOrder(Long orderId) {
// 事务外操作
Order order = orderRepository.findById(orderId);
PaymentResult result = paymentService.pay(order);
emailService.sendNotification(order);
// 只在事务内执行必要的数据库操作
updateOrderStatus(orderId, "PAID");
}
@Transactional
public void updateOrderStatus(Long orderId, String status) {
orderRepository.updateStatus(orderId, status);
}最终一致性方案:
对于跨服务的业务,使用最终一致性方案:
// 使用消息队列实现最终一致性
public void processOrder(Long orderId) {
// 1. 本地事务:创建订单,发送消息(事务消息)
Order order = createOrder(orderId);
// 2. 发送消息到 MQ
messageQueueService.send("order.paid", order);
// 3. 其他服务消费消息,执行相应逻辑
// 库存服务:扣减库存
// 通知服务:发送通知
}
// 库存服务消费消息
@RabbitListener(queues = "order.paid")
public void handleOrderPaid(Order order) {
inventoryService.deductStock(order.getSkuId(), order.getQuantity());
}错误认知:
正确理解:
读操作也会加锁的情况:
-- 1. LOCK IN SHARE MODE(共享锁)
SELECT * FROM orders WHERE id = 1001 LOCK IN SHARE MODE;
-- 加共享锁,会阻塞写操作
-- 2. FOR UPDATE(排他锁)
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 加排他锁,会阻塞所有操作
-- 3. 串行化隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM orders WHERE id = 1001;
-- 自动加共享锁快照读与当前读:
-- 快照读(不加锁)
SELECT * FROM orders WHERE id = 1001;
-- 读的是快照数据,不加锁
-- 当前读(加锁)
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 读的是最新数据,加排他锁
UPDATE orders SET status = 'PAID' WHERE id = 1001;
-- 当前读,加排他锁读取导致死锁的案例:
-- 事务 A
BEGIN;
SELECT * FROM orders WHERE id = 1001 LOCK IN SHARE MODE;
-- 加共享锁
-- 事务 B
BEGIN;
SELECT * FROM orders WHERE id = 1001 LOCK IN SHARE MODE;
-- 也加共享锁,兼容
-- 事务 A 继续
UPDATE orders SET status = 'PAID' WHERE id = 1001;
-- 需要排他锁,等待 B 释放共享锁
-- 事务 B 继续
UPDATE orders SET status = 'CANCELLED' WHERE id = 1001;
-- 也需要排他锁,等待 A 释放共享锁
-- 死锁!错误认知:
正确理解:
关闭死锁检测的风险:
死锁检测的开销:
合理方案:
-- √ 保持死锁检测开启
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- Value: ON
-- √ 设置合理的锁等待超时时间
SET innodb_lock_wait_timeout = 10; -- 10 秒
-- √ 在应用层做好重试
-- 见 7.5 节重试机制何时考虑关闭死锁检测:
1. MySQL 有哪些锁类型?分别是什么作用?
答案:
按粒度划分:
按类型划分:
InnoDB 特有:
2. InnoDB 的行锁是如何实现的?
答案:
InnoDB 通过给索引上的索引项加锁来实现行锁:
示例:
-- 通过主键更新:行锁
UPDATE users SET name = 'Alice' WHERE id = 1;
-- 无索引更新:全表扫描,锁住所有行,近似锁全表
UPDATE users SET name = 'Alice' WHERE name = 'Bob';3. 什么是间隙锁?为什么需要间隙锁?
答案:
间隙锁(Gap Lock)是 InnoDB 在可重复读隔离级别下,为了解决幻读问题而引入的锁机制。
作用:
示例:
-- 表数据:id = 1, 5, 10
BEGIN;
SELECT * FROM users WHERE id > 3 AND id < 8 FOR UPDATE;
-- 锁定间隙 (1, 5) 和 (5, 10)
INSERT INTO users VALUES(2, 'test'); -- 阻塞
INSERT INTO users VALUES(8, 'test'); -- 阻塞4. 什么是意向锁?为什么需要意向锁?
答案:
意向锁是表级锁,用于协调行锁和表锁的关系。
为什么需要:
类型:
自动获取:
5. 死锁的四个必要条件是什么?如何破坏?
答案:
四个必要条件:
破坏方法:
实际应用:
6. MySQL 如何检测和处理死锁?
答案:
死锁检测:
选择回滚事务的策略:
配置参数:
-- 查看死锁检测开关
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- 关闭死锁检测(不推荐)
SET GLOBAL innodb_deadlock_detect = OFF;查看死锁信息:
SHOW ENGINE INNODB STATUS\G
-- 查看 LATEST DETECTED DEADLOCK 部分7. 不同隔离级别下的锁行为有什么区别?
答案:
| 隔离级别 | 读操作 | 写操作 | 间隙锁 | 问题 |
|---|---|---|---|---|
| 读未提交 | 不加锁 | 行锁 | 无 | 脏读、不可重复读、幻读 |
| 读已提交 | 不加锁(快照读) | 行锁 | 无 | 不可重复读、幻读 |
| 可重复读 | 不加锁(快照读) | 行锁+间隙锁 | 有 | 无 |
| 串行化 | 共享锁 | 排他锁 | 有 | 无 |
示例:
-- 读已提交(RC):只锁记录
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 只锁 id > 5 的已存在记录
-- 可重复读(RR):锁记录+间隙
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 锁定 (5, +∞) 的所有间隙和记录8. 为什么长事务会放大锁冲突?
答案:
原因:
示例:
-- 长事务:持有锁 5 秒
BEGIN;
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 执行业务逻辑(5 秒)
UPDATE orders SET status = 'PAID' WHERE id = 1001;
COMMIT;
-- 影响:
-- 假设 QPS=100,5 秒内有 500 个请求
-- 这 500 个请求都会被阻塞优化:
9. 为什么统一更新顺序能减少死锁?
答案:
原理:
示例:
-- × 不同顺序:可能死锁
-- 事务 A
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 事务 B
UPDATE account SET balance = balance - 50 WHERE id = 2;
UPDATE account SET balance = balance + 50 WHERE id = 1;
-- √ 统一顺序:不会死锁
-- 都按 id 从小到大
SELECT * FROM account WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- 然后执行转账10. SHOW ENGINE INNODB STATUS 应该重点看什么?
答案:
重点关注:
LATEST DETECTED DEADLOCK: 最近一次死锁信息阅读顺序:
示例输出解读:
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 2 sec
UPDATE account SET balance = balance - 100 WHERE id = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS ... lock_mode X ... waiting
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 1 sec
UPDATE account SET balance = balance - 50 WHERE id = 2
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS ... lock_mode X
-- 解读:
-- 事务 1 执行 UPDATE id=1,等待锁
-- 事务 2 执行 UPDATE id=2,持有锁
-- 需要看后续信息确认循环等待11. 热点行竞争如何优化?
答案:
问题:
优化方案:
库存分桶
缓存预扣减
乐观锁
令牌桶限流
选择建议:
12. 如何排查线上的锁冲突问题?
答案:
排查步骤:
判断锁等待还是死锁
查看锁等待情况
SELECT * FROM information_schema.innodb_lock_waits;
SELECT * FROM information_schema.innodb_trx;分析死锁信息
SHOW ENGINE INNODB STATUS\G
-- 查看 LATEST DETECTED DEADLOCK查看长时间事务
SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW())
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;检查索引设计
SHOW INDEX FROM 表名;
EXPLAIN UPDATE ... WHERE ...;结合业务日志
13. 如何设计一个高并发的库存扣减系统?
答案:
架构设计:
前端层
应用层
数据库层
完整流程:
1. 用户请求 → 限流检查
2. Redis 扣库存 → 成功/失败
3. 成功 → 创建订单(异步)
4. 定时任务 → 同步 Redis 到数据库
5. 数据库分桶 → 提升并发能力关键点:
14. 如何保证多表更新的一致性和避免死锁?
答案:
方案:
统一加锁顺序
// 按 ID 排序后加锁
List<Long> ids = Arrays.asList(orderId, paymentId, inventoryId);
Collections.sort(ids);
for (Long id : ids) {
lockManager.lock(id);
}使用分布式锁
// 业务层先获取分布式锁
if (redisLock.tryLock("process_order:" + orderId, 30, TimeUnit.SECONDS)) {
try {
// 执行多表更新
updateOrder(orderId);
updatePayment(paymentId);
updateInventory(inventoryId);
} finally {
redisLock.unlock("process_order:" + orderId);
}
}使用事务管理器
// Spring 事务管理
@Transactional
public void processOrder(Long orderId) {
// 统一在事务内执行
// Spring 保证原子性
}最终一致性方案
// 使用消息队列
// 1. 更新订单表(本地事务)
// 2. 发送消息到 MQ
// 3. 其他服务消费消息,更新各自表选择建议:
15. 如何监控和告警数据库锁问题?
答案:
监控指标:
实现方案:
MySQL Performance Schema
-- 启用锁监控
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/lock%';
-- 查询锁统计
SELECT * FROM performance_schema.events_waits_summary_by_instance
WHERE EVENT_NAME LIKE 'wait/lock%';自定义监控脚本
#!/bin/bash
# 定期检查锁等待
while true; do
mysql -e "
SELECT COUNT(*) AS lock_wait_count
FROM information_schema.innodb_lock_waits;
" | mail -s "Lock Wait Alert" admin@example.com
sleep 60
donePrometheus + Grafana
# prometheus.yml
- job_name: 'mysql'
static_configs:
- targets: ['localhost:9104']
# Grafana Dashboard
- 锁等待次数图表
- 锁等待时间图表
- 死锁次数图表告警规则
合理设计索引
控制事务范围
统一加锁顺序
避免长事务
// √ 推荐:小事务
@Transactional
public void updateOrderStatus(Long orderId, String status) {
orderRepository.updateStatus(orderId, status);
}
// × 避免:大事务
@Transactional
public void processOrder(Long orderId) {
// 包含大量业务逻辑
}使用乐观锁
// 低冲突场景使用乐观锁
@Transactional
public boolean deductStock(Long skuId, Integer quantity) {
Stock stock = stockRepository.findById(skuId);
if (stock.getAvailable() < quantity) {
return false;
}
int affected = stockRepository.updateWithVersion(
skuId, quantity, stock.getVersion()
);
return affected > 0;
}做好重试机制
@Retryable(
value = DeadlockLoserDataAccessException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional
public void processOrder(Long orderId) {
// 业务逻辑
}监控锁指标
设置合理参数
-- 锁等待超时时间
SET innodb_lock_wait_timeout = 10;
-- 死锁检测(保持开启)
SET innodb_deadlock_detect = ON;
-- 事务隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;定期排查
热点数据分散
读写分离
异步处理
最终一致性
| 特性 | 旧版(本文编写时,MySQL 5.7) | 当前(MySQL 8.0/8.4 LTS) |
|---|---|---|
| 默认字符集 | utf8(需显式配置 utf8mb4) | utf8mb4(MySQL 8.0 起默认) |
| 索引 | 普通 B+Tree | 降序索引、隐藏索引、函数索引(8.0+) |
| SQL 能力 | 常规查询 | 递归 CTE、窗口函数(8.0+) |
| 版本策略 | 5.7 | 8.0(主流)/ 8.4 LTS / 9.x(创新版) |
| Java 驱动 | mysql-connector-java 5.x/8.0 | mysql-connector-j 8.x/9.x |
本文基于 MySQL 5.7 编写,核心概念(索引、事务、锁、MVCC、InnoDB)在 8.0/8.4 中依然适用;8.0 的默认字符集、隐藏索引与 SQL 增强(CTE/窗口函数)是升级后的主要差异。