高并发账户记录查询

【摘要】
面对高并发账户记录查询问题,按照本文的介绍一步一步操作,就能有效提升性能。点击了解高并发账户记录查询

问题描述

高并发账户记录查询在银行、互联网企业、通信企业中广泛存在。例如:网上银行、手机银行、电商个人账户查询、互联网游戏账户等等。这类查询有三个共同点:

1、  数据总量非常大。用户数量本身就非常多,再加上多年的账户数据,数据量可以达到几千万甚至上亿条。

2、  访问人数众多。几百万甚至上千万人访问,属于高并发查询。

3、  不能让用户等待。手机、网页要达到秒级响应,否则严重影响用户体验。

下面以某银行账户活期明细查询为例,给出这类问题的解决办法。

某银行共一亿个活期账户,每个账户平均每月有7条数据,每年数据总量84亿条。每条数据中的机构字段,还要关联分支机构表(几千条)记录。在性能上,要求单台服务器支持一千个以上的查询,响应时间不能超过1秒。

有序行存

活期明细数据随着时间增长非常快,一年就有84亿条。如果放到内存中,需要大量内存空间,硬件投入成本太高,所以要放到硬盘上存储。分支机构表只有几千条数据,可以放在内存中存储。

在硬盘上存储,要考虑是行存还是列存。列存数据分块压缩,能减少遍历数据量。但由于账户查询是随机的,整块读取会有额外解压计算。而且每次取数都针对整个分块,复杂度较高,性能不如行存。因此,这个场景要选择行存存储,如下图:

..

1:行存和列存

具体的实现可以采用JavaC++SPL等高级语言。这里我们以代码量最少的SPL语言为例讲解。


A

1

=connect("db").cursor(“select *   from detail order by id")

2

=file("detailR.ctx") 

3

=A2.create@r(ID,CORPID,AMT).append(B1)

代码示例1

A1:连接生产数据库,用游标读取活期存款数据,按照账户id排序。

A2:建立本地组表文件。

A3:建立组表,并从数据库游标读取活期存款明细数据,写入文件。

 

其中,A3中的@r选项,就是建立行存文件。一年84亿条数据都导出,时间会比较长。但是这是一次性的工作,后续就只需要追加增量数据即可。增量数据的追加方法,后面会有介绍。如果按照账户排序会对生产数据库造成较大压力,可以导出之后基于文件排序。排序使用SPLsortx函数,具体用法参见函数参考。

利用索引

要利用索引提速,先要对明细文件建立索引。由于明细数据量大,建立的索引文件也会很大。很难全部加载到内存中。可以建立多级索引,如下图:

..

2:多级缓存

还是以SPL为例,建立多级索引,只需要在“代码示例1”的基础上,增加一个网格即可:


A

1

=connect("db").cursor(“select *   from detail order by id")

2

=file("detailR.ctx") 

3

=A2.create@r(ID,CORPID,AMT).append(B1)

4

=A2.open().index(index_detailR_id;ID)

代码示例2

A4:对行存文件建立索引文件。

 

加载的索引级别越多,占用存储空间越大。同时,账户id的跨度变小,加载到内存中后,索引效果也会变好。查询时可以根据内存大小,尽可能加载更多级别的索引,可以有效提高查询速度。

在查询之前,系统初始化或者数据变动时,要预先加载多级索引,以SPL代码为例:


A

B

1

if !ifv(detailR)

=file("detailR.ctx").open().index@3(index_detailR_id)

2


=env(detailR,B1)

代码示例3

A1:判断全局变量中是否存在detailR,如果存在,表示已经加载了索引。

B1:如果全局变量中没有detailR,那么打开组表加载三级索引。@2或者@3表示加载2或者3级索引。

B2detailR存入全局变量。

 

这段预先加载的初始化代码(代码示例3),可以保存成init.dfx,放入集算器主目录(main path),在节点机(unitServer)启动的时候会自动被调用,如下图:

..

3:节点机自动调用初始化代码

 

由于我们提前准备好的活期数据是对账户id物理有序的,查询时根据索引找到账户id后,可以在硬盘连续读取,显著减少磁盘IO,有效提速,如下图:

..

4:索引和有序行存

查询账户10100,先利用内存中预先加载的多级索引和索引文件,快速定位到活期明细数据文件中的位置,再连续读取到账户id有变化为止。因为数据是按照账户id物理有序的,所以文件的其他位置不可能再有10100账户的数据了。

利用索引查询的示例代码如下:


A

1

=detailR.icursor   (;ID=="10100",index_detailR_id).fetch()

代码示例4

A1:全程变量detailR已经缓存了三级索引,现在可以基于它利用索引,按照条件取出账户为10100的记录。

 

每个账户的数据量并不大,所以可以全部读入内存。

活期明细前端应用(网页或者APP),要通过集算器的JDBC驱动调用SPL代码查询。调用的时候,需要将账户id作为参数传给SPL程序。例如:定义一个网格参数countid,传入账户id10100A1中的代码就要改为:=detailR.icursor (;ID==countid,index_detailR_id).fetch()

关联查询

查到指定账户数据装入内存后,可以将机构数据也读入内存。在内存中关联计算,性能可以得到保障。示例代码如下:


A

1

=detailR.icursor (;ID==countid,index_detailR_id).fetch()

2

=file("corp.btx").import@b(corp_id,corp_name).keys(corpid)

3

=A1.switch(corp_id,A2:corp_id)

4

return A3.new(id,corp_id.corp_name:corp_name,amt)

代码示例5

A2:读入机构数据。

A3:账号10100的活期明细数据关联机构数据。

A4:将账户id、分支机构名称和金额返回给前端调用者(网页或者APP)。

 

机构数据可以在应用系统初始化的时候加载入内存,不必每次读取,查询速度更快。在上面提到的init.dfx中增加代码如下:


A

B

1

if !ifv(detailR)

=file("detailR.ctx").open().index@3(index_detailR_id)

2


=env(detailR,B1)

3

if !ifv(corp)

=file("corp.btx").import@b(corp_id,corp_name).keys(corpid)

4


=env(corp,B3)

代码示例6

预先加载机构数据之后,查询代码要在“代码示例5”的基础上去掉加载机构数据的部分,修改之后的查询代码如下:


A

1

=detailR.icursor (;ID==countid,index_detailR_id).fetch()

2

=A1.switch(corp_id,corp:corp_id)

3

return A3.new(id,corp_id.corp_name:corp_name,amt)

代码示例7

A2:直接用预先加载的全程变量corp和活期明细数据关联计算,省去了每次查询加载机构数据的时间。

数据更新

活期明细存款是按账号有序的,并不是按日期有序。所以,不能在末尾追加当日新增数据。活期明细几十亿条,如果每天有序归并新数据的话,耗时太长。每月归并一次的方案比较理想。如果将活期明细数据看成是主文件,那么更新原理如下图:

..

5:数据更新

 

数据更新的示例代码如下:


A

B

C

1

if day(now())==1

=file("detailR.ctx").reset()

/每月重整

2

=connect("db").cursor(“select *   from detail where date=?",date(now()))

/读当日数据

3

=file("detailR.ctx").open().append@a(A2)

/每日归并

代码示例8

A1B1:如果是每月1日,重整文件。

A2:从生产数据库中读入当日数据。

A3:将当日数据有序归并到集算器自动生成的补文件中。