数据库系列之GoldenDB分布式事务测试

2022-05-09 00:00:00 数据 事务 分布式 隔离 级别

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 = 
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: 10001select_type: SQLNodetable:1partitions:type:possible_keys: SELECT id,k,c,pad frm sbtest2 limit 10key: Cluster1,g1,g2key_len:ref: Parent=NULL,Child=NULL,NEXT=NULLrows:filtered:extra: ur,sbtest2=hashmysql > explain select * from sbtest2 limit 10;id: 10001select_type: SQLNodetable:1partitions:type:possible_keys: SELECT id,k,c,pad,gtid as gtid1 from sbtest2 limit 10key: Cluster1,g1,g2key_len:ref: Parent=NULL,Child=NULL,NEXT=NULLrows: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.sqldelete 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中的结果

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

2)测试结果

sort 1.log | uniq c696 404 100011 49110 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/bashoutId=$1inId=$2i=0j=0while [ $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   fidone
  • 读id.dat,发起50个转账进程(mTran.sh)

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

#!/bin/bashoutId=$1inId=$2while truedo  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;dosh getSum.sh $LINE &done

3)测试过程

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

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

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

sort 1.log | uniq c5656  100000094 100000176 999999

附批量kill后台进程语句

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

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

sort 1.log | uniq c5656  100000094 100000176	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实现方案

  3. 数据库系列之GoldenDB中分布式事务实现

相关文章