云卷云舒:算力网络+云原生(下):云数据库发展的新篇章-CSDN博客
一、现有技术的技术方案
在实现一个具有复杂业务逻辑的应用系统时,大多数情况下,编码过程中必定会包含着较多的数据访问方法(java中称之为方法)或函数(c中称之为方法),同样也就意味着多次的数据库连接。一方面复杂的逻辑会将程序的响应时间拖延过长,另一方面多次的数据库连接势必会给数据库带来过多的访问压力。
那么在架构时有效的解决上述问题就十分必要,综合了目前业界众多主流方案,最终可以归纳为以下几种:
1、并发方案
该方案的思想,即将所有涉及读取数据库的方法予以分类,将相互之间没有依赖关系(即A方法的执行不依赖于B方结果的情况)的一类方法通过并发的方案执行,这样N个方法的执行时间理论上就等于响应时间最大的那个方法的响应时间,避免了其他N-1个方法的响应时间,整体上提高了程序的执行效率;同时辅助以线程池来进行线程管理和预防线程泛滥。
该解决方案架构难度较低,通过编写并发逻辑实现响应时间优化,为目前比较常用的一种软件架构方案。
2、分布式缓存方案
该方案的思想,即将经常需要读取的数据缓存起来,而且是通过分布式的缓存模块来承载数据,数据读取时可以快速的从缓存内读取内容。避免了大量的数据库连接,提高访问效率的同时,也降低了数据库服务的访问压力。该方案是目前应用最广泛的架构方式,而且优化效果也比较明显。
二、技术分析
针对前面提到的二种主流解决方案进行详细分析后,可以得出其各自场景下的应用弊端:
1、并发方案
该方案中,通过将没有依赖关系的方法归为一类进行并发执行,但是:
(1)并发模型需要编写专门的并发方法,一般的并发方法都要通过比较复杂的代码逻辑实现,如“线程池维护”、“多类异常处理”、“线程安全控制”对于编码者来说都是比较复杂且容易出错的环节,依赖于编码者编程水平,如若控制不当很容易出现内存溢出、线程泛滥的后果。
(2)并发方案并没有减少针对数据库的访问次数,数据库同样面对着较大的压力。
所以该方案并没有很好的针对复杂业务逻辑带来的一系列问题进行优化和解决。
2、分布式缓存方案
该方案中,将经常需要读取的数据通过分布式的缓存模块来存储加速,但是:
(1)与方案1-并发方案一样,编码者同样需要编写复杂的缓存逻辑,来有效的控制缓存的过期策略、更新策略、删除策略等,同样在编码层次上对编码者要求较高,如若处置不当也容易引起同样的风险。
(2)实现缓存模型的通用办法均通过为某一类需要缓存的数据建立一个Key-Value的内存临时库,当数据第一次被访问时将其读入内存内,如果在设定的缓存有效周期内,该数据再没有被读取则删除,如果在一个有效周期内被频繁读取,则定期更新数据(更新时间均小于有效周期)。那么很有可能很多的数据都是在读取一次过后即被读入缓存,就再也未被使用过,但是在一个缓存有效期内仍然会占据内存空间,所以该缓存模型仍存在着巨大的优化空间。
(3)单独与应用程序部署的分布式缓存组件,在应用程序进行读取时势必带来了额外的网络IO开销,而且如果分布式缓存组件服务异常,应用程序很有可能会受到影响
本文内设计了一种架构,从业务逻辑的关联度出发,在应用程序代码段和数据库连接的建立之间增加了一个数据访问代理环节,实现在一次数据库访问的同时为下一次可能的读取做预加载操作,提高数据的访问效率。
三、基于业务关联度的数据预加载方法
本方案的出发点是考虑到应用程序运行的时候,代码逻辑都是固定的,那么对于数据库来说完成一次完整业务处理的过程中,数据库的读取顺序都是固定的,即一段完整的业务逻辑内各方法之间的关联度是十分有挖掘价值的。即程序执行过A 数据库请求(DB_REQ)后必定(或一定概率)会执行B DB_REQ,那么在程序执行A DB_REQ的时候,就为其准备好B DB_REQ的信息,再下一次的B DB_REQ到来的时候,就可以直接的反馈结果,极大地提高了访问效率,也降低了实际的数据库的访问效率。
下面描述预加载模型。
首先针对业务逻辑内的方法定义模型如下图:
请求 | 方法 | 参数 | SQL示例 |
R1 | Method 1 | P1,P2 | Select * from table where k1=p1 and k2=p2 |
R2 | Method 2 | P1,P2,P3 | Select * from table where k1=p1 and k2 in (P2,P3) |
R3 | Method 1 | P1,P3 | Select * from table where k1=p1 and k2=p3 |
…. | |||
n | Method N | P1,P2,..,PN | Select * from table where k1<p1 and k2 in (P2,P3,…,PN) |
上图中,方法一列代表着整个应用程序内所有涉及数据库读取的方法或函数,命名为Method1,2,…,N,同一个方案可能存在不同个数的参数,相同参数个数的方法也存在着参数取值的区别,所以“方法”和“参数”两个字段可以唯一的标识一次请求,即可以代表一次SQL查询操作。
预加载模型如下图:
上图中,预加载模型包括代理模块、关联度量模块和内存池三部分。整个模型的工作流程描述如下:
- 应用程序内发送DB请求R1到达预加载模型的代理模块,除了携带本方法所需的请求参数外,还携带下一次请求的预告知目标信息(如R2,代表在相同的一个线程内,下一次就会发起目标R2请求),和距离下一次请求的最大延时T1(告知代理模块R2请求最晚会在T1时间后发出)。
- 代理模块收到R1后,为R1请求DB,获取结果集,同时为R2请求DB获取结果集,暂存于本地缓存内。同时等待R2的真实请求到达代理模块,如果R2未在T1时间内到达代理模块,则忽略之前为R2的代理请求结果集(从本地缓存内删除R2代理请求结果集);如果R2在T1时间内被接收到,那么认为R1所携带的预告知信息可信,则在代理模块内置的关联度量表中查找是否存在CR列为R1且NR为R2的记录行,如果存在则将HN和TN列加一,且计算出CV列值,如果不存在则初始化一条记录。(注:CR、NR、HN、TN、CV等值在附录中描述。)
3.通过不断的迭代运算,代理模块内置的关联度量表中会不断丰富,且条数不断增长,但是数据再表中的保存时间不会无条件延长,本方案中设计了一个失效模型。如下:
模型默认所有记录行的有效期为InitValidCycle=10min,那么无效数据的认定规则为:每间隔1min对该表记录进行扫描:
- 如果CV值小于黄金分割值GSV(Golden section value) = 0.618,即认为该预测记录的置信度较低,可以清洗表内的记录,并删除对应的本地缓存的数据,释放内存空间。
- 如果一条记录的CV值一直大于等于黄金分割值GSV,但是在表内已经超过InitValidCycle=10min,则同样认为该行记录不可信,可以清洗表内的记录,并删除对应的本地缓存的数据,释放内存空间。
4.实际请求到达时,代理模块首先去内存池读取预加载的数据,如果没有读取到,则会实际的发起一次真实的DB,获取结果为程序段返回。
经过不断的迭代运算,数据预加载的准确度将越来越高,使得即使复杂的业务逻辑下应用程序也可以比较高效的读取服务端数据,同时内存中缓存的数据都是经过不断清洗淘汰后剩余的数据,均为价值较大的数据,极大地提高了内存资源的利用率。
本方案使用方式如下:
- 代理模块以SDK包的方式引入程序工程内,在实际编写业务代码的时候,需要在请求进入的位置添加SDK包内的方法,使得代理程序生效;如果不引入SDK包,则在程序内无法直接使用SDK包内的方法。
- 代码编写方式举例如下:
如未经过本方案优化时,代码段举例为:
Obj o1 = db.getDataFromDB(id1); (R1请求)Obj o2 = db.getDataFromDB(id2); (R2请求)Obj o3 = db.getDataFromDB(id3); (R3请求)
经过本方案优化后,代码段举例为:
Obj[] o = Proxy.getDataFromProxy(id1,id2,id3);
//定义代理方法
Proxy.getDataFromProxy(id1,id2,id3,T1){Obj o1 = db.getDataFromDB(id1); (R1请求)Obj o2 = db.getDataFromDB(id2); (R2请求)Obj o3 = db.getDataFromDB(id3); (R3请求)}
- 按照上图实例,代码不是像以前一样分别按顺序发出R1R2R3三个请求,而是向代理模块Proxy发出请求,利用代理Proxy. getDataFromProxy方法,把原本逻辑写在代理方法内,由代理方法getDataFromProxy管理三个请求R1R2R3,在请求getDataFromProxy方法的时候,传入了id1,id2,id3,和延时参数T1,那么在请求代理方法getDataFromProxy的时候,在完成R1请求的同时,会同时把R2R3两个请求都进行预加载(当然符合前面(1)(2)所述的T1延时相关要求)。
- 前面实例是将R1R2R3同时放到一个代理方法内,代表着请求R1的时候,同时带入了R2R3所需要的参数id2和id3以及延时参数T1,那么则完成了R2R3的预加载。
- 同理,如果代理方法只传入R1R2和T1,则代表着仅完成R2的预加载;如果只传入R2R3和T1,则代表着仅完成R3的预加载。
仅个人观点,供讨论。