前情提要
什么是事务?
定义:事务是一组必须全部成功或全部失败的数据库操作(例如:转账需要同时完成扣款和收款)
ACID 特性:
- 原子性(Atomicity):事务要么全部执行,要么全部回滚
- 一致性(Consistency):事务执行后数据库必须处于合法状态(如账户余额不能为负数)
- 隔离性(Isolation):事务之间互相隔离,通过锁机制实现
- 持久性(Durability):事务提交后数据永久保存
为什么需要锁?
典型问题:两个用户同时购买最后一个商品,无锁会导致超卖
锁的作用:像“单线程”操作关键数据,防止并发冲突
锁的分类
按锁的粒度分类
全局锁(Global Lock)
定义:锁定整个 MySQL 实例,所有数据库的写操作会被阻塞。
使用场景:全库逻辑备份(如 mysqldump
)。
语法:
FLUSH TABLES WITH READ LOCK; -- 加全局读锁
UNLOCK TABLES; -- 释放锁
注意事项:
在 MySQL 里并没有像全局读锁
FLUSH TABLES WITH READ LOCK
那样专门用于全局写操作的全局写锁语句全局锁会导致业务停摆,备份推荐用
mysqldump --single-transaction
(仅限 InnoDB)
表级锁(Table-Level Lock)
定义:锁定整张表,分为读锁(共享锁)和写锁(排他锁)
使用场景:
- MyISAM 引擎默认锁机制
- 修改表结构(如
ALTER TABLE
)
语法:
LOCK TABLES table_name READ; -- 加读锁
LOCK TABLES table_name WRITE; -- 加写锁
UNLOCK TABLES; -- 释放锁
代码示例:
-- 会话A
LOCK TABLES orders READ; -- 会话A给orders表加了读锁
-- 会话B
INSERT INTO orders VALUES (...); -- 会话B给orders表添加数据被阻塞
行级锁(Row-Level Lock)
定义:锁定单行或多行数据(InnoDB 核心特性)
使用场景:高并发下精确控制数据访问
语法:
SELECT * FROM users WHERE id=1 FOR UPDATE; -- 排他锁
SELECT * FROM users WHERE id=1 FOR SHARE; -- 共享锁
注意事项:
- 行锁必须通过索引实现,否则退化为表锁
- 示例:
UPDATE users SET age=20 WHERE name='张三'
(若name
无索引,锁整个表)
页级锁(Page-Level Lock)
定义:锁定数据页(16KB),仅 BDB 引擎支持
现状:已被淘汰,了解即可
按锁的类型分类
共享锁(S Lock)
定义:允许多事务读同一数据,阻止写操作
类比:多人同时阅读同一本书,但不能修改
兼容性:与共享锁兼容,与排他锁冲突
排他锁(X Lock)
定义:独占锁,阻止其他事务读写
类比:你独占了书桌写作业,其他人不能看也不能改
兼容性:与所有锁冲突
意向锁(Intention Lock)
定义:表级锁,快速判断表中是否有行锁
- IS 锁:事务计划加行级共享锁
- IX 锁:事务计划加行级排他锁
作用:避免逐行检查锁状态,提升效率
按锁的实现方式分类
悲观锁
定义:假设并发冲突会发生,先加锁再操作
实现:SELECT ... FOR UPDATE
适用场景:写操作频繁(如秒杀)
乐观锁
定义:假设无冲突,提交时检查版本号
实现:
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=100 AND version=current_version;
适用场景:读多写少(如文章阅读量统计)
InnoDB 行级锁的四种实现
记录锁(Record Lock)
定义:锁定索引中的一条记录
触发条件:精确匹配唯一索引(如 WHERE id=1)
代码示例:
-- 锁定 id=1 的行
SELECT * FROM users WHERE id=1 FOR UPDATE;
间隙锁(Gap Lock)
定义:锁定索引记录之间的间隙(如 id
在 (5,10) 之间)
触发条件:范围查询或查询不存在的值
作用:防止其他事务插入数据(解决幻读)
仅生效于:REPEATABLE READ
隔离级别
临键锁(Next-Key Lock)
定义:记录锁 + 间隙锁,锁定左开右闭区间
触发条件:非唯一索引的范围查询
代码示例:
-- 锁定 age 在 (20,25] 区间
SELECT * FROM users WHERE age > 20 AND age <=25 FOR UPDATE;
插入意向锁(Insert Intention Lock)
定义:插入前标记目标位置的间隙锁
特点:不阻塞其他插入意向锁,但阻塞间隙锁
事务隔离级别与锁的关系
隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁机制特点 |
---|---|---|---|---|
Read Uncommitted | 可能 | 可能 | 可能 | 不加锁,直接读最新数据(可能脏读) |
Read Committed | 避免 | 可能 | 可能 | 行锁,无间隙锁 |
Repeatable Read | 避免 | 避免 | 可能 | 默认使用临键锁,防止幻读 |
Serializable | 避免 | 避免 | 避免 | 所有 SELECT 自动转为 FOR SHARE |
建议:默认使用 REPEATABLE READ
,高并发写入场景可尝试 READ COMMITTED
锁的监控与死锁处理
查看锁状态
代码示例:
-- 当前持有的锁
SELECT * FROM information_schema.INNODB_LOCKS;
-- 锁等待关系
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 事务状态(包含锁信息)
SELECT * FROM information_schema.INNODB_TRX;
死锁自动处理
InnoDB 自动检测死锁,回滚代价较小的事务
手动处理:
SHOW ENGINE INNODB STATUS; -- 查看死锁日志
KILL [transaction_id]; -- 终止事务
避免死锁的黄金法则
按固定顺序访问资源(如按主键排序更新)
尽量使用短事务,快速提交
为高频查询字段添加索引
常见问题
为什么我的 SQL 执行卡住?
可能原因:锁等待超时
解决步骤:
查询阻塞事务:
SELECT * FROM information_schema.INNODB_TRX;
终止阻塞者:
KILL 12345; -- 替换为实际事务ID
操作案例
索引缺失导致锁升级
创建无索引表:
CREATE TABLE test (name VARCHAR(20)); INSERT INTO test VALUES ('Alice'), ('Bob');
会话A:
BEGIN; UPDATE test SET name='Charlie' WHERE name='Alice'; -- 无索引,触发表锁
会话B:
UPDATE test SET name='Dave' WHERE name='Bob'; -- 被阻塞!
结论:无索引的更新操作会锁住整张表
总结
索引优化:
- 所有 WHERE 条件字段尽量加索引
- 避免全表扫描(如
LIKE '%xxx%'
)
事务设计:
- 事务尽量简短,避免在事务中执行网络请求
- 批量操作分批次提交
锁超时设置:
SET innodb_lock_wait_timeout = 5; -- 等待5秒自动放弃
锁替代方案:
- 乐观锁:通过版本号控制
- 队列机制:异步处理高并发请求