早期数据库不论读取还是写入,都用锁来实现。但是锁会带来性能的问题。人们尝试各种优化方案。写入和读取的优化方式不同。
对于数据库写入操作,没有特别好的办法,因为无论如何要避免并发修改一个数据,就得靠锁。不同的数据库对于写入操作都会加悲观锁(比如MySQL是X锁)。为了避免X锁带来的性能问题,人们在合适的场合会选择用乐观锁来优化。有的数据库内建乐观锁,但是有的没有(比如MySQL就没有),所以需要开发人员自己在数据表里加version列,自己写业务代码实现。
顺便提一句,乐观锁并不一定总是比 悲观锁性能表现更好,这要看竞争的程度。如果数据访问竞争的非常厉害,乐观锁只会让CPU和IO白白浪费而已。
对于读取,优化就是MVCC。现在主流的商业数据库都是基于MVCC,如MySQL InnoDB和Postgres。MVCC的意思用简单的话讲就是对数据库的任何修改的提交都不会直接覆盖之前的数据,而是产生一个新的版本与老版本共存,使得读取时可以完全不加锁。这样读某一个数据时,事务可以根据隔离级别选择要读取哪个版本的数据。过程中完全不需要加锁。
由于每次读写操作都需要获取锁,所以造成了一定的效率损失。但是对于大部分数据库来说,读操作都比写操作频繁的多,因此InnnoDB专门针对读操作进行了优化,通过多版本快照隔离的机制,将一个record的每一个历史版本都保存下来,读操作选择合适的历史版本进行读取,因此读操作不再需要加锁,从而提升了系统整体的并行程度。
这样,实现两个隔离级别就非常容易:
- Read Committed - 一个事务读取数据时总是读这个数据最近一次被commit的版本
- Repeatable Read - 一个事务读取数据时总是读取当前事务开始之前最后一次被commit的版本(所以底层实现时需要比较当前事务和数据被commit的版本号)。
举个简单的例子:
- 一个事务A(txnId=100)修改了数据X,使得X=1,并且commit了
- 另外一个事务B(txnId=101)开始尝试读取X,但是还X=1。但B没有提交。
- 第三个事务C(txnId=102)修改了数据X,使得X=2。并且提交了
- 事务B又一次读取了X。这时
- 如果事务B是Read Committed。那么就读取X的最新commit的版本,也就是X=2