Mysql基础(②锁)
分类 | 锁类型 | 说明 | 示例 / 特点 |
---|---|---|---|
按粒度 | 行级锁 | 仅锁定单行数据,粒度最细 | MySQL InnoDB 的 FOR UPDATE 行锁,并发性能高,锁冲突概率低 |
表级锁 | 锁定整张表,粒度最粗 | MySQL MyISAM 的表锁、InnoDB 的 AUTO_INCREMENT 锁,并发性能低 | |
页级锁 | 锁定数据页(数据库存储的基本单位),粒度介于行和表之间 | MySQL BDB 引擎,平衡并发和锁开销,但可能出现页内锁冲突 | |
按功能 | 共享锁(S 锁) | 允许事务读取数据,多个事务可同时持有 | SELECT ... LOCK IN SHARE MODE (MySQL),读操作不阻塞其他读操作 |
排他锁(X 锁) | 允许事务修改数据,仅一个事务可持有 | SELECT ... FOR UPDATE (MySQL),写操作阻塞其他所有读写操作 | |
意向锁 | 表级锁,标识事务对表中 rows 的锁类型(为行锁做准备) | InnoDB 的意向共享锁(IS)、意向排他锁(IX),自动添加,无需手动操作 | |
乐观锁 | 非真正的锁,通过版本号或时间戳实现,假设冲突概率低 | UPDATE ... WHERE version = ? ,适合读多写少场景,无锁等待开销 |
行级锁
你打开编辑(BEGIN 事务),系统自动给这条客户记录加行锁,同事此时点编辑会卡住(等你释放锁)。你改完点保存(COMMIT),锁释放,同事才能编辑。
先创建一张简单的表并插入数据:
-- 创建表(InnoDB引擎支持行级锁)
CREATE TABLE `user_account` (`id` int(11) NOT NULL PRIMARY KEY,`name` varchar(50) NOT NULL,`balance` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- 插入3条数据(3行)
INSERT INTO `user_account` VALUES
(1, '张三', 1000),
(2, '李四', 2000),
(3, '王五', 3000);
演示 1:行级锁只锁单行,不影响其他行
我们打开两个 MySQL 窗口(模拟两个事务),按步骤执行:
窗口 1(事务 A):
-- 开启事务
BEGIN;-- 修改id=1的行(自动加行级锁)
UPDATE user_account SET balance = balance - 100 WHERE id = 1;-- 不提交事务(锁不释放)
窗口 2(事务 B):
-- 开启事务
BEGIN;-- 尝试修改id=1的行(会被阻塞,因为被事务A锁定)
UPDATE user_account SET balance = balance + 100 WHERE id = 1;
-- 执行后会卡住,等待锁释放
窗口 2 继续操作:
-- 尝试修改id=2的行(可以正常执行,不受影响)
UPDATE user_account SET balance = balance - 200 WHERE id = 2;
-- 结果:Query OK, 1 row affected (0.00 sec)
-- 说明:行级锁只锁了id=1,不影响id=2的行
最后步骤:
窗口 1 提交事务(释放锁):COMMIT;
窗口 2 之前卡住的语句会立即执行,然后也提交:COMMIT;
演示 2:验证锁释放后的结果
两个事务都提交后,查询表数据:
SELECT * FROM user_account;
运行结果:
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900 | -- 事务A扣了100
| 2 | 李四 | 1800 | -- 事务B扣了200
| 3 | 王五 | 3000 | -- 未操作,不变
+----+--------+---------+
关键结论
行级锁只会锁定被操作的那一行(如 id=1),其他行(如 id=2、3)可以正常操作。
锁会一直持有,直到事务提交(
COMMIT
)或回滚(ROLLBACK
)才释放。如果两个事务操作同一行,后执行的会等待前一个事务释放锁,避免数据混乱。
表级锁
数据库中常见的表级锁有两种:
读锁(共享锁,Shared Lock):多个事务可以同时持有,持有期间只能读数据,不能修改,也不允许其他事务加写锁。
写锁(排他锁,Exclusive Lock):只有一个事务可以持有,持有期间可以读写数据,其他事务既不能读(加读锁)也不能写(加写锁)。
假设我们有一张表 user
,数据如下:
id | name | balance |
---|---|---|
1 | 张三 | 1000 |
2 | 李四 | 2000 |
示例 1:加读锁(共享锁)
事务 A(终端 1)操作:
-- 开启会话,给user表加读锁(只能读,不能写)
LOCK TABLES user READ;-- 可以正常读取数据
SELECT * FROM user WHERE id = 1;
-- 结果:id=1, name=张三, balance=1000
事务 B(终端 2)操作:
-- 其他事务可以加读锁并读取(读锁共享)
LOCK TABLES user READ;
SELECT * FROM user WHERE id = 1; -- 正常返回结果-- 但不能修改数据(会被阻塞,直到锁释放)
UPDATE user SET balance = 1500 WHERE id = 1;
-- 此处会卡住,等待事务A释放锁
事务 A 释放锁:
-- 释放锁
UNLOCK TABLES;
此时事务 B 的 UPDATE
语句会立即执行(如果没有其他锁)。
示例 2:加写锁(排他锁)
事务 A(终端 1)操作:
-- 给user表加写锁(独占,可读写)
LOCK TABLES user WRITE;-- 可以读取和修改数据
UPDATE user SET balance = 1500 WHERE id = 1;
SELECT * FROM user WHERE id = 1; -- 结果:balance=1500
事务 B(终端 2)操作:
-- 尝试读数据(加读锁)会被阻塞
LOCK TABLES user READ;
-- 此处卡住,无法执行,直到事务A释放锁-- 尝试修改数据也会被阻塞
UPDATE user SET balance = 2500 WHERE id = 2;
-- 同样卡住
事务 A 释放锁:
UNLOCK TABLES;
此时事务 B 的操作会立即执行。
页级锁
假设我们使用支持页级锁的数据库(如启用 BDB 引擎的 MySQL,仅作演示),创建一张表并插入数据:
-- 创建使用BDB引擎的表(支持页级锁)
CREATE TABLE `page_demo` (`id` int(11) NOT NULL PRIMARY KEY,`name` varchar(50) NOT NULL
) ENGINE=BDB DEFAULT CHARSET=utf8;-- 插入8行数据,假设数据库按4行一页存储:
-- 页1:id=1~4,页2:id=5~8
INSERT INTO `page_demo` VALUES
(1, '数据1'), (2, '数据2'), (3, '数据3'), (4, '数据4'),
(5, '数据5'), (6, '数据6'), (7, '数据7'), (8, '数据8');
场景 1:操作同一页的数据(页 1)
终端 1(事务 A):修改页 1 中的数据(id=2)
-- 开启事务(BDB引擎会自动加页级锁)
BEGIN;
-- 修改id=2(属于页1),触发页1的锁定
UPDATE page_demo SET name='修改后的数据2' WHERE id=2;
-- 不提交事务(保持锁)
终端 2(事务 B):尝试操作页 1 中的其他数据(id=3)
BEGIN;
-- 尝试修改id=3(同属页1),会被阻塞(页1被锁)
UPDATE page_demo SET name='修改后的数据3' WHERE id=3;
-- 执行后会卡住,等待终端1释放锁
终端 2 继续尝试:操作另一页(页 2)的数据(id=5)
-- 修改id=5(属于页2),可以正常执行(页2未被锁)
UPDATE page_demo SET name='修改后的数据5' WHERE id=5;
-- 结果:Query OK, 1 row affected (0.00 sec)
释放锁:终端 1 提交事务
COMMIT; -- 释放页1的锁
此时终端 2 中被阻塞的修改 id=3 的语句会立即执行。
运行结果:
+----+-----------------+
| id | name |
+----+-----------------+
| 1 | 数据1 | -- 未操作
| 2 | 修改后的数据2 | -- 事务A修改
| 3 | 修改后的数据3 | -- 事务B修改(锁释放后执行)
| 4 | 数据4 | -- 未操作
| 5 | 修改后的数据5 | -- 事务B修改(页2未被锁)
| 6 | 数据6 | -- 未操作
| 7 | 数据7 | -- 未操作
| 8 | 数据8 | -- 未操作
+----+-----------------+