这个时候,比较sql1和sql2和sql3的效率就会大大提高,虽然sql1 和 sql2两个数组的长度相等,还是要一个元素一个元素的比较,但由于里面大量用到了String常量,相同的String常量具有相同的reference,所以5步下来,就可以判断出sql1和sql2数组的元素是完全相等的;4步下来,加上第一个字符的比较,就可以判断sql1和sql3的第4个元素是不相等的。
我们看到,做法1和做法2,能够100%的提高SQL的比较效率,大部分情况下,也许比parameters的比较还快。
三、定长预取
多用户访问同一页面的可能性比较大的情况下,比如,论坛的某些热门话题,很可能被多人同时翻阅。这时候,如果把根据范围取出的数据对象List也按照QueryKey存入缓存中,那么就可以大大提高响应速度,减轻数据服务器负担,当然,你的Web Server的内存负担也大大增加了。
我们进一步考虑下面两种情况:
1. 用户自定义页面记录数
一般来说,用户可以自定义自己的每页显示记录个数,比如,有些用户喜欢每页20条,有的喜欢每页10条。
假设用户A翻到一个论坛的第一页,显示1 – 20条信息;用户B翻到同一个论坛的第一页,显示1 – 10条信息。这个时候,缓存的命中率是很低的。用户A和用户B无法共享缓存信息。因为他们的range(的span)总是不同,QueryKey永远不可能相同。
2. 记录很多、每页记录数过少
假设一个论坛里面有1000条信息,每页显示10条,那么共有100页。如果用户一页一页的翻动,每次程序发出一个span大小为10的Query请求,取出10条记录,根据QueryKey缓存起来。由于页面记录数过少,每次数据库查询的效率很低,缓存命中率也很低。
为了提高缓存命中率,并且顺便实现数据预取功能,我们可以采取 同一定长Span的方案。比如,还是上面的例子,我们在程序中设定统一Span大小为100。
当用户A请求1 – 10的记录的时候,程序判断这个落在 1 – 100的范围内,那么用range (1, 100)获取100条记录,把前面的10条返回给用户。当用户A翻了一页,请求11 – 20的记录的时候,程序判断还是落在 1 – 100的范围内,而且已经存在于缓存中,那么直接把对应的11 – 20条返回给用户A就可以。
当用户B 请求1 – 20的记录的时候,程序判断这个落在 1 – 100的范围内,而且已经存在于缓存中,那么直接把对应的1 – 20条返回给用户B就可以。
可以看到,这种定长预取方案能够大大提高数据库查询的效率和缓存的命中率。
关于Cache & QueryKey 部分,偶有1个问题:你是如何做到cache的自动清理和聪明地清理?
举个例子
假设有这样的查询语句:select * from message where message_to = ?
执行了2次值不同的操作:'buaawhl' 和 'Readonly'
那么就有2个不同QueryKey对应到Cache里的对象。
这个时候再执行一个write的操作:往message表里面插入了一条message_to等于‘buaawhl’的记录,那么之前在Cache里QueryKey为'buaawhl'的对象会不会自动失效?而QueryKey为'Readonly'的对象是否还能保持有效呢?
没有这么智能。我现在无法做到 cache的自动清理和聪明地清理。
我现在做的持久层本身是不做cache的清理管理工作,这个工作交给 调用程序自己去做。cache也需要用户自己实现,并且明确提供。
查询的时候,需要指定cache
java代码: |
finder.setRowClass(Message.class); finder.setCache(cache); finder.queryRowsRange(conP, sql, params, offset, span);
|
这个时候,finder会用 QueryKey(sql, params, offset, span) 作为Key,
从指定的cache里面,查找对应的记录集合。
如果查不到,从conP真正获取一个connection,连接并查找数据库,把结果放到cache里面。
这里有个优化,如果指定了缓存,而且只有当缓存中不存在目标数据的时候,才真正地从连接池中获取connection。主要是考虑到连接池的大小总是有限的,如果并发用户多的话,这样就可以节省连接池里的connection的分配。
---
update, delete, update的时候,用户需要手动自己清理cache的内容。
java代码: |
persister.insertRow(con, message); cache.clear(); // 或者更智能的操作, 比如, 根据message的message_to value, // 清理Cache里面的符合下列条件的QueryKey // (sql 包含 message_to = ?, 并且 params[0] = buaawhl)
|
我做的持久层,由于直接使用SQL,在 自动智能过滤缓存数据 方面,具有先天的缺陷。
因为SQL可以写的很复杂,比HQL复杂很多。而且还有各种 Native SQL特性,没有一个统一的中间语言(比如HQL), 解析起来也相当复杂。
即使有这么一个中间语言,解析处理的代价和难度也相当大。相当于实现了一个小型的HSQL级别的内存数据库。
而对于用户来说,定义DAO方法的时候,很清楚自己SQL的语义,实现智能缓存处理更容易一些,所以干脆把cache的管理交给用户自己。
这样做的另一个目的是,我想把 对应的页面缓存也放到同一个cache中去。
还有一种情况,比如,user, group, group user, 三个不同的Data Object类,由于相互关联,那么用户可以指定这三个类使用同一个cache,简化管理。
还有一种情况,比如,我想用message类,同时对应 站内短信,和论坛帖子两个对象,我也可以为这两种不同的情况,指定不同cache。
---
Hibernate如果想实现 自动智能过滤缓存数据 方面,那么具有天生的优势。
因为本来Hibernate就是要 解析HQL的,而且Hibernate管理数据类本身及其之间的关联。什么信息都有了,做起来也相对容易很多。
当然Hibernate并没有做这个工作。Hibernate只提供了一个evictQueries()方法,不分类型地清理所有的cached query.
Hibernate的QueryKey也是直接使用结果SQL,而不是HQL。
java代码: |
// from hibernate 2.7 public class QueryKey implements Serializable { private final String sqlQueryString; private final Type[] types; private final Object[] values; private final Integer firstRow; private final Integer maxRows; private final Map namedParameters; ... public boolean equals(Object other) { QueryKey that = (QueryKey) other; if ( !sqlQueryString.equals(that.sqlQueryString) ) return false; if ( !EqualsHelper.equals(firstRow, that.firstRow) || !EqualsHelper.equals(maxRows, that.maxRows) ) return false; ... return true; }
|
可以看到,hibernate query key 直接比较结果SQL。而且我们知道,这个SQL是HQL转换过来的结果,reference一定不会相等。
假设这个SQL很长的时候,而两个SQL又相同,这个比较就会比较消耗时间。而且,这个QueryKey占的空间也比较大。
(在我的持久层里面,SQL尽量采用常量字符串、或常量字符串数组,在一定程度上解决这个问题。当然,这需要用户在使用的时候,有意识的支持)
我想了一下,为什么hibernate QueryKey直接采用结果SQL,而不用HQL。
这个SQL是HQL的解析结果,直接使用结果,节省了解析时间。比如,From A where id = 1 和 from A WHERE ID = '1',两个HQL字符串不相同,但语义相同,转换出来的SQL一定相同。
如果Hibernate要加入智能管理QueryCache的功能,需要在QueryKey里面加入更多的信息(比如,HQL的解析结果的条件过滤部分),这样QueryKey的占用空间就会进一步加大。
有一个hibernate cache讨论。
http://forum.javaeye.com/viewtopic.php?t=6593
我的另一个帖子里面也有介绍。
http://forum.javaeye.com/viewtopic.php?t=9706
Hibernate主要的缓存是ID缓存(二级缓存),而QueryCache缓存的支持非常初级。
ID缓存的Key非常简单,就是persistent class + entity identifier。
具体来说,每个不同的persistent class有独立的ID缓存,该独立ID缓存的key就是 entity identifier。
ID缓存的管理是不是在 Method Interceptor里面管理的。我从hibernate代码中看不出这一点。也许在Hibernate自定义的Event中处理。
我大致猜测一下,Hibernate把updated, inserted, deleted Entities(对于用户来说,就是PO;对于Hibernate来说,就是Entity, Proxy)都保存在内部的一个Collection结构里面。在最后Session.flush()的时候,统一处理,同步更新数据库状态,和ID缓存状态。