项目更新数据库数据出错了,因为我对数据库的隔离级别及实现细节理解有问题。
今天复习了一次。
首先再明确一次数据库的四个隔离级别:
- Read uncommitted
- Read committed
- Repeatable read
- Serializable
这四个隔离级别对应是要解决脏读(Dirty read)、不可重复读(Non-repeatable read)、幻象读(Phantom read)的问题。
MySQL 可以通过
1 | SELECT @@global.tx_isolation; |
查看当前数据库设置的隔离级别。
Read uncommitted
最低级的隔离级别就是 Read uncommitted,这是最低级的,拥有最高的并发性能,但是也有一列的并发问题。
除非数据并发安全性要求不是特别高,否则一般都不怎么使用。
Read committed 与 脏读
Read uncommitted 之上就是 Read committed,这一级解决了脏读问题。
所谓脏读,就是一个事务可以读到另一个事务没有提交的数据。带来的问题是当另一个事务最终回滚,
脏读读到的数据实际上是不在数据库里面的。
如上图所示,事务1最后读到的age的值是21。但是事务2最后回滚数据,所以21的值是不在数据库里面存在的。
Read committed 解决了脏读问题,不会允许事务读取另一个事务未提交的数据。
大部分支持事务的数据库默认隔离级别就是 Read committed。
Repeatable read 与 不可重复读
第三级 Repeatable read 在之前的基础上,还解决了不可重复读的问题。
MySQL 的 Innodb 引擎的默认隔离级别就是 Repeatable read。
下面的图是不可重复读的情况。
当事务2更新id为1的users的age并提交后,事务1第二次查询的信息是事务2更新之后的数据,与事务开始的时候的查询结果不一致。
这样前后两次的查询结果不一致的问题,就是不可重复读。
有两种策略可以避免不可重复读(non-repeatable read)。
- 一个是要求事务2延迟到事务1提交或者回滚之后再执行。这种方式实现了T1, T2 的串行化调度。
串行化调度可以支持可重复读(repeatable reads)。 - 另一种策略是多版本并发控制。为了得到更好的并发性能,允许事务2先提交。
但因为事务1在事务2之前开始,事务1必须在其开始执行时间点的数据库的快照上面操作。
当事务1最终提交时候,数据库会检查其结果是否等价于T1, T2串行调度。
如果等价,则允许事务1提交,如果不等价,事务1需要回滚并抛出个串行化失败的错误。
我出错的地方就是这里,我以为 MySQL 会串行化调度,
实际上为了并发性能,MySQL使用的是多版本并发控制。
我想在一个事务内加载数据,再根据加载出来的数据的情况对数据库做不同的操作。
虽然我在事务内虽然读取到的值和第一次是一样的,但这是因为读取的是快照的值,
数据库已经更新为新的值了。
这就造成了我的项目中重复计费……
Serializable 与 幻象读
隔离级别最高级的就是 Serializable,这一级除了解决之前所有的问题,还解决了幻象读的问题。
幻象读主要就是指另一个事务执行了 Insert 操作,导致当前事务两次查询出来的结果集不一致,
第二次的结果集会比第一次多出新插入的数据。
Serializable 解决了脏读、不可重复读和幻象读这三个问题。
总结:隔离级别 vs 读现象(Isolation Levels vs Read Phenomena)
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
未授权读 | 可能发生 | 可能发生 | 可能发生 |
授权读 | - | 可能发生 | 可能发生 |
可重复读 | - | - | 可能发生 |
可序列化 | - | - | - |
“可能发生”表示这个隔离级别会发生对应现象,”-“表示不会发生。