首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 数据库 > 其他数据库 >

数据库对象的缓存谋略

2012-11-08 
数据库对象的缓存策略数据库对象的缓存策略前言本文探讨Jive(曾经开源的Java论坛)和Hibernate(Java开源持

数据库对象的缓存策略
数据库对象的缓存策略

前言
本文探讨Jive(曾经开源的Java论坛)和Hibernate(Java开源持久层)的数据库对象的缓存策略,并阐述作者本人的Lightor(Java开源持久层)采用的数据库对象缓存策略。
本文的探讨基于以前开源的Jive代码,Hibernate2.1.7源码,和作者本人的Lightor代码。
本文用ID (Identifier的缩写)来代表数据记录的关键字。
数据对象查询一般分为两种:条件查询,返回一个满足条件的数据对象列表; ID查询,返回ID对应的数据对象。
本文主要探讨“条件查询”和“ID查询”这两种情况的缓存策略。
本文只探讨一个JVM内的数据缓存策略,不涉及分布式缓存;本文只探讨对应单表的数据对象的缓存,不涉及关联表对象的情况。

一、Jive的缓存策略
1.Jive的缓存策略的过程描述:
(1)条件查询的时候,Jive用 select id from table_name where …. (只选择ID字段)这样的SQL语句查询数据库,来获得一个ID列表。
(2) Jive根据ID列表中的每个ID,首先查看缓存中是否存在对应ID的数据对象:如果存在,那么直接取出,加入到 结果列表中;如果不存在,那么通过一条select * from table_name where id = {ID value} 这样的SQL查询数据库,取出对应的数据对象,放入到结果列表,并把这个数据对象按照ID放入到缓存中。
(3) ID查询的时候,Jive执行类似第(2)步的过程,先从缓存中查找该ID,查不到,再查询数据库,然后把结果放入到缓存。
(4) 删除、更新、增加数据的时候,同时更新缓存。
2.Jive缓存策略的优点:
(1) ID查询的时候,如果该ID已经存在于缓存中,那么可以直接取出。节省了一条数据库查询。
(2) 当多次条件查询的结果集相交的情况下,交集里面的数据对象不用重复从数据库整个获取,直接从缓存中获取即可。
比如,第一次查询的ID列表为{1, 2},然后根据ID列表的ID从数据库中一个一个取出数据对象,结果集为{a(id = 1),  b(id = 2)}。
下一次查询的ID列表为{2, 3},由于ID = 2的数据对象已经存在于缓存中,那么只要从数据库中取出ID = 3的数据对象即可。
3.Jive缓存策略的缺点:
(1) 在根据条件查找数据对象列表的过程中,DAO的第(1)步用来获得ID列表的那一次数据库查询,是必不可少的。
(2) 如果第(1)步返回的ID列表中有n个ID,在最坏的命中率(缓存中一个对应ID都没有)情况下,Jive还要再查询n次数据库。最坏情况下,共需要n + 1数据库查询。

二、Hibernate的二级缓存策略
Hibernate用Session类包装了数据库连接从打开到关闭的过程。
Session内部维护一个数据对象集合,包括了本Session内选取的、操作的数据对象。这称为Session内部缓存,是Hibernate的第一级最快缓存,属于Hibernate的既定行为,不需要进行配置(也没有办法配置 :-)。
Session的生命期很短,存在于Session内部的第一级最快缓存的生命期当然也很短,命中率自然也很低。当然,这个Session内部缓存的主要作用是保持Session内部数据状态同步。
如果需要跨Session的命中率较高的全局缓存,那么必须对Hibernate进行二级缓存配置。一般来说,同样数据类型(Class)的数据对象,共用一个二级缓存(或其中的同一块)。
1.Hibernate二级缓存策略的过程描述:
(1)条件查询的时候,总是发出一条select * from table_name where …. (选择所有字段)这样的SQL语句查询数据库,一次获得所有的数据对象。
(2) 把获得的所有数据对象根据ID放入到第二级缓存中。
(3) 当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存。
(4) 删除、更新、增加数据的时候,同时更新缓存。

2.Hibernate二级缓存策略的优点:
(1) 具有Jive缓存策略同样的第(1)条优点:ID查询的时候,如果该ID已经存在于缓存中,那么可以直接取出。节省了一条数据库查询。
(2) 不具有Jive缓存策略的第(2)条缺点,即hibernate不会有最坏情况下的 n + 1次数据库查询。
3.Hibernate二级缓存策略的缺点:
(1) 同Jive缓存策略的第(1)条缺点一样,条件查询的时候,第(1)步的数据库查询语句是不可少的。而且Hibernate选择所有的字段,比只选择ID字段花费的时间和空间都多。
(2) 不具备Jive缓存策略的第(2)条优点。条件查询的时候,必须把数据库对象从数据库中整个取出,即使该数据库的ID已经存在于缓存中。

三、Hibernate的Query缓存策略
可以看到,Jive缓存和Hibernate的二级缓存策略,都只是针对于ID查询的缓存策略,对于条件查询则毫无作用。(尽管Jive缓存的第(2)个优点,能够避免重复从数据库获取同一个ID对应的数据对象,但select id from …这条数据库查询是每次条件查询都必不可少的)。
为此,Hibernate提供了针对条件查询的Query缓存。
1.Hibernate的Query缓存策略的过程描述:
(1) 条件查询的请求一般都包括如下信息:SQL, SQL需要的参数,记录范围(起始位置rowStart,最大记录个数maxRows),等。
(2) Hibernate首先根据这些信息组成一个Query Key,根据这个Query Key到Query缓存中查找对应的结果列表。如果存在,那么返回这个结果列表;如果不存在,查询数据库,获取结果列表,把整个结果列表根据Query Key放入到Query缓存中。
(3) Query Key中的SQL涉及到一些表名,如果这些表的任何数据发生修改、删除、增加等操作,这些相关的Query Key都要从缓存中清空。
2.Hibernate的Query缓存策略的优点
(1) 条件查询的时候,如果Query Key已经存在于缓存,那么不需要再查询数据库。命中的情况下,一次数据库查询也不需要。
3.Hibernate的Query缓存策略的缺点
(1) 条件查询涉及到的表中,如果有任何一条记录增加、删除、或改变,那么缓存中所有和该表相关的Query Key都会失效。
比如,有这样几组Query Key,它们的SQL里面都包括table1。
SQL = select * from table1 where c1 = ? ….,  parameter = 1, rowStart = 11, maxRows = 20.
SQL = select * from table1 where c1 = ? ….,  parameter = 1, rowStart = 21, maxRows = 20.
SQL = select * from table1 where c1 = ? …..,  parameter = 2, rowStart = 11, maxRows = 20.
SQL = select * from table1 where c1 = ? …..,  parameter = 2, rowStart = 11, maxRows = 20.
SQL = select * from table1 where c2 = ? ….,  parameter = ‘abc’, rowStart = 11, maxRows = 20.

当table1的任何数据对象(任何字段)改变、增加、删除的时候,这些Query Key对应的结果集都不能保证没有发生变化。
很难做到根据数据对象的改动精确判断哪些Query Key对应的结果集受到影响。最简单的实现方法,就是清空所有SQL包含table1的Query Key。

(2) Query缓存中,Query Key对应的是数据对象列表,假如不同的Query Key对应的数据对象列表有交集,那么,交集部分的数据对象就是重复存储的。
比如,Query Key 1对应的数据对象列表为{a(id = 1), b(id = 2)},Query Key 2对应的数据对象列表为{a(id = 1), c(id = 3)},这个a就在两个List同时存在了两份。

4.二级缓存和Query缓存同步的困惑
假如,Query缓存中,一个Query Key对应的结果列表为{a (id = 1) , b (id = 2), c (id = 3)}; 二级缓存里面有也id = 1对应的数据对象a。
这两个数据对象a之间是什么关系?能够保持状态同步吗?
我阅读Hibernate的相关源码,没有发现两个缓存之间的这种同步关系。
或者两者之间毫无关系。就像我上面所说的,只要表数据发生变化,相关的Query Key都要被清空。所以不用考虑同步问题?

四、Lightor的缓存策略
Lightor是我做的Java开源持久层框架。Lightor的意思是,Lightweight O/R。Hibernate,JDO,EJB CMP这些持久层框架,都是Layer。Lightor算不上Layer,而只是一个Helper。这里的O/R意思不是Object/Relational,而是Object/ResultSet的意思。:-)
Lightor的缓存策略,主要参照Hibernate的缓存思路,Lightor的缓存也分为 Query缓存和ID缓存。但其中有一点不同,两者之间并不是毫无联系的,而是相互关联的。
1.Lightor的缓存策略的过程描述:
(1) 条件查询的请求一般都包括如下信息:SQL, 对应SQL的参数,起始记录位置(rowStart),最大记录个数(maxRows),等。
(2) Lightor首先根据这些信息组成一个Query Key,根据这个Query Key到Query缓存中查找对应的结果ID列表。注意,这里获取的是ID列表。
如果结果ID列表存在于Query缓存,那么根据这个ID列表的每个ID,到ID缓存中取对应的数据对象。如果所有ID对应的数据对象都找到,那个返回这个数据对象结果列表。注意,这里获取的是整个数据对象(所有字段)的列表。
如果结果ID列表不存在于Query缓存,或者结果ID列表中的某一个ID不存在于ID缓存,那么,就查询数据库,获取结果列表。然后,把获取的每个数据对象按照ID放入到ID缓存;并组装成一个ID列表,按照Query Key存放到Query缓存中。注意,这里是把ID列表,而不是整个对象列表,放入到Query缓存中。
(3) ID查询的时候,Lightor先从ID缓存中查找该ID,如果不存在,那么查询数据库,把结果放入ID缓存。
(4) Query Key中的SQL涉及到一些表名,如果这些表的任何数据发生修改、删除、增加等操作,这些相关的Query Key都要从缓存中清空。
2.Lightor的缓存策略的优点
(1) Lightor的ID缓存具有Jive缓存,和Hibernate二级ID缓存的优点。ID查询的时候,如果该ID已经存在于缓存中,那么可以直接取出。节省了一条数据库查询。
(2) Lightor的Query缓存具有Hibernate的Query缓存的优点。条件查询的时候,如果Query Key已经存在于缓存,那么不需要再查询数据库。命中的情况下,一次数据库查询也不需要。
(3) Lightor的Query缓存中,Query Key对应的是ID列表,而不是数据对象列表,真正的数据对象只存在于ID缓存中。所以,不同的Query Key对应的ID列表如果有交集,ID对应的数据对象也不会在ID缓存中重复存储。
(4) Lightor的缓存也没有Jive缓存的最坏情况n + 1次数据库查询缺点。
3.Lightor的缓存策略的缺点
(1) Lightor的Query缓存具有Hibernate的Query缓存的缺点。条件查询涉及到的表中,如果有任何一条记录增加、删除、或改变,那么缓存中所有和该表相关的Query Key都会失效。
(2) Lightor的ID缓存也具有hibernate的二级ID缓存具有的缺点。条件查询的时候,即使ID已经存在于缓存中,也需要重新把数据对象整个从数据库取出,放入到缓存中。

五、Query Key的效率
Query缓存的Query Key的空间和时间开销比较大。
Query Key里面存放的东西不少,SQL, 参数,范围(起始,个数)。
这里面最大的东西就是SQL。又占地方,又花时间(hashCode, equals)。
Query Key最关键的两个方法是hashCode和equals,重点是SQL的hashCode和equals。

Lightor的做法是,由于Lightor直接使用SQL,不用HQL、OQL之类,所以推荐尽量使用static final String的SQL,能够节省空间和时间,以至于Query Key的效率能够相当于ID Key的效率。
至于Hibernate的QueryKey,有兴趣的读者可以去下载阅读Hibernate的各个版本的源代码,跟踪一下QueryKey的实现优化过程。

六、总结
这里列一个表,综合表示Jive, Hibernate, Lightor的缓存策略的特征。
     N + 1问题重复ID缓存问题Query缓存支持
Jive缓存          有无不支持
Hibernate缓存无有支持
Lightor缓存无有支持

注:
“重复ID缓存问题”的含义是,每次条件查询,不是只取ID列表,而是取出完整对象(所有字段)的列表。这样,同一个ID对应的数据对象,即使在缓存中已经存在,也可能被重新放入缓存。参见相关缓存的缺点描述。
“重复ID缓存问题”的负面效应到底有多大,就看你的select id from …(只选择ID)比你的 select * from … (选择所有字段)快多少。主要影响因素是,字段的个数,字段值的长度,与数据库服务器之间网络传输速度。
不管怎么说,即使选择所有字段,也只是一次数据库查询。而N + 1问题带来的可能最坏的负面效应(N + 1次数据查询)却是非常大的。
选择缓存策略的时候,应根据这些情况发生的概率和正负面效应进行取舍。

-----  added later

看到Robbin在04年6月的一篇相关文章。

Hibernate Iterator JCS分析
http://www.hibernate.org.cn/71.html

Hibernate Iterator JCS分析 写道
而Hibernate List方式是JDBC的简单封装,一次sql就把所有的数据都取出来了,它不会像Iterator那样先取主键,然后再取数据,因此List无法利用JCS。不过List也可以把从数据库中取出的数据填充到JCS里面去。

最佳的方式:第一次访问使用List,快速填充JCS,以后访问采用Iterator,充分利用JCS。
1 楼 庄表伟 2004-12-22   支持!
有没有Lightor的源代码可以看啊? 2 楼 buaawhl 2004-12-22   庄表伟 写道支持!
有没有Lightor的源代码可以看啊?

多谢庄表伟的支持。:-)
对了,我根据fastm+ 0.6 的特性,也在fastm里面加入了对应的解析引擎。不用换行也可以解析,多行也可以解析。
我正在用fastm做一个forum,里面的template用fastm,持久层用Lightor。
准备到时候发布forum的时候,一起发布fastm和lightor的更新版本。

一.前因
Lightor我是很久之前做的,在fastm之前。一直不好意思拿出来献丑。
写这个帖子有两个目的:
1. 我一直在关注 DuDo的OpenORM。
OpenORM是一个JDO, Hibernate 级别的项目,是一个完整的layer。
Lightor是一个JAXOR, iBatis级别的项目,是一个jdbc helper。
看到DuDo说缓存部分还没于做,我恰好写了缓存这部分。希望能做个参考。

2. Fastm Forum里面用到了Lightor。
Fastm Forum过一段时间会发布,恐怕这个陌生的Lightor会影响潜在的对Fastm Forum感兴趣的开发人员。一定会有这样的评论,为什么不用Hibernate呢?(为什么不用WebWork呢?为什么不用Spring呢?虽然,我不用Hibernate,Spring,但并不妨碍我对Hibernate、Spring的敬仰、学习和参照。:-)
所以想借此机会,介绍一下Lightor。

二.Lightor特性简介
工作中,我习惯了写 长而且复杂的SQL语句(有时候需要带有一些Native SQL feature),并希望能够随时把这个SQL放到数据库里面跑一下,Explain Plan一下,调整join的顺序,hint的种类,提高SQL的效率。Delete, update一批数据这样的SQL我也想用。
以前用Entity Bean的时候,对象关联会给Entity Bean的分发造成很大的麻烦,所以,也养成了不使用关联的习惯。另外,使用关联对象,总是要担心效率,实现难度也很大。
至于Criteria这样的SQL拼装帮助器,根据我的习惯,还不如直接拼装SQL直观。

Lightor不解释QL,Lightor直接使用SQL。Lightor不支持对象关联。当然,Lightor是支持和提倡Join SQL的。
Lightor有些像.net的DataSet,是一个把ResultSet映射为Object的工具。
Lightor不支持POJO。Lightor有三个基本类。
ReadOnlyRow用来对应只读记录,比如View的记录。UpdatableRow用来对应表记录。CompositeRow用来对应Join SQL语句的多表记录。
UpdatableRow具有getter, setter方法。用户修改了它的属性之后,可以直接调用insertRow, updateRo, deleteRow方法。Lightor帮助你拼装update, delete, insert语句。当然,select 语句还是要你自己写。
其实,Hibernate, JDO也是如此,主要帮你拼装update, delete, insert, select by id语句。其他的select 语句还是需要你自己写HQL。
Session主要是对connection.open() 到 connection.close()的包装。
(Local)Transaction主要是对 上一个connection.commit() 到下一个connection.commit() 之间的包装。至于,Global Transation的包装,我的感觉,就更没有必要了。既然需要用到Global Transation,一定是非常复杂关键的业务,直接使用JTA都不能让人完全放心呢,何况使用第三方的包装。
Lightor不包装Session,不包装Transaction。我的原则是,没有必要包装的,就不包装,包装不好的,就不包装。否则,包装的结果是阻碍用户,而不是便利用户。
Lightor的方法显示的需要Connection参数。Lightor的任务就是帮你拼装SQL,然后执行。Lightor的SQL拼装功能也是暴露给用户的,用户可以只适用Lightor来拼装SQL,然后自己选择怎么执行。
可见,Lightor完全是以SQL, ResultSet为中心的。是一个Lightweight Object/ResultSet Mapping工具。

一定会有人问,那为什么不用JAXOR,或者iBatis啊。
我曾以为JAXOR正是我需要的,但发现还是不合用。JAXOR也包装了Session Context是通过Thread Local来传输的(也不能说不好,只是个人习惯。比如,很多人还认为暴露Connection参数很不好);JAXOR生成的一套DB Object类和DAO类有些厚重。
iBatis用的既不是Object QL,也不是SQL,而是SQL Mapper。除了提供把ResultSet映射为Object,没有更多的附加功能,还增加了一步解析SQL Mapper的负担。JAXOR和Lightor一样,不解析,直接使用SQL。HQL, OQL需要多一步解析,至少提供了很多OO的特性。我真的想不到,iBatis的应用的场合。

至于Lightor的Cache机制,上一个帖子,有介绍。
如果要使用Cache,必须显示调用Lightor的Cache支持方法,并从外部提供Cache。Lightor本身不管理Cache,用户自己管理Cache。这样设计的原因是,让用户更好的控制DB Object Cache和 Page Cache之间的同步。用户完全可以把相关的DB Object 和 Page 放到同一个Cache里面。
比如,论坛的 message类object,和message经过(图片、表情)过滤后的显示页面,可以放到同一个Cache里面。清空的时候,一起清空。
不会存在OSCache TagLib和Hibernate Cache之间相互不知道的问题。Sourceforge Forumnuke的作者magic2k的blog讨论了这个问题。只能通过Page Cache Taglib的Timeout来实现。我感觉,这是一种很蹩脚的方法。我个人喜欢完全完整(至少基础理论模型没有缺陷)的控制和解决,而不喜欢凑合的方案。

至于POJO,我觉得,让框架动态改我的代码,总觉得有些不放心,感觉不能完全控制自己的东西。这也是我虽然觉得AOP不错,但不愿意使用的原因。
至于代码量,使用Lightor的代码量和使用其他O/R Mapping的代码量差不多。
SQL <-> HQL,  save or update <-> insert, update, getter, setter。就是关联对象部分,Lightor的代码会多一些。

至于关联对象,我举个例子。
一个 购买单 里面有 product id。关联 product。
一个 进货单 里面也有 product id。关联 product。
这样,可以根据 购买单 或 进货单的记录,直接获得相关的 product。这样确实可以节省代码。但是却多了两个关系。
在Lightor里面,必须用一个公用方法,getProductById()明确要求获得product。这样,多了这个方法的调用,但是却少了两个关系。

三、Lightor的source
我很早就在Sourceforge上,申请了Lightor项目。但一直没有把源代码发布上去。主要是 insert 部分,incremental, sequence只通过了MySQL, Oracle的测试。我没有其他数据库。还有就是,并不觉得Lightor有什么值得推荐的。
我对Lightor没什么太大信心,不像对Fastm那样有十足的信心。
真正的Lightor发布代码,将在Fastm Forum发布的时候,一起发布。
这里,我先把Lightor现在的源代码放在这个帖子的附件里。
希望 庄表伟 等感兴趣的朋友先睹为快,一起讨论,进一步改进。

题外话,关于Webapp开发
以我的有限经验,关于Webapp(主要是Web MIS)的开发, 50% - 60%是UI Logic,我用fastm对付,能够把Web UI Logic的开发结构化,对象化,就等于成功了一半。10% - 20%是DB Object 和DAO,我用Lightor对付,不一定比其他O/R Mapping对付的好。其余的20 %– 30%是Business Logic,也许要用到工作流。 3 楼 庄表伟 2004-12-24   我现在在做的工作,和你的也有些类似。

还记得我的ValueSet定义吗?

我将ValueSet作为整个框架的基础,这个框架不但包含fastm+,也包括为了得到fastm+能够使用的ValueSet而提供的业务逻辑层框架以及O/R框架。

ValueSet作为数据存放的基本方式,贯穿整个体系。

目前由于我是在为公司开发项目,所以不便公开源代码。等我将这个项目完成之后,我会写一些介绍的文章的。 4 楼 zhuam 2005-01-20   Cache方案,要针对实际情况而设计,作者分析的不错,受益中 5 楼 firebody 2005-01-27   DAO层以及数持久层的cache策略确实值得仔细研究,毕竟它对项目的影响是很大的,特别在需要提高性能的时候。
hibernate的二级cache是提高整体性能的关键。
引用(3) 当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存。
这点还需要做特殊说明,只有session.load By Id,才会从二级缓存中提取数据。
引用
(4) 删除、更新、增加数据的时候,同时更新缓存。
这点说的比较模糊,虽然道理如此,但是并不是任何有关联的对象缓存都会得到更新。 在父子集合关系的维护上,如果启用了二级缓存,这点尤其值得注意。 注意缓存/内存中的数据 和 数据库底层数据的一致性。
引用3.Hibernate的Query缓存策略的缺点
(1) 条件查询涉及到的表中,如果有任何一条记录增加、删除、或改变,那么缓存中所有和该表相关的Query Key都会失效。
嘿嘿,补充:hinerate提供的API保证对querycache的更新,但不能保证绝对与数据库底层同步。比如直接sql update。 6 楼 buaawhl 2005-01-30   多谢补充.



(4) 删除、更新、增加数据的时候,同时更新缓存。
这点说的比较模糊,虽然道理如此,但是并不是任何有关联的对象缓存都会得到更新。 在父子集合关系的维护上,如果启用了二级缓存,这点尤其值得注意。 注意缓存/内存中的数据 和 数据库底层数据的一致性。


父子集合关系的缓存维护,我不是很清楚。能详细展开讲讲吗?

-- 关于QueryCache
firebody 写道
嘿嘿,补充:hinerate提供的API保证对querycache的更新,但不能保证绝对与数据库底层同步。比如直接sql update。


ReadOnly 写道
http://forum.iteye.com/viewtopic.php?t=10080#55767

Hibernate的Query Cache策略就是和CafeBabe说的一样,给本次操作中用到的所有Entity做上失效的标志,具体的代码在net.sf.hibernate.cache.UpdateTimestampsCache

在使用Hibernate的Query Cache时候,不需要担心会发生Cache和数据库不一致的情况,Hibernate会帮偶们处理。但是如果不通过Hibernate代码,直接修改数据库内容,Hibernate的Cache就一无所知了,所以Hibernate暴露了Cache的清除方法,给偶们一个擦屁股的机会。
引用

-- 关于Hibernate 源码

Gigix发了CSDN 技术专栏征求作家广告。
http://forum.iteye.com/viewtopic.php?t=10567

了解 Hibernate 源码 的朋友们,有两个栏目可以选择。
1.   O / R Mapping。
2.   源码剖析。

热点排行