《大规模分布式存储系统》第10章 数据库功能【10.3】
10.3 写事务写事务,包括更新(UPDATE)、插入(INSERT)、删除(DELETE)、替换
(REPLACE,插入或者更新,如果行不存在则插入新行;否则,更新已有行),由
MergeServer解析后生成物理执行计划,这个物理执行计划最终将发给UpdateServer执
行。写事务可能需要读取基线数据,用于判断更新或者插入的数据行是否存在,判
断某个条件是否满足,等等,这些基线数据也会由MergeServer传给UpdateServer。
10.3.1 写事务执行流程
大部分写事务都是针对单行的操作,如果单行事务不带其他条件:
●REPLACE:REPLACE事务不关心写入行是否已经存在,因此,MergeServer直
接将修改操作发送给UpdateServer执行。
●INSERT:MergeServer首先读取ChunkServer中的基线数据,并将基线数据中行
是否存在信息发送给UpdateServer,UpdateServer接着查看增量数据中行是否被删除或
者有新的修改操作,融合基线数据和增量数据后,如果行不存在,则执行插入操
作;否则,返回行已存在错误。
●UPDATE:与INSERT事务执行步骤类似,不同点在于,行已存在则执行更新操
作;否则,什么也不做。
●DELETE:与UPDATE事务执行步骤类似。如果行已存在则执行删除操作;否
则,什么也不做。
如果单行写事务带有其他条件:
●UPDATE:如果UPDATE事务带有其他条件,那么,MergeServer除了从基线数
据中读取行是否存在,还需要读取用于条件判断的基线数据,并传给UpdateServer。
UpdateServer融合基线数据和增量数据后,将会执行条件判断,如果行存在且判断条
件成立则执行更新操作。否则,返回行已存在或者条件不成立错误。
●DELETE:与UPDATE事务执行步骤类似。
例10-6 有一张表格item(user_id,item_id,item_status,item_name),其中,<
user_id,item_id>为联合主键。
MergeServer首先解析图10-6的SQL语句产生执行计划,确定待修改行的主键为
<1,2>,接着,请求主键<1,2>所在的ChunkServer,获取基线数据中行是否存
在,最后,将SQL执行计划和基线数据中行是否存在一起发送给UpdateServer。
UpdateServer融合基线数据和增量数据,如果行已存在且未被删除,UPDATE和
DELETE语句执行成功,INSERT语句执行返回“行已存在”;如果行不存在或者最后
被删除,INSERT语句执行成功,UPDATE和DELETE语句返回“行不存在”。
图 10-6 单行写事务(不带条件)
图10-7中的UPDATE和DELETE语句还带有item_name="item1"的条件,
MergeServer除了请求ChunkServer获取基线数据中行是否存在,还需要获取item_name
的内容,并将这些信息一起发送给UpdateServer。UpdateServer融合基线数据和增量
数据,判断最终结果中行是否存在,以及item_name的内容是否为"item1",只有两个
条件同时成立,UPDATE和DELETE语句才能够执行成功;否则,返回“行不存在或者
item_name列的内容不符合预期”。
图 10-7 单行写事务(带条件)
当然,并不是所有的写事务都这么简单。复杂的写事务可能需要修改多行数
据,事务执行过程也可能比较复杂。
例10-7 有两张表格item(user_id,item_id,item_status,item_name)以及
user(user_id,user_name)。其中,<user_id,item_id>为item表格的联合主键,
user_id为user表格的主键。
图10-8的UPDATE语句可能会更新多行。MergeServer首先从ChunkServer获取编
号为1的用户包含的全部item(可能包含多行),并传给UpdateServer。接着,
UpdateServer融合基线数据和增量数据,更新每个存在且未被删除的item的item_status
列。
图 10-8 复杂写事务举例
图10-8的DELETE语句更加复杂,执行时需要首先获取user_name为“张三”的用户
的user_id,考虑到事务隔离级别,这里可能需要锁住user_name为“张三”的数据行
(防止user_name被修改为其他值)甚至锁住整张user表(防止其他行的user_name修
改为“张三”或者插入user_name为“张三”的新行)。接着,获取用户名为“张三”的所
有用户的所有item,最后,删除这些item。这条语句执行的难点在于如何降低锁粒度
以及锁占用时间,具体的做法请读者自行思考。
10.3.2 多版本并发控制
OceanBase的MemTable包含两个部分:索引结构及行操作链。其中,索引结构存
储行头信息,采用9.1.2节中的内存B树实现;行操作链表中存储了不同版本的修改操
作,从而支持多版本并发控制。
OceanBase支持多线程并发修改,写操作拆分为两个阶段:
●预提交(多线程执行):事务执行线程首先锁住待更新数据行,接着,将事务
中针对数据行的操作追加到该行的未提交行操作链表中,最后,往提交任务队列中
加入一个提交任务。
●提交(单线程执行):提交线程不断地扫描并取出提交任务队列中的提交任
务,将这些任务的操作日志追加到日志缓冲区中。如果日志缓冲区到达一定大小,
将日志缓冲区中的数据同步到备机,同时写入主机的磁盘日志文件。操作日志写成
功后,将未提交行操作链表中的cell操作追加到已提交行操作链表的末尾,释放锁并
回复客户端写操作成功。
如图10-9所示,MemTable行操作链表包含两个部分:已提交部分和未提交部
分。另外,每个事务管理结构记录了当前事务正在操作的数据行的行头,每个数据
行的行头包含已提交和未提交行操作链表的头部指针。在预提交阶段,每个事务会
将cell操作追加到未提交行操作链表中,并在行头保存未提交行操作链表的头部指针
以及锁信息,同时,将行头信息记录到事务管理结构中;在提交阶段,根据事务管
理结构中记录的行头信息找到未提交行操作链表,链接到已提交行操作链表的末
尾,并释放行头记录的锁。
Class ObTransExecutor
{
public:
//处理预提交任务
void handle_trans(void*ptask,void*pdata);
//处理提交任务
void handle_commit(void*ptask,void*pdata);
};
图 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-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-11 select……for update示例
事务执行过程中可能会发生死锁,例如事务T1持有账户A的写锁并尝试获取账户
B的写锁,事务T2持有账户B的写锁并尝试获取账户A的写锁,这两个事务因为循环
等待而出现死锁。OceanBase目前处理死锁的方式很简单,事务执行过程中如果超过
一定时间无法获取写锁,则自动回滚。
2.多线程并发日志回放
9.2.3节介绍了主备同步原理,引入多版本并发控制机制后,UpdateServer备机支
持多线程并发回放日志功能。如图10-12所示,有一个日志分发线程每次从日志源读
取一批日志,拆分为单独的日志回放任务交给不同的日志回放线程处理。一批日志
回放完成时,日志提交线程会将对应的事务提交到内存表并将日志内容持久化到日
志文件。
图 10-12 备机多线程并发日志回放
Class ObLogReplayWorker
{
public:
//提交一批待回放的操作日志
//@paramtask_id最后一条操作日志的编号
//@parambuf日志缓冲区
//@paramlen日志缓冲区的大小
//@paramreplay_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并持久化到日志文件中。
页:
[1]