目录
OK,我们redis的内容先告一段落,因为后面的就是分布式锁还有集群的问题了。我们接下来就先讲另一种数据库——mysql,这是一种关系数据库。关于这个就不从头开始讲起,直接进入mysql的金丹境,深入了解mysql的事务和隔离级别。
事务的特性(ACID)
事务是是一组操作的集合,他是一个不可分割的单位,事务内的操作会被作为一整个整体,可以理解为他们相依为命,同生共死,同成功,同失败。事务有四大特性,简称ACID,下面详细介绍。
原子性(Atomicity)
事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
eg:比如说在银行转账过程中,转出者的减钱操作和转入者的加钱操作,必须是同时成功同时失败的,不能出现一个减钱成功而另一个没有加钱或者加钱成功减钱失败的情况。
一致性(Consistency)
事务完成时,必须使所有的数据都保持一致状态。
eg:还是银行转账的场景,A扣了一千块钱,那么B就必须加一千块钱。不能多加到两三千、或者少加到500,甚至减钱的情况。
隔离性(Isonlation)
数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
eg:比如在银行查账过程中,A查账是一个事务,但此时又另一个人B给A正在转钱也是一个事务,转钱的操作正在运行还没有彻底完成,那么A查的账就不能是B转完钱后的金额。万一B没有转成功,不就出错了是吗。
持久性(Durability)
事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
eg:银行在转账完成后,事务一旦提交,数据就已经落盘了,无法再回溯。
事务的隔离级别
事务隔离级别有四种,这部分与下面的事务的并发问题有很大关联,一起食用更佳。而控制事务的隔离级别的,就与MVCC有关,下一篇将会讲到,敬请期待。
未提交读(READ UNCOMMITTED)
一个事务能读取到其他事物修改但没有提交的数据。
读已提交(READ COMMITTED)
事务只能读取到其他事务提交了的内容
可重复读(REPEATABLE READ)
事务开始时,会创建一个一致性视图(快照),依赖于MVCC的实现。该级别下事务只能看到当前事务开始前已经提交的数据,即使其他事务提交了更新,那么该事务也是读不到的。
可序列化(SERIALIZABLE)
整个表都会被加锁(表级锁),阻止其他事务插入、删除、更新,确保事务A看到的行始终不变,如果其他尝试INSERT INTO,它会被阻塞,直到当前事务提交或回滚。
事务并发问题
事务的并发问题与上面提到的事务特性之隔离性有关。不同的隔离级别会有不同的并发问题。所以这部分与事务的隔离级别有很大关系,在看这部分内容时,建议与上面的隔离级别的内容相互理解更佳。
脏读 —— 读到别的事务修改但未提交的内容
解释:
脏读 发生在 未提交读(READ UNCOMMITTED) 级别下,事务 A 读取了事务 B 尚未提交 的数据。如果事务 B 回滚了,那事务 A 读到的数据就成了“脏”数据。
解决方案:
使用 读已提交(READ COMMITTED) 级别,避免读取未提交的数据。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
示例:事务 A 读取事务 B 未提交的数据
假设有一张 账户(account)表:
id | name | balance |
---|---|---|
1 | 小王 | 1000 |
1️⃣ 事务 B:修改数据但未提交
START TRANSACTION;
UPDATE account SET balance = 5000 WHERE id = 1;
-- 事务 B 此时还未提交
此时,表中的数据还没正式生效,但 MySQL 在 READ UNCOMMITTED
级别下,事务 A 仍然能读取到这个未提交的修改。
2️⃣ 事务 A:读取数据
START TRANSACTION;
SELECT balance FROM account WHERE id = 1;
- 如果 隔离级别是 READ UNCOMMITTED,事务 A 会看到 balance = 5000(即事务 B 尚未提交的修改)。
- 但如果 隔离级别是 READ COMMITTED 或更高,事务 A 只能看到 balance = 1000(即事务 B 提交前的数据)。
3️⃣ 事务 B 回滚
如果事务 B 发生错误或放弃修改:
ROLLBACK;
那么,balance 恢复为 1000。
而事务 A 之前读取到 5000,但这个数据实际上从未真正生效,造成了脏读!
4️⃣ 将隔离级别设置为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
这样事务就只会读取到已经提交的事务,操作回滚的数据事务不会读取到。
不可重复读 —— 单条数据两次读取到的内容不一致
解释:
不可重复读 发生在 读已提交(READ COMMITTED) 级别下,事务A先读取了表中数据,然后B将这个数据修改了并提交了,事务A再读取该数据时,发现数据不一样了。
解决方案:
使用 可重复度(REPEATABLE READ) 级别,每次都读取第一次读到的数据。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
示例(READ COMMITTED 下的不可重复读)
1️⃣假设有一张 account
表:
id | name | balance |
---|---|---|
1 | 小王 | 1000 |
2️⃣事务 A(第一次查询):
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 结果:1000
3️⃣事务 B(修改并提交):
START TRANSACTION;
UPDATE account SET balance = 5000 WHERE id = 1;
COMMIT;
4️⃣事务 A(第二次查询):
SELECT balance FROM account WHERE id = 1; -- 结果:5000
事务 A 前后两次查询的结果不一致,发生了不可重复读。
5️⃣切换隔离级别为:可重复度
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
- 事务 A 读取时,生成快照(Snapshot),记录当前可见的数据版本。
- 事务 A 只能看到这个快照中的数据,即事务开始前已提交的数据。
- 事务 B 的修改不会影响事务 A,直到事务 A 提交后再进行新查询时才可见
幻读 —— 两次查询的行数不一致
解释:
幻读 发生在 可重复读(REPEATABLE READ) 级别下,事务 A 在事务进行期间,事务 B 插入或删除 了一些数据,导致事务 A 再次查询时发现数据行数变化了。
解决方案:
使用 可序列化(SERIALIZABLE) 级别,加锁防止其他事务插入新数据,彻底杜绝幻读。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
注意点,幻读和不可重复读的区别
- 不可重复读 发生在 单条数据的修改(UPDATE),可重复读(REPEATABLE READ)通过 MVCC 机制 解决了这个问题。
- 幻读 发生在 记录的增删(INSERT/DELETE),MVCC 只能保证同一条记录的一致性,但无法控制新插入或删除的记录。
问题:为什么 MVCC 不能解决幻读?
在 REPEATABLE READ 级别,MVCC 只能保证:
- 已存在的行(行级别)的数据在事务内一致。
- 无法控制新插入的行,因为 MVCC 只能管理已读取的数据快照,而不会锁住未出现的数据。
换句话说:
- 不可重复读:事务内部的 相同数据 读取两次,结果不同(UPDATE 造成)。
- 幻读:事务内部的 同一批查询 读取两次,结果的行数不同(INSERT/DELETE 造成)
总结
现象 | 产生条件 | 解决办法 |
脏读 | 事务读到了未提交的数据 | READ COMMITTED(读已提交) |
不可重复读 | 事务的同一条数据被修改 | REPEATABLE READ(可重复读) |
幻读 | 事务的查询结果发生变化(增删数据) | SERIALIZABLE(可序列化) |
MySQL 默认的隔离级别是 REPEATABLE READ,可以防止脏读和不可重复读,但仍可能发生幻读。
想让事务更安全,就要在提高隔离级别和性能损耗之间找到平衡。