GoldenDB分布式事务测试与隔离级别解析

GoldenDB分布式事务测试揭示CR级别下聚合函数的脏读问题,MVCC_CR提供更强一致性,但仍需根据业务场景选择。

原文标题:数据库系列之GoldenDB分布式事务测试

原文作者:牧羊人的方向

冷月清谈:

本文分析了GoldenDB的分布式事务机制,特别是不同隔离级别的实现和测试结果。
GoldenDB通过在Proxy层引入不同隔离级别来控制分布式事务。读语句的隔离级别包括UR(不控制)、CR(强一致性读)和MVCC_CR(多版本并发读一致性)。写语句的隔离级别包括SW(无控制)和CW(强一致性写)。
CR级别在读一致性场景下,通过检查数据行GTID与全局活跃事务列表来避免脏读。但对于聚合函数(如count、sum),CR级别采用脏读方式,MVCC_CR级别则提供强一致性。
文章通过两个测试案例说明了CR级别在聚合函数上的不足。测试一中,并发删除插入操作导致查询结果不一致。测试二中,转账测试场景下,并发更新和查询操作也导致余额统计出现错误。这两个测试都表明,CR级别下,聚合函数存在脏读问题,需要使用MVCC_CR级别来保证一致性。
GoldenDB的MVCC_CR级别通过在Proxy层控制聚合查询下推到数据节点,利用undo log构建前镜像数据返回结果。普通查询仍然使用CR机制,冲突时会重试,多次失败后才通过undo log返回上一版本数据。
文章建议根据具体业务场景选择是否使用MVCC。如果业务对聚合查询一致性要求较高,建议开启MVCC。但如果业务逻辑可以实现读写序列化,则优先考虑应用层控制。

怜星夜思:

1、GoldenDB的MVCC机制在高并发场景下,性能如何?如何评估和优化?
2、除了MVCC,GoldenDB还有哪些机制可以保证分布式事务的一致性?
3、GoldenDB的分布式事务机制与其他主流分布式数据库(如TiDB)相比,有哪些优势和劣势?

原文内容

在“”中简要介绍了GoldenDB中分布式事务的实现原理,本文将结合分布式事务测试的结果进一步理解GoldenDB中分布式事务的实现机制。

1、隔离级别介绍
1.1 读一致性处理
事务隔离级别是数据库事务处理的基础,SQL-92标准定义了4种隔离级别:读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)、串行化(SERIALIZABLE)。这种分类方式是基于锁机制进行并发控制得出的理论,通常通过锁进行并发控制会出现写会堵塞读,读会阻塞写。为了解决读写冲突问题,原生的mysql中引入了MVCC机制,在一致性读的时候通过读取undo表空间的前镜像数据来减少读写冲突。另外,MVCC只在Read Committed和Repeatable Read两个隔离级别下工作,一般来说,传统数据库的RR和RC隔离级别都实现了MVCC机制。

在读一致性业务场景下,当隔离级别为RC(读已提交)时,访问的行数据上存在活跃事务时将怎么处理?在GoldenDB中提供了两种思路:
  • RC隔离级别:等待直至全局GTID释放后读取当前行

  • MVCC_CR隔离级别:构造分布式快照,读前像数据

在实际分布式事务实现的过程中,GoldenDB会在proxy层对GTID进行活跃判断,根据不同的隔离级别返回读一致性场景下的数据请求。

1.2 GoldenDB中的隔离级别

针对不同的业务场景,有同时读写的、有并发写的,分布式事务控制本身的代价是非常高的,为了获得最优的系统性能,应用可以灵活地控制分布式事务的执行程度。在GoldenDB的proxy层将事务的隔离级别分为读语句和写语句。

1)读语句的具体级别
  • UR(uncommitted read):未提交读,即中间件不做任何分布式事务控制,业务要么允许脏读,要么不存在读的时候同时写;

  • CR(consistency read):强一致性读,在高并发读写时,不存在脏读的可能性,但效率较低

  • MVCC_CR:带MVCC多版本并发读一致性机制,主要解决聚合函数脏读的问题。

  • 读语句的事务控制

CR级别下,count、sum等聚合函数采用脏读方式,MVCC_CR下才是强一致读。同时,为避免读历史版本报错,需要修改proxy节点proxy.ini如下参数:
#活跃GTID列表本地有效时间
active_gtid_valid_time = 0
2)写语句的隔离级别
  • SW(single write):单事务写,即不存在多个事务同时写相同的数据,不需要分布式事务控制;

  • CW(consistency write):强一致性写,存在多个事务同时写相同的数据,需要进行分布式事务控制;

  • 写语句的事务控制

3)隔离级别的控制优先级

系统有默认的控制级别,应用可以在事务上设置这两个标志,也可以在语句上设置。如果都设置了,优先级为:语句级 >> 事务级 >> 系统级。

4)CR和UR的区别

读默认为CR(可指定为MVCC_CR),使用explain,可以查看指定CR和UR的不同CR级别会额外查询GTID列,用于判断是否有活跃事务。

mysql > explain select * from sbtest2 limit 10 ur;
id: 10001
select_type: SQLNode
table:1
partitions:
type:
possible_keys: SELECT id,k,c,pad frm sbtest2 limit 10
key: Cluster1,g1,g2
key_len:
ref: Parent=NULL,Child=NULL,NEXT=NULL
rows:
filtered:
extra: ur,sbtest2=hash

mysql > explain select * from sbtest2 limit 10;
id: 10001
select_type: SQLNode
table:1
partitions:
type:
possible_keys: SELECT id,k,c,pad,gtid as gtid1 from sbtest2 limit 10
key: Cluster1,g1,g2
key_len:
ref: Parent=NULL,Child=NULL,NEXT=NULL
rows:
filtered:
extra: cr,sbtest2=hash
1.3 GoldenDB中读一致性实现
GoldenDB中在proxy层定义了事务的隔离级别,当满足一致性读CR时,通过检查数据行GTID列对应的全局状态,来判断该数据行是否正在被其它全局事务修改。如果GTID在全局活跃事务列表中,则表明该数据正在被修改,不能返回给应用。如下图所示,事务TX1更新表T1的AC列,在goldendb中的事务处理逻辑中会先申请GTID,并更新到表T1的GTID列,比如AC列值由30更新到50,GTID由1000更新到1002。当事务TX2查询该表的该行记录时,会先去GTM查询活跃GTID,发现有GTID值为1002,再从表T1中查询记录,因为DB数据节点的隔离级别是RC模式,查询到的结果是未提交的数据,也就是更新前的数据AC=30+GTID=1000,再跟活跃事务列表一对比,发现不在列表中,就将更新前的数据直接返回给应用了。这样就实现的事务的隔离性,避免了脏读的情况。

还有一种极端的情况是,查询的事务TX2在事务TX1释放GTID前查询的活跃事务GTID,在事务TX1数据节点commit后查询的表数据,这样查询到DB节点是提交后的记录也就是AC=50+GTID=1002。在proxy判活的时候,发现是在活跃事务列表中,就会尝试重新查询,当GTID已经释放后,返回到该条记录已经提交的数据。这种情况下返回的依旧是事务已经提交的数据,也不存在脏读和读不一致的情况。

2、分布式事务测试
2.1 分布式事务测试一

1)测试过程

循环执行删除整表、插入1000条数据的脚本,同时不断查询表数据量

#cat delAndIns.sql
delete from sbtest7 where 1=1;
begin;
INSERT into sbtest7(id,k,c,pad) values();
commit;

使用命令执行删除整表,插入1000条数据

while true; do mysql h192.168.112.101 P8880 uxxx pxxx dbm f<delAndIns.sql;done;

执行命令查询表数据量,并记录结果

while true; do mysql h192.168.112.101 P8880 uxxx pxxx dbm Bse select count(*) from sbtest7>>1.log;done;

以上两个命令运行一段时间后,检查1.log中的结果

#检查除10000以外,是否有其它情况
cat 1.log | grep vE 1000|^0
#统计各种情况出现的次数
sort 1.log | uniq -c

2)测试结果

sort 1.log | uniq c
696 0
404 1000
11 491
10 509

3)原因:CR级别下,聚合函数采用脏读模式,需要修改MVCC_CR级别

2.2 分布式事务测试二
1)测试方案
  • 测试内容:转账测试,在一个事务中,进行不同账户转入、转出操作,其他事务对涉及账户余额之和进行查询

  • 测试过程:50个进程,分别更新不同的id组,然后有50个进程,分别查询这些id组sum(k)的值

  • 期望结果:余额应保持不变

2)测试涉及文件说明

共5个文件:getSum.sh、id.dat、mGetSum.sh、mTrans.sh、trans.sh,其中id.dat文件中为50个id组。

  • 转账脚本trans.sh

#!/bin/bash
outId=$1
inId=$2
i=0
j=0
while [ $i le 100000 ]
do
let i++
mysql h192.168.112.101 P8880 ud2m pxxx d2m e
begin;
update sbtest1 set k = k-1 where id = ${outId};
update sbtest1 set k = k+1 where id = ${inId};
commit;
let j=$i%100
if [ $j == 0 ]; then
echo $i
fi
done
  • 读id.dat,发起50个转账进程(mTran.sh)

cat id.dat | while read LINE;
do
sh tran.sh $LINE &
done
  • 查询sum(k)值的脚本(getSum.sh)

#!/bin/bash
outId=$1
inId=$2
while true
do
mysql h192.168.112.102 P8880 ud2m Bse select sum(k) from sbtest1 where id in(${outID},${inId}) >> getSum${outID}.log
  • 读取id组,发起50个查询进程(mGetSum.sh)

cat id.dat | while read LINE;
do
sh getSum.sh $LINE &
done

3)测试过程

  • 使用如下命令,初始化id组的k值为500000

update sbtest1 set k=500000 where id < 100 or (id>=256 and id<356);
  • 运行以下两个命令,发起转账和查询:

sh mTrans.sh
sh mGetSum.sh
  • 使用如下命令,统计输出结果

sort 1.log | uniq c
5656 1000000
94 1000001
76 999999

附批量kill后台进程语句

ps -ef | grep trans.sh | grep v grep | awk {print $2} | xargs kill -9
ps -ef | grep getSum.sh | grep v grep | awk {print $2} | xargs kill -9

4)测试结果:存在查询出现余额不一致的情况

sort 1.log | uniq c
5656 1000000
94 1000001
76 99999

5)原因:CR级别下,聚合函数采用脏读方式,需要修改MVCC_CR隔离级别

总结goldendb目前版本的分布式事务机制,proxy层的CR隔离级别能够保证普通查询的读一致性,但是存在几个问题:一是会将结果集拿在proxy层进行判活,在大结果集的情况下会导致proxy层OMM的情况出现,尤其是在批量业务场景中;二是对聚合函数不能保证一致性读,从分布式事务的测试场景中也发现存在脏读的情况。为此goldendb中引入了MVCC_CR隔离级别,以解决聚合函数一致性的问题,通过在proxy层参数控制,对于聚合类的查询会下推到DB数据节点进行判活并通过undo构建前镜像数据返回,而普通的查询语句还是CR隔离级别的机制,出现活跃事务GTID冲突时候会重试,重试几次失败后才会下发到DB节点通过undo返回上一版本的数据。但是MVCC_CR下对于普通查询还是需要将结果集汇总到proxy层进行判活。

至于在实际业务中是否使用MVCC,还是需要结合具体的业务场景来分析,如果业务中存在聚合类查询需要保证一致性要求,而且需要数据库层来保证一致性的,建议开启MVCC。但是如果能从业务逻辑上通过读写序列化来实现一致性,还是优先建议应用层去控制,因为从目前版本来看MVCC的实现机制还在不断优化调整中。至于proxy层汇总查询结果进行判活,在实际生产系统中更加考验goldendb分布式数据库的健壮性和业务的可用性了。

参考资料:

  1. GoldenDB分布式数据库事务方案

  2. GoldenDB分布式MVCC实现方案

GoldenDB的优势在于其架构更贴近传统数据库,对DBA更友好,迁移成本更低。劣势在于其社区活跃度和生态不如TiDB。

GoldenDB用的是两阶段提交(2PC)来保证分布式事务的一致性,MVCC主要是为了提高并发性能。另外,全局时间戳(GTID)也起到了作用,用来给全局事务排序和控制可见性。

MVCC在高并发下确实会带来一定的性能开销,主要体现在undo log的维护和版本链的遍历上。评估性能可以使用sysbench之类的压测工具,模拟高并发读写场景,关注TPS、延迟等指标。优化方面,可以考虑调整undo log的大小、优化查询语句以减少版本链遍历、以及根据业务场景选择合适的隔离级别。

GoldenDB主要依靠两阶段提交协议(2PC)来保证分布式事务的一致性。MVCC是用于提高并发性能的,而不是为了保证一致性。此外,GoldenDB还有全局时间戳(GTID)机制,用于全局事务的排序和可见性控制。

和TiDB相比,GoldenDB的优势在于架构更像传统数据库,DBA上手更容易,迁移成本也低一些。劣势是社区不如TiDB活跃,生态也还没那么完善。

高并发下MVCC性能如何,得看具体配置和业务场景。评估的话,压测是肯定少不了的,关注TPS、延迟这些指标的同时,也要看看系统资源的消耗情况。优化的话,可以从几个方面入手,比如调整undo log的大小,优化SQL,或者干脆根据业务场景调整隔离级别,在性能和一致性之间找个平衡点。

评估MVCC性能,可以进行压力测试,模拟高并发读写场景。优化方面,可以考虑调整undo表空间的大小,优化查询,以减少版本链遍历。

分布式事务一致性主要靠两阶段提交(2PC)。MVCC是提高并发性能的,GTID用于全局事务排序和可见性控制。

GoldenDB架构更贴近传统数据库,DBA友好,迁移成本低;劣势是社区和生态不如TiDB。