用两个窗口观察行级锁和表级锁:同时修改同一行、给表加写锁和读锁、让读锁保持到事务结束,最后制造一次死锁。
MySQL 8.0 和 MySQL Workbench,机房电脑已装好。所有语句在 Workbench 的 SQL 编辑区中执行。
Ctrl+Enter 执行光标所在的一句,Ctrl+Shift+Enter 执行全部。Mac 上把 Ctrl 换成 ⌘。
输入法切到英文。表名、字段名不加引号,字符串值才用单引号。
与实验一相同的脚本,执行后三个账户回到初始余额。
-- 第 9 章实验数据:银行账户库 bankaccount 和账户表 account -- 三次实验共用。整段执行一次即可,想从头再做时重新执行。 CREATE DATABASE IF NOT EXISTS bankaccount DEFAULT CHARACTER SET utf8mb4; USE bankaccount; DROP TABLE IF EXISTS account; -- id 账户号,R 余额,datetime 最近一次操作的时间 CREATE TABLE account ( id VARCHAR(20) NOT NULL, R DECIMAL(10,2) NOT NULL, datetime DATETIME, PRIMARY KEY (id) ); INSERT INTO account VALUES ('account1', 500.00, '2026-09-01 09:00:00'), ('account2', 900.00, '2026-09-01 09:00:00'), ('account3', 900.00, '2026-09-01 09:00:00'); SELECT * FROM account;
要用两个窗口:在 Workbench 首页再点一次同一个连接,会打开第二个连接标签页。两个标签页是两个独立的会话,下面称为窗口 A 和窗口 B,两个窗口都先执行 USE bankaccount;。每一步只执行这一步的语句:选中这几行,按 Ctrl+Shift+Enter。标着“等待”的一步,语句会一直处于执行状态。这时要在 20 秒内切到另一个窗口做下一步,否则 Workbench 会报超时。
START TRANSACTION; UPDATE account SET R = R - 100 WHERE id = 'account1';
A 给 account1 这一行加上了 X 锁
START TRANSACTION; UPDATE account SET R = R - 200 WHERE id = 'account1';
等待
COMMIT;A 释放锁,B 的修改完成
COMMIT; SELECT * FROM account WHERE id = 'account1';
account1 是 200
| UPDATE 加锁 | 修改一行之前,MySQL 自动给这一行加 X 锁,直到事务结束才释放 |
| B 等待 | X 锁不相容,B 要等 A 的事务结束 |
| R = R - 200 | B 等到的是 A 提交后的 400,再减 200,两次取款都算数 |
标着“等待”的一步,语句会一直处于执行状态。这时要在 20 秒内切到另一个窗口做下一步,否则 Workbench 会报超时。
LOCK TABLES account WRITE; SELECT * FROM account;
A 加上写锁后自己可以读写
SELECT * FROM account;
等待
UNLOCK TABLES;
A 解锁,B 的查询马上返回结果
| LOCK TABLES account WRITE | 给整张表加写锁,也就是 X 锁 |
| B 等待 | 表上有 X 锁时,其他会话连读都要等待 |
| UNLOCK TABLES | 释放当前会话加的全部表锁 |
标着“等待”的一步,语句会一直处于执行状态。这时要在 20 秒内切到另一个窗口做下一步,否则 Workbench 会报超时。
LOCK TABLES account READ; SELECT * FROM account; -- 加了读锁,自己也不能修改,这一句会报错 1099 UPDATE account SET R = 800 WHERE id = 'account1';
报错 1099
SELECT * FROM account; LOCK TABLES account READ; UNLOCK TABLES;
B 能读,也能再加读锁
UPDATE account SET R = 600 WHERE id = 'account2';
等待
UNLOCK TABLES;
A 解锁后,B 的修改完成
| LOCK TABLES account READ | 给整张表加读锁,也就是 S 锁 |
| 报错 1099 | Table was locked with a READ lock,加了读锁的会话只能读 |
| B 再加读锁 | S 锁和 S 锁相容,几个会话可以同时加读锁 |
| B 修改等待 | 表上有 S 锁时不能加 X 锁,要等读锁释放 |
标着“等待”的一步,语句会一直处于执行状态。这时要在 20 秒内切到另一个窗口做下一步,否则 Workbench 会报超时。
START TRANSACTION; SELECT * FROM account WHERE id = 'account2' FOR SHARE;
A 给 account2 加 S 锁
UPDATE account SET R = R + 100 WHERE id = 'account2';
等待
SELECT * FROM account WHERE id = 'account2' FOR SHARE; COMMIT;
A 两次读到的相同。提交后 S 锁释放,B 的修改完成
| FOR SHARE | 读取时给这些行加 S 锁,保持到事务结束 |
| 三级封锁协议 | 读之前加 S 锁,直到事务结束才释放,其他事务不能修改,两次读的结果一定相同 |
| B 等待 | B 要加 X 锁,与 A 的 S 锁不相容 |
标着“等待”的一步,语句会一直处于执行状态。这时要在 20 秒内切到另一个窗口做下一步,否则 Workbench 会报超时。
START TRANSACTION; UPDATE account SET R = R - 100 WHERE id = 'account1';
A 锁住 account1
START TRANSACTION; UPDATE account SET R = R - 100 WHERE id = 'account2';
B 锁住 account2
UPDATE account SET R = R + 100 WHERE id = 'account2';
等待 B 释放 account2
-- 这一句会报错 1213 UPDATE account SET R = R + 100 WHERE id = 'account1';
报错 1213,B 的事务被回滚
COMMIT; SELECT * FROM account;
A 的两次修改都完成
| 死锁 | A 等 B 的 account2,B 等 A 的 account1,形成循环等待 |
| 报错 1213 | Deadlock found,MySQL 发现死锁后回滚其中一个事务,另一个继续执行 |
| 避免死锁 | 约定所有事务按相同的顺序访问数据,例如都先改 account1 再改 account2 |