数据库拥有ACID四个基本属性。
其中的I(隔离性)要求并发事务中,事务的中间状态是无法被别的事务查看的。例如A账户转到B账户的事务中,不能让别的事务在B账户扣除了100块前查看到A事务增加了100块这个中间状态。
但出于性能的考虑,许多数据库都允许使用者配置隔离级别牺牲一定的隔离性换取并发性。
SQL定义了四种隔离级别:
- Read Uncommitted: 读取未提交的数据,即可读取其他事务修改了但是没有提交的数据。这是最低的隔离级别(会导致脏读)
- Read Committed:只能读取已经提交的数据。解决了脏读的问题但是没有解决“不可重复读”,即一个事务中多次读取的数据的内容可能是不一样的。例如,事务2在事务1开始后修改了A账户为150,而第一次读到A账户有100块,然后事务2提交,第二次读的时候就变成了150块。
- Repeatable Read:保证可以事务多次读取到的数据行的内容是一致的。但还是有可能导致幻读,即同一事务中,第二次读到的数据行中拥有第一次没有读取到的。
- Serializable:最高级别的隔离级别,即事务是可串行执行的,就像一个事务执行的时候没有别的事务执行一样。
并发控制
事务的锁分为读锁和写锁。允许为同一个元素增加多个读锁,但只允许加一个写锁,且写事务会阻塞读事务。
由于互联网的业务属性决定,读事务远远比写事务多得多。而加锁一定程度上阻碍了读的性能,对于读性能的优化是一个刚需。
现在有以下两种方法可以大大提高读取的效率而不需要加锁
写时复制(Copy-On-Write)
读操作不需要加锁,而当需要写操作的时候,以B+树为例:
1 拷贝:将从叶子到根的所有节点拷贝出来
2 对拷贝的内容进行修改。
3 提交后,原子地切换根节点指向新的节点。
这样读操作并不需要加锁,并不会被写操作所阻塞,但问题是写的时候需要拷贝结点,而且多个写操作是互斥的,一个时刻只能允许一个写操作
多版本并发控制(Multi-Version Concurrency Control,MVCC)
对于读操作也不需要加锁,原理是对于每行的数据维护多个数据版本。MySQL InnoDB的存储引擎为例,InnoDB对每行数据隐式地维护了两列——“最近被修改的事务号”和“被删除事务号”。
SELECT: 需要满足以下两个条件才能返回
- 行的修改版本号小于当前事务号。(证明事务开始前就被提交了)
- 行的删除号不存在,或者大于该事务号。(没有被删除,或者事务开始后才被删除的,保证可重复读) 在可重复读的隔离级别下,后开始的事务对数据的影响不应该被先前的事务看到,所以应该忽略后面事务的操作。
INSERT
直接把修改的事务号改为当前事务号
DELETE
直接把删除的事务号改为当前事务号。而不是真正的删除
UPDATE
更新行的时候,复制一份数据并修改最近修改的事务号为当前事务。
MVCC在读取数据时候不需要加锁,会通过对应的事务号返回需要的记录,大大提高了并发性。但由于维护了多个版本的数据,需要定期清理不再使用的数据。