获取海量序列的优化方案
在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 > |
以上做法不仅实现了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 > |
事情到此就完美解决了吗?并没有!上述的解决方案,如果再进一步降低获取序列查询的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都是开源项目,他们的思路及实现方法都值得借鉴。
最后,希望本文能够切实帮助到大家。