22. transactional
事务型缓存,必须运行在 JTA 事务环境中。
在事务型缓存中,缓存的相关操作也被添加到事务之中(此时的缓存,类似一个内存数据库),
如果由于某种原因导致事务失败,我们可以连同缓冲池中的数据一同回滚到事务开始之前的状态。
事务型缓存实现了“Repeatable read”事务隔离等级,有效保障了数据的合法性,适用于对关键
数据的缓存。
注意:目前 Hibernate 内置的 Cache 中,只有 JBossCache 支持事务性的 Cache 实现。
不同的缓存实现,可支持的缓存同步策略也各不相同(表 5-2)。
表 5-2 不同缓存实现所对应的同步策略
名 称 read-only read-write nonstrict-read-write transactional
HashTable Y Y Y
EHCache Y Y Y
OSCache Y Y Y
SwarmCache Y Y
JBossCache Y Y
5.1.5 事务管理
事务管理概述
“事务”是一个逻辑工作单元,它包括一系列的操作。事务包含 4 个基本特性,也就是我们常说
的 ACID,其中包括:
1. Atomic(原子性,这里的“原子”即代表事务中的各个操作不可分割)
事务中包含的操作被看作一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失
败。
如:A 通过某网上银行系统给 B 转账,此时会执行两个数据更新操作,减少 A 的余额,增
加 B 的余额。这两个操作形成一个事务,在此事务内的这两个更新操作必须符合“要么全部成
功,要么全部失败”的事务原子性原则。单纯 A 余额的减少或者 B 余额的增加都会造成账务系
统的混乱。
2. Consistency(一致性)
一致性意味着,只有合法的数据可以被写入数据库,如果数据有任何违例(比如数据与字段
类型不符),则事务应该将其回滚到最初状态。
3. Isolation(隔离性)
事务允许多个用户对同一个数据的并发访问,而不破坏数据的正确性和完整性。同时,并行
事务的修改必须与其他并行事务的修改相互独立。
按照比较严格的隔离逻辑来讲,一个事务看到的数据要么是另外一个事务修改这些事务之前
的状态,要么是第二个事务已经修改完成的数据,但是这个事务不能看到其他事务正在修改的数
据。
针对不同的情况,事务的隔离级别要求也各有差异,下面一节中,我们将具体探讨事务隔离
33. 劣很大程度上决定了系统的整体性能。
同样,这个领域,往往也存在最大的性能调整空间。对于同样的查询结果,不同的实现机制其性
能差距可能超出大多数人的想象(出现几百倍的性能差距并不奇怪)
。
因此,在开始具体应用开发之前,了解这方面的实现原理和机制是非常必要的,而这,也是我们
下面即将讨论的主题。
Hibernate 2 中,Session 接口提供了以下方法以完成数据的批量查询功能(相对于 Session.load 的
单一数据加载而言):
public List find(…);
public Iterator iterate(…);
Hibernate 查询接口 Query、Criteria 的查询功能,其内部也正是基于这两个方法实现,因此,对
Session.find/iterate 方法的讨论,涵盖了 Hibernate 中数据批量查询的主要领域,值得引起特别的关注。
另外,值得注意的是:Hibernate3 中,上述方法已经从 Session 接口中废除,统一由 Query 接口
提供。find、iterate 分别对应于 Query.list 和 Query.iterate 方法,对应关系如表 5-4 所示。
表 5-4 Hibernate 2 与 Hibernaet 3 中方法的对应关系
Hibernate 2 Hibernate 3
Session.find() session.createQuery().list()
Session.iterate() session.createQuery().iterate()
从实现机制而言,这两个版本之间并没有什么差异。
在下面的内容中,为了保持语义一致性,我们以 Hibernate 2 作为基准版本进行描述。
find/iterate方法均可根据指定条件查询并返回符合查询条件的实体对象集。如:
//Session.find
String hql = "from TUser where age > ?";
List userList = session.find(hql,new Integer(18),Hibernate.INTEGER);
int len = userList.size();
for (int i=0;i<len;i++){
TUser user = (TUser)userList.get(i);
System.out.println("User Name:"+user.getName());
}
运行上面的代码,得到屏幕输出:
Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age,
tuser0_.version as version from t_user tuser0_ where (age>? )
User Name:Emma
User Name:Sammi
运行下面的代码:
//Session.iterate
String hql = "from TUser where age > ?";
Iterator it = session.iterate(
hql,
new Integer(18),
Hibernate.INTEGER
);
while (it.hasNext()){
TUser user = (TUser)it.next();
System.out.println("User Name:"+user.getName());
}
34. 得到屏幕输出:
Hibernate: select tuser0_.id as x0_0_ from t_user tuser0_ where (age>? )
Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_,
tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=?
Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_,
tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=?
User Name:Emma
User Name:Sammi
可以看到,Session.find/iterate方法实现了相同的功能— 根据查询条件从数据库获取符合条件
—
的记录,并返回对应的实体集。
从表象上来看,这两个方法达到了同样的目的,只是返回的集合类型不同,find方法返回List,iterate
返回Iterator。这两个方法的区别是否仅限于集合操作的方式差异?
对比上面的输出日志,相信大家都会产生一些疑惑,这两个方法调用的SQL并不一致,那么是否
其实现机制上也有所不同?
显然,find方法通过一条Select SQL实现了查询操作,而iterate方法,则执行了3次Select SQL,第
一次获取了所有符合条件的记录的id,之后,再根据各个id从库表中读取对应的记录,这是一个典型
的N+1次查询问题。
对于这里的例子,库表中有两条符合查询提交的记录,就需要执行2+1=3条Select语句。iterate方
法导致的N+1次查询相对list方法的一次查询,无疑性能较为低下。如果符合条件数据有100 000万条,
那么就要执行100000+1条Select SQL,可想是怎样的性能噩梦。
既然如此,为何Hibernate还要提供iterator方法,而不是仅仅提供高效的find方法?
这个问题与Hibernate缓存机制密切相关。
尝试运行以下代码:
String hql = "from TUser where age > ?";
List userList = session.find(hql,new Integer(18),Hibernate.INTEGER);
int len = userList.size();
for (int i=0;i<len;i++){
TUser user = (TUser)userList.get(i);
System.out.println("User Name:"+user.getName());
}
System.out.println("nStart query by iterate ……n");
Iterator it = session.iterate(hql,new Integer(18),Hibernate.INTEGER);
while (it.hasNext()){
TUser user = (TUser)it.next();
System.out.println("User Name:"+user.getName());
}
可以看到,这段代码实际上是将之前的find和iterate方法联用,根据同一条件进行查询。屏幕输出
如下:
Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age,
tuser0_.version as version from t_user tuser0_ where (age>? )
User Name:Emma
User Name:Sammi
Start query by iterate……
Hibernate: select tuser0_.id as x0_0_ from t_user tuser0_ where (age>? )
User Name:Emma
User Name:Sammi
注意“Start query by iterate…”之后的输出,这部分是由iterate方法执行所引发的操作日志。
35. 这里,Hibernate只执行了一次SQL,即从库表中取出所有满足条件的记录id。前面iterate方法运行
过程中根据id查询记录的两条SQL语句并没有执行。
这其中的差异就在于Hibernate缓存机制。
find方法将执行Select SQL从数据库中获得所有符合条件的记录并构造相应的实体对象,实体对象
构建完毕之后,就将其纳入缓存。
这样,之后iterate方法执行时,它首先执行一条Select SQL以获得所有符合查询条件的数据id,随
即,iterate方法首先在本地缓存中根据id查找对应的实体对象是否存在(类似Session.load方法),如果缓
存中已经存在对应的数据,则直接以此数据对象作为查询结果,如果没找到,再执行相应的Select语
句获得对应的库表记录(iterate方法如果执行了数据库读取操作并构建了完整的数据对象,也会将其
查询结果纳入缓存)。
find方法将读取的数据纳入缓存,为之后的iterate方法提供了现成的可用数据,于是出现了上面这
种情况。
再执行以下代码:
String hql = "from TUser where age > ?";
List userList = session.find(hql,new Integer(18),Hibernate.INTEGER);
int len = userList.size();
for (int i=0;i<len;i++){
TUser user = (TUser)userList.get(i);
System.out.println("User Name:"+user.getName());
}
System.out.println("nStart 2nd list……n");
userList = session.find(hql,new Integer(18),Hibernate.INTEGER);
len = userList.size();
for (int i=0;i<len;i++){
TUser user = (TUser)userList.get(i);
System.out.println("User Name:"+user.getName());
}
观察日志:
Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age,
tuser0_.version as version from t_user tuser0_ where (age>? )
User Name:Emma
User Name:Sammi
Start 2nd list……
Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age,
tuser0_.version as version from t_user tuser0_ where (age>? )
User Name:Emma
User Name:Sammi
两次 find 方法的重复执行并没有减少 SQL 的执行数量,这里缓存机制似乎并没有产生效果。
道理很简单,我们进行 find 数据查询时,即使缓存中已经有一些符合条件的实体对象存在,我们
也无法保证这些数据就是库表中所有符合条件的数据。假设第一次查询条件是 age>25,随即缓存中就
包括了所有 age>25 的 user 数据;第二次查询条件为 age>20,此时缓存中虽然包含了满足 age>25 的
数据,但这些并不是满足条件 age>20 的全部数据。
因此,find 方法还是需要执行一次 Select SQL 以保证查询结果的完整性(iterate 方法通过首先查
询获取所有符合条件记录的 id,以此保证查询结果的完整性)
。
因此,find 方法实际上无法利用缓存,它对缓存只写不读。而 iterate 方法则可以充分发挥缓存带