10.3.2 多版本并发控制

OceanBase的MemTable包含两个部分:索引结构及行操作链。其中,索引结构存储行头信息,采用9.1.2节中的内存B树实现;行操作链表中存储了不同版本的修改操作,从而支持多版本并发控制。

OceanBase支持多线程并发修改,写操作拆分为两个阶段:

●预提交(多线程执行):事务执行线程首先锁住待更新数据行,接着,将事务中针对数据行的操作追加到该行的未提交行操作链表中,最后,往提交任务队列中加入一个提交任务。

●提交(单线程执行):提交线程不断地扫描并取出提交任务队列中的提交任务,将这些任务的操作日志追加到日志缓冲区中。如果日志缓冲区到达一定大小,将日志缓冲区中的数据同步到备机,同时写入主机的磁盘日志文件。操作日志写成功后,将未提交行操作链表中的cell操作追加到已提交行操作链表的末尾,释放锁并回复客户端写操作成功。

如图10-9所示,MemTable行操作链表包含两个部分:已提交部分和未提交部分。另外,每个事务管理结构记录了当前事务正在操作的数据行的行头,每个数据行的行头包含已提交和未提交行操作链表的头部指针。在预提交阶段,每个事务会将cell操作追加到未提交行操作链表中,并在行头保存未提交行操作链表的头部指针以及锁信息,同时,将行头信息记录到事务管理结构中;在提交阶段,根据事务管理结构中记录的行头信息找到未提交行操作链表,链接到已提交行操作链表的末尾,并释放行头记录的锁。


Class ObTransExecutor

{

public:

//处理预提交任务

void handle_trans(voidptask,voidpdata);

//处理提交任务

void handle_commit(voidptask,voidpdata);

};


10.3.2 多版本并发控制 - 图1

图 10-9 MemTable实现MVCC

ObTransExecutor是UpdateServer读写事务处理的入口类,它主要包含两个方法:handle_trans以及handle_commit。其中,handle_trans处理预提交任务,handle_commit处理提交任务。handle_trans首先将写事务预提交到MemTable中,接着将写事务加入到提交任务队列。提交线程不断地从提交任务队列中取出提交任务,并调用handle_commit进行处理。

每个写事务会根据提交时的系统时间生成一个事务版本,读事务只会读取在它之前提交的写事务的修改操作。

如图10-10所示,对主键为1的商品有2个写事务,事务T1(提交版本号为2)将商品购买人数修改为100,事务T2(提交版本号为4)将商品购买人数修改为50。那么,事务T2预提交时,T1已经提交,该商品的已提交行操作链包含一个cell:<update,购买人数,100>,未提交操作链包含一个cell:<update,购买人数,50>。事务T2成功提交后,该商品的已提交行操作链将包含两个cell:<update,购买人数,100>以及<update,购买人数,50>,未提交行操作链为空。对于只读事务:

10.3.2 多版本并发控制 - 图2

图 10-10 读写事务并发执行实例

●T3:事务版本号为1,T1和T2均未提交,该行数据为空。

●T4:事务版本号为3,T1已提交,T2未提交,读取到<update,购买人数, 100>。尽管T2在T4执行过程中将购买人数修改为50,T4第二次读取时会过滤掉T2的修改操作,因而两次读取将得到相同的结果。

●T5:事务版本号为5,T1和T2均已提交,读取到<update,购买人数,100>以及<update,购买人数,50>,购买人数最终值为50。

1.锁机制

OceanBase锁定粒度为行锁,默认情况下的隔离级别为读取已提交(read committed)。另外,读操作总是读取某个版本的快照数据,不需要加锁。

●只写事务(修改单行):事务预提交时对待修改的数据行加写锁,事务提交时释放写锁。

●只写事务(修改多行):事务预提交时对待修改的多个数据行加写锁,事务提交时释放写锁。为了保证一致性,采用两阶段锁的方式实现,即需要在事务预提交阶段获取所有数据行的写锁,如果获取某行写锁失败,整个事务执行失败。

●读写事务(read committed):读写事务中的读操作读取某个版本的快照,写操作的加锁方式与只写事务相同。

为了保证系统并发性能,OceanBase暂时不支持更高的隔离级别。另外,为了支持对一致性要求很高的业务,OceanBase允许用户显式锁住某个数据行。例如,有一张账务表account(account_id,balance),其中account_id为主键。假设需要从A账户(account_id=1)向B账户(account_id=2)转账100元,那么,A账户需要减少100元,B账户需要增加100元,整个转账操作是一个事务,执行过程中需要防止A账户和B账户被其他事务并发修改。

如图10-11所示,OceanBase提供了"select…for update"语句用于显示锁住A账户或者B账户,防止转账过程中被其他事务并发修改。

10.3.2 多版本并发控制 - 图3

图 10-11 select……for update示例

事务执行过程中可能会发生死锁,例如事务T1持有账户A的写锁并尝试获取账户B的写锁,事务T2持有账户B的写锁并尝试获取账户A的写锁,这两个事务因为循环等待而出现死锁。OceanBase目前处理死锁的方式很简单,事务执行过程中如果超过一定时间无法获取写锁,则自动回滚。

2.多线程并发日志回放

9.2.3节介绍了主备同步原理,引入多版本并发控制机制后,UpdateServer备机支持多线程并发回放日志功能。如图10-12所示,有一个日志分发线程每次从日志源读取一批日志,拆分为单独的日志回放任务交给不同的日志回放线程处理。一批日志回放完成时,日志提交线程会将对应的事务提交到内存表并将日志内容持久化到日志文件。

10.3.2 多版本并发控制 - 图4

图 10-12 备机多线程并发日志回放


Class ObLogReplayWorker

{

public:

//提交一批待回放的操作日志

//@param[out]task_id最后一条操作日志的编号

//@param[in]buf日志缓冲区

//@param[in]len日志缓冲区的大小

//@param[in]replay_type日志回放类型,包括RT_LOCAL(回放本地日志)和RT_APPLY(回放通过网络接收到的日志)

int submit_batch(int64_t&task_id,const char*buf,int64_t len,const ReplayType replay_type);

public:

//回放一条操作日志

int handle_apply(ObLogTask*task);

};


在9.3.3节中提到,备UpdateServer有专门的日志回放线程不断地调用ObUpsLog-Mgr中的replay_log函数获取并回放操作日志。UpdateServer支持多线程并发写事务后,replay_log函数实现成调用ObLogReplayWorker中的submit_batch,将一批待回放的操作日志加入到回放任务队列中。多个日志回放线程会取出回放任务并不断地调用handle_apply回放操作日志,即首先将操作日志预提交到MemTable中,接着加入到提交任务队列。另外,还有一个单独的提交线程会从提交任务队列中一次取出一批任务,提交到MemTable并持久化到日志文件中。