同一张卡,同时刷两次
引例 一张卡,两个人用
你和妈妈共用一张银行卡,卡里有 1000 元。同一秒钟,你在便利店刷了 100 元,妈妈在菜市场刷了 200 元。卡里应该剩 700 元——可数据库要是没管好,剩下的可能是 800 元,甚至 900 元。
很多人同时使用同一份数据,怎样保证结果算得对?这就是本章要解决的问题。
串行:一个用完下一个再用,不会出错,但大家都在排队。
并发:大家一起用,速度快,但要有规矩,免得互相干扰。数据库选的是并发,再用事务、隔离级别、封锁这三样来定规矩。
事务:要么全做,要么全不做
引例 转账做到一半,停电了
张三给李四转 100 元,数据库要做两件事:张三的余额减 100,李四的余额加 100。如果第一件做完就停电了,张三的钱扣了,李四却没收到——100 元凭空消失。
这两步必须绑在一起:要么都做完,要么都不做。绑在一起的这一组操作,就叫事务。
- 开始START TRANSACTION;
- 提交COMMIT; -- 全部生效
- 回滚ROLLBACK; -- 全部撤销
原子性 要么全做,要么全不做。转到一半停电,重启后自动撤销已扣的钱。
一致性 事务前后,数据都是合理的。转账前后,两人的钱加起来不变。
隔离性 同时运行的事务互不干扰。别人看不到「已经扣了、还没加」的中间状态。
持久性 提交以后,结果不会丢。COMMIT 之后马上停电,重启后转账仍然有效。
记住后面讲的隔离级别和封锁,都是为了守住 ACID 里的 I(隔离性)。
四种并发问题
引例 生活里的四件事
刷卡算错,只是其中一种。再想三件事:同事说「老板要给大家涨工资」,你转身就告诉了家人,第二天老板说只是随口一提;早上看天气预报是 25℃,中午再看变成了 15℃;数班级群的人数,转头再数,多了两个人。
这四件事,正好对应事务并发时的四种错误。点任意一种,按时间顺序看它是怎么发生的。
两个事务改同一个数据,后写的把先写的覆盖了。两个人同时刷卡
读到别的事务还没提交的数据;对方一回滚,读到的就是假的。老板「随口一提」的涨工资
同一事务里读同一行两次,值不一样:中间别人改了,并且提交了。天气预报中途更新了
同一事务里按同一条件查两次,行数不一样:中间别人插入或删除了行。群里多了两个人
易混判断时先看对方提交没有:没提交就被读到,是脏读。已经提交了,再看变的是值(不可重复读)还是行数(幻象读)。
| 时间 | 事务 T1 | 余额 R | 事务 T2 |
|---|
隔离级别:挡住哪些问题
引例 看通知的四种规矩
学院要发一份通知。最松的规矩:别人还在起草,你就能看到草稿;紧一点:只看盖了章的正式通知;再紧一点:开始办事时拍张照,之后一直看照片,中途改了也不影响你;最紧:大家排队,一个人看完,下一个再看。
规矩越严,看到的越可靠,大家也等得越久。隔离级别就是这四档规矩。
| 隔离级别 | 脏读 | 不可重复读 | 幻象读 |
|---|---|---|---|
| READ UNCOMMITTED读未提交 | ✗ | ✗ | ✗ |
| READ COMMITTED读已提交 | ✓ | ✗ | ✗ |
| REPEATABLE READ可重复读MySQL 默认 | ✓ | ✓ | ✗ |
| SERIALIZABLE串行化 | ✓ | ✓ | ✓ |
✓ 表示能挡住,✗ 表示挡不住。丢失更新四个级别都能挡住,表里不列。
-- 查看当前级别
SELECT @@transaction_isolation;
-- 把当前连接设为「读已提交」
SET SESSION TRANSACTION ISOLATION LEVEL
READ COMMITTED;SESSION 只改当前这个连接;换成 GLOBAL 改的是以后新建的连接。
封锁:X 锁和 S 锁
引例 自习室占座
想用自习室的一个座位,先把书包放上去,别人就不会来坐;走的时候拿走书包,座位才空出来。墙上的课表则不同:大家可以围着一起看,但有人在看的时候,谁也不能把它撕下来改。
放书包就是加锁,拿走就是释放锁。要改的东西得独占,只看的东西可以共享。
排它锁,也叫写锁。加锁的事务能读能写,其他事务读、写都要等。
共享锁,也叫读锁。大家都能读,谁都不能写,加锁的事务自己也不能写。
| 已经加了 ↓ 再申请 → | S 锁 | X 锁 |
|---|---|---|
| S 锁 | 可以 | 等待 |
| X 锁 | 等待 | 等待 |
- S 锁LOCK TABLES account READ;
- X 锁LOCK TABLES account WRITE;
- 释放UNLOCK TABLES;
了解:封锁协议什么时候加锁、什么时候放
| 协议 | 规则 | 能防止 |
|---|---|---|
| 一级 | 写之前加 X 锁,事务结束才放 | 丢失更新 |
| 二级 | 一级 + 读之前加 S 锁,读完就放 | 再加上脏读 |
| 三级 | 一级 + 读之前加 S 锁,事务结束才放 | 再加上不可重复读 |
另外,锁的范围可大可小:表级锁锁整张表,行级锁只锁用到的行。MySQL 的 InnoDB 默认用行级锁。
死锁:互相等,谁也动不了
引例 独木桥上碰面
两个人从独木桥的两头同时走上桥,在中间碰面了。你等他退回去,他等你退回去,谁都不肯让——两个人就一直僵在桥上。
锁也会带来这种僵局:两个事务各拿着一把锁,又都在等对方手里的那一把。
规定加锁顺序,例如都先锁学生表、再锁成绩表。后来的只是排队,不会互相卡住。
一开始就把要用的锁全部申请,要么都拿到,要么都不拿。
真的发生了也不用慌:MySQL 会自动发现死锁,回滚其中一个事务(报错 1213),让另一个接着执行。被回滚的那个,重新执行一次即可。
| 时间 | 事务 T1 | 事务 T2 |
|---|