数据库系统不能只服务一个用户,需要允许多个用户同时操作数据,即并发。所以需要保证多个并发事务的隔离,这里涉及到并发控制技术,常见的并发控制方法有基于锁的并发控制(Lock based Concurrency Controll)和多版本并发控制(Multiple version Concurrency Controll)
基于锁的并发控制
数据库系统对用户在事务过程中操作的每一行数据都加锁,读操作加读锁,写操作加写锁。读写阻塞,写写阻塞。如果两个不同的事务试图修改同一行数据,事务1先加上写锁正常执行,事务2就只能阻塞等待,当事务1执行结束释放写锁后,事务2再继续执行。
以转账操作为例,A账户有100元,B账户有200元,如果事务1执行从A账户转10元到B账户,那么在事务1执行过程中,A B两行账户数据都会被加上写锁。如果事务2执行另一次转账操作,从A账户转50元到B账户,那么给A加写锁时失败,事务2会一直等待直到加写锁成功为止。
如果在事务1与事务2的执行过程中,又来一个事务3想要查询A B两个账户的余额总和,那么需要读取A、B两行,读之前要先加读锁,但是在事务1和事务2操作时,读锁加不成功,那么事务3就需要等待事务1和事务2都结束了才能执行。
多版本的并发控制
在上面例子中,一个读取A、B两账户余额总和的操作,无论事务1和事务2是否执行完成,其结果都是确定的(300元)。但基于锁的并发控制,读写是阻塞的,极大的降低了数据库的并发性能,所以出现了MVCC的方法来做并发控制。对于数据库的数据,每次修改都保留历史版本,这样读操作可以直接在历史版本上执行,不会被写操作阻塞。
在MVCC方法下,每一个事务都会有一个提交版本,继续上面事务三的例子。假设数据的初始版本是98,假定事务1是的版本号100,事务2是101。那么修改都完成后,数据库中的数据状态如下:
每次数据修改的记录都会被串联起来。另有一个全局变量Global Committed Version(GCV) 标示全局最后提交的版本号。在事务1执行之前GCV是98,事务1提交之后GCV变成100,事务二提交之后GCV变成101。所以,当事务3开始执行时,先获取GCV,然后根据这个版本号去读相应的数据。
MVCC的修改操作依然会依赖上面提到的锁机制,所以写操作之间的冲突依然需要等待。但是MVCC最大的优势就是读操作与写操作完全隔离开,互相不影响,对于数据库的性能和并发能力提升非常有益。
OceanBase使用的方案
使用MVCC方案时GCV的修改需要依次递增,如果GCV被修改成了101,表示101及之前的所有事务都提交了。OceanBase使用了分布式事务后,这个保证变得困难,例如,版本号为100的事务可能是分布式事务,101可能是单机事务,分布式事务的提交过程要显著长于单机事务。结果,版本号101的事务先完成,GCV就被修改成了101。但是此时版本号为100事务还在提交过程中,并不能被读取。
所以,OceanBase采用了MVCC和Lock-based结合的方式,读取操作先获取GCV,然后按照这个版本号读取每一行数据。如果这行数据上没有修改,或者有修改但是修改的版本号会大于GCV,那么这行数据可以直接按照版本号读取。如果这行数据上有一个正在提交中事务,并且版本号有可能小于读取使用的版本号,那么读取操作就需要等待这个事务结束,就好像Lock-based方案中读锁等待写锁一样。但是,这个发生的概率极低,所以整体的性能还是像MVCC一样好。