暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

获取海量序列的优化方案

原创 漫步者 2023-01-04
233

获取海量序列的优化方案

                                                                                                                                                

在BSS3.0计费项目的实施过程中,我们需要短时间内初始化亿级的多张表记录,每条记录的主键值是通过获取TimesTen(以下简称TT)内存数据库中的序列实现的。在应用系统高并发获取序列的时候,TT主机在10几分钟内出现了CPU使用率高达95%的情况,通过简单的加大序列缓存参数并不能解决该问题。同样的问题也存在于其它应用系统使用Oracle序列的过程中,比如我们的“统一接触平台”,它在短时间内需要发送大量的短信,短信记录的唯一ID也是通过获取序列完成的。应用在高并发情况下频繁读取Oracle序列会造成dual虚表的资源竞争,导致数据库和应用的性能下降。我在经历多次思考以后,尝试着用以下思路去解决上述问题。

最为直接的思路就是分析序列的使用场景,如果单独一张表的列值是序列生成的,我们就可以直接在记录被insert操作时候,把ID列对应的值写成对应序列的nextval值,如果再配合批量提交等优化措施,那么性能优化效果应该是非常出色的。如果我们进行的,不仅仅是单独一张表的插入操作,并且其它关系表仍需要引用生成的序列ID,那么这种情况下应用程序就必须先取出序列,然后进行各种操作,这样就造成了数据库事务访问量的翻倍,并且降低了应用的性能。

一种思路是使用NO-SQL去解决该问题:Redis是一款开源的高性能的key-value数据库,通过对数值key的INCR自增得到序列值,即使普通主机的key访问也可以达到100,000TPS的吞吐量,比TT单实例的序列访问60,000TPS高出不少。由于该方案涉及应用改造点较多,我们就放弃了此方案。

我们也考虑自己通过程序生成序列,在主机实现时钟同步的情况下,采用“sequenceID(64bit) = 41(bit)毫秒时间 + 7(bit)应用或者节点编号 + 16(bit)随机数”的方式。这种方式优点是代码逻辑可控,获取序列值可以通过较为灵活的数据交换协议实现,而且实现可以参考Twitter公司的SnowFlake算法;缺点是需要考虑高并发下的实现逻辑,并且序列长度一开始就比较长,序列在数据库中存储时会浪费宝贵的内存和存储空间,另外这个方案同样设计代码改造点较多,所以该方案最终也被放弃了。

另一种思路是又回到原来采用数据库序列的实现方式上,我们考虑采用在多个TT实例上创建相同名称的序列,在增加序列cache的情况下,让这些序列具有不同的起始值,指定特定的步长参数以避免重复。比如三个TT实例的情况下:第一个实例的sequence属性start=0, increment=3;第二个实例的sequence属性start=1, increment=3;第三个实例的sequence属性start=2, increment=3。这样应用在高并发的情况下分别获取以上三个序列,可以采用轮询的算法,这样主机计算资源就能够得到充分的利用,这种方案在并发量适中情况下,效果应该不错,但是在计费系统这种10分钟需要处理2个多亿数据的情况下,虽然缩短了整体处理的总时间,但是主机消耗的CPU资源在短时间内还会飙高的。

Command> create sequence SEQ_TEST   increment by 1   start with 1000   cache 1000   noCycle;

Command> select SEQ_TEST.nextval from dual

       > union all

       > select SEQ_TEST.nextval from dual

       > union all

       > select SEQ_TEST.nextval from dual

       > union all

       > select SEQ_TEST.nextval from dual

       > union all

       > select SEQ_TEST.nextval from dual;

< 1000 >

< 1001 >

< 1002 >

< 1003 >

< 1004 >

最后一种思路就是通过“union all”联合多个获取序列的语句“select seq.nextval from dual”来实现,应用一次从数据库获取多个序列并进行缓存来降低访问序列的TPS,下图是测试过程:

以上做法不仅实现了SQL一次查询得到多个序列,而且在服务端不改造的情况下能有效降低应用获取序列的TPS,并且不需要虚表的配合;但缺点是SQL语法比较冗长。针对语句冗长的问题,可以考虑建一个虚表(类似dual的表),在虚表插入多条数据,使用“select seq_test.nextval from seq_test”一次得到多个序列,seq_test虚表的创建语句为“create table TSEQ_TEST (  DUMMY VARCHAR2(1 BYTE) INLINE)”,插入数据的语句为“insert into TSEQ_TEST values('X')”,测试过程如下图:

Command> create table TSEQ_TEST (DUMMY VARCHAR2(1 BYTE) INLINE);

Command> insert into TSEQ_TEST values('X');

1 row inserted.

Command> insert into TSEQ_TEST values('X');

1 row inserted.

Command> insert into TSEQ_TEST values('X');

1 row inserted.

Command> select SEQ_TEST.nextval from TSEQ_TEST;

< 1005 >

< 1006 >

< 1007 >

在解决计费应用系统获取序列瓶颈的过程中,深圳总部设计人员采纳了上述优化方案,应用线程每次从TT中读取500条序列,采用先进先出的队列缓存多个序列,等缓存的序列使用完后,再重新读取下一次。总部同事协助进行的性能测试结果表明:在序列参数不调整的情况下,高并发测试程序获取序列可以达到70W/TPS惊人的吞吐量,并且CPU资源占用率为60~70%。

事情到此就完美解决了吗?并没有!上述的解决方案,如果再进一步降低获取序列查询的TPS,会导致应用每次查询返回的数据较多,给网络传输造成了一定的压力,另外也对我司的数据库中间件DMDB造成一定压力。如果我们把数据库的序列步长参数设置较大,在充分利用数据库的锁机制解决高并发的情况下,让应用程序一次从数据库获取一个段的序列,并且应用自身缓存序列进行使用岂不美哉?

以序列步长100为例,我们看下面例子:

Command> create sequence SEQ_TEST1   increment by 100   start with 1   cache 1000   noCycle;

Command> select SEQ_TEST1.nextval from dual;

< 1 >

Command> select SEQ_TEST1.nextval from dual;

< 101 >

Command> select SEQ_TEST1.nextval from dual;

< 201 >

 

 

 

 

应用线程每次获取序列段的起始值,并把起始值和自己计算的后99个序列值一并缓存使用,用完以后再获取另外一个段。该方案也适合计费两个序列存在相关性的场景,比如A序列取奇数,B序列取偶数,最终两张表数据会合并到一张。解决上述问题的方法是应用获取一个段的序列,在缓存时根据策略忽略掉不符合要求的序列值。这种方案在充分减少序列查询TPS吞吐量的前提下,又大大增强了网络的传输效率。

以上内容,讲述了海量数据获取序列唯一值的种种方案以及改进思路,大家在参考和使用上述方案的过程中可以根据自身应用的特点、数据量的大小进行灵活使用。从长远的角度考虑,大型分布式项目一定要有自己的序列生成器,美团的Leaf,滴滴的tinyid,百度的UidGenerator都是开源项目,他们的思路及实现方法都值得借鉴。

最后,希望本文能够切实帮助到大家。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论