1. 生命周期
Hibernate有三种状态,可以互相发生转换。
瞬时对象(transientObjects)就是刚刚new出来的对象,数据库中没有记录,也没有被Hibernate的Session管理起来,这种对象是会被GC回收的。[数据库里没有,session里没有]
持久对象(persistentObjects)经历了持久化操作的对象,数据库有记录,被Hibernate的Session管理起来。[数据库里有,session里也有]
托管对象(detachObject),数据库有记录,但没被Hibernate的Session管理起来。[数据库里有,但是session里没有]
对象转换图
台词:hibernate的生命周期有三种状态,临时、持久和托管。所谓临时状态就是刚刚new出来的对象的状态,临时对象并未存在于数据库也没有被session管理,此时这个对象是有可能被GC回收的,如果临时对象经历了持久化操作,如save()或saveOrUpdate()等,那么它将进入持久状态,持久对象会出现在数据库中,且被session管理起来,当然如果一开始就是查询业务,如get()、load()和iterator()等操作,那么对象将直接进入持久状态。持久对象通过delete()方法可以退回到临时状态,也可以通过close()、clear()或evict()等操作进入托管状态,托管对象在数据库中仍是存在的,但不会再被session管理了,除非再次进行持久化操作,如update()或saveOrUpdate()方法等,那么它将退回到持久状态,或者调用delete()方法退回到临时状态,否则,托管对象最终也会被GC回收。
瞬时 - 持久
tips:如果调用session.delete(userPojo),则userPojo回退为瞬时对象。
持久 - 托管
如果session关闭,或者调用clear方法,或者调用evict方法,那么对象就从持久状态,变成了托管状态,该对象在数据库中仍然存在,但是不再由session管理。
如果继续去调用session.update(userPojo),则再次利用session接管这个对象,对象从托管状态重新变成持久状态,当然前提是session没有关闭。
如果调用session.delete(userPojo),则userPojo回退为瞬时对象。
2. 并发问题
案例:如果用户A查询到某商店的IPHONE剩余100台,然后A要买两个,同时,用户B也查询到库存100台IPHONE,B要买100个,这样就有可能产生并发问题。
2.1 悲观锁
悲观锁是数据库的update锁机制[select * from emp for update],不是hibernate锁。悲观锁是在查询上上锁,可以保证A在查询数据的时候,B被阻塞,无法进行查询。
测试:这里无法测试多线程效果,我们可以通过SQL语句看出来,悲观锁已经生效。
@Test
public void retrieveByUpGrade() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
Emp emp = (Emp)session.load(Emp.class, 1,LockOptions.UPGRADE);
System.out.println(emp);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
session.close();
}
}
tips:在我们load方法或者get方法上,具有锁的参数,我们选择LockOptions.UPGRADE之后,在Load数据的时候,就不会有其它线程进来捣乱了,我们也可以通过Hibernate发送的SQL来观察到,SQL是for
update的。
悲观锁缺陷:悲观锁锁的范围实在是太大了,将整个查询过程都锁住了,很浪费性能,而乐观锁就完美的解决了这种性能问题。
2.2 乐观锁
乐观锁的原理就是在数据库表中添加一个版本字段version,初始为0。读取出数据时,将此版本号version一同读出,在提交修改操作之前,再查询一遍数据库中的version,如果和第一次查询到的一致,则予以更新,并将version加1,否则认为是过期数据,由程序控制,重新发送请求。
例:
- A进入数据库,读取一条数据,得到version为0,拿在手里。
- B进入数据库,读取相同数据,得到version为0,拿在手里。
- A修改该条数据,修改完提交前又查询一遍本条数据的version仍为0,版本一致,允许修改,version+1,提交。
- B修改该条数据,修改完提交前又查询一遍本条数据的version已经改为1,此时B手里的数据为过期数据,不允许B进行操作。
- B获取最新version为1,然后由程序控制重新发送请求,但重新发送请求之前还要再查询version,发现也为1,版本一致,允许修改,version再+1,提交。
乐观锁使用:在实体类中添加一个字段version,必须是private,以免外部调用,否则乐观锁失效。测试的时候,无需setVersion字段,hibernate会自动帮我们进行version的值的注入。
Emp.java中添加version字段,并设置set/get
private Integer version;
Emp.hbm.xml文件中添加version标签
<!--version要写在所有id和property之间-->
<version name="version"></version>
测试:这里无法测试多线程效果,我们可以通过SQL语句看出来,乐观锁已经生效。
@Test
public void retrieveByVersion() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
Emp emp = (Emp)session.load(Emp.class, 1);
emp.setRealName("赵四");
session.update(emp);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
session.close();
}
}
tips:语句的条件中自动添加了version=?,说明在修改数据的时候,是需要根据version来判断的,即乐观锁生效。
3. Flush操作
flush是session中的一个方法,主要做了两件事,清理缓存(session临时区)以及执行SQL。flush操作在事物提交时或者执行查询之前,都会默认执行,也可以显示调用session.flush()来强制清理缓存。
session被创建后有临时区elementData和持久区tail,如下图红标。
3.1 测试:Flush-UUID
当主键生成机制设定为UUID、并且事物隔离级别为ORACLE默认read committed时,我们debug三个连续操作:session.save()、session.flush()、transaction.commit()。
try{
session.save(emp);
session.flush();
session.commit();
}
运行流程记录,如下:
当save执行前 | |
---|---|
session临时区 | null |
session持久区 | null |
dirty | false |
控制台SQL | 未出现 |
当save执行后 | |
---|---|
session临时区 | 有值 |
session持久区 | 有值 |
dirty | true |
控制台SQL | 未出现 |
当flush执行后 | |
---|---|
session临时区 | null |
session持久区 | 有值 |
dirty | false |
控制台SQL | 出现 |
当commit执行后 | |
---|---|
session临时区 | null |
session持久区 | 有值 |
dirty | false |
控制台SQL | - |
tips:save操作会将pojo交给session去管理,session的临时区和持久区各执一份,此时的数据是脏数据,因为是不确定的。
tips:在主键机制是uuid的情况下,save不执行SQL。
tips:flush或锁定持久区的数据,此时其他事物无法读取,flush执行SQL。
3.2 测试:Flush-Native
如果主键生成机制从UUID改为Native,则测试结果不同。
当save执行前 | |
---|---|
session临时区 | null |
session持久区 | null |
dirty | false |
控制台SQL | 未出现 |
当save执行后 | |
---|---|
session临时区 | 有值 |
session持久区 | 有值 |
dirty | true |
控制台SQL | 出现 |
当flush执行后 | |
---|---|
session临时区 | null |
session持久区 | 有值 |
dirty | false |
控制台SQL | - |
当commit执行后 | |
---|---|
session临时区 | null |
session持久区 | 有值 |
dirty | false |
控制台SQL | - |
tips:native是hibernate帮我们进行自增,所以它直接就可以确定SQL内容。
3.3 批处理Flush
如果循环10万零8次save()操作,对session的临时区来说是一个不小的负载,很容易造成内存溢出,所以我们设计,每100次save()操作,flush一次session。
for(int i=0 ; i<10008 ; i++){
UserPojo userPojo = new UserPojo();
userPojo.setUsername("zhangsan");
userPojo.setAge(50);
session.save(userPojo);// 添加数据
if(i%100==0) session.flush();// 每100条flush一次
}
session.flush();// flush最后那8条
transaction.commit();// 提交事物
4. 缓存
一条查询请求发出,会先去【一级缓存】中查询,如果有,直接返回数据,如果没有,再去【二级缓存】中查询(二级缓存开启的前提下),如果有,直接返回数据,如果还是没有,再去【数据库】中查询,查询到数据之后,再将这份数据备份到一级缓存和二级缓存中,最后返回数据。
所以说,缓存可以提高效率,但是缓存只能缓存实体对象,不能缓存其他属性。
4.1 一级缓存
一级缓存就是session级别的缓存,如果session关闭了,那么一级缓存就被清空,即一级缓存不能跨session存在。
我们在同一个session中做两遍相同的事情,会发现第二次的操作不发SQL,而是从一级缓存中获取。
分别测试get/load/list方法是否都使用一级缓存机制
@Test
public void firstLevelCacheRetrieveTest() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
// 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
Emp emp1 = (Emp) session.get(Emp.class, 1);
// 不发SQL,从一级缓存中取值
Emp emp2 = (Emp) session.get(Emp.class, 1);
// 不发SQL,从一级缓存中取值
Emp emp3 = (Emp) session.load(Emp.class, 1);
System.out.println(emp1);
System.out.println(emp2);
System.out.println(emp3);
Query query = session.createQuery("from Emp");
// 发SQL,从数据库中取值
List<Emp> emps1 = query.list();
// 发SQL,从数据库中取值
List<Emp> emps2 = query.list();
System.out.println(emps1);
System.out.println(emps2);
session.flush();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
session.close();
}
}
tips:get和load都使用一级缓存,但是list不使用。
4.1.1 一级缓存同步
在修改持久数据之后,缓存会自动同步。
查询一次数据,然后利用set修改这个数据,再查询一遍相同的数据,发SQL吗?数据会同步更改么?
@Test
public void firstLevelCacheDmlTest() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
// 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
Emp emp1 = (Emp) session.get(Emp.class, 1);
// 对持久化对象的修改会直接影响数据库数据和一级缓存
emp1.setRealName("奥特曼");
// 不发SQL,使用一级缓存取出的值是"奥特曼",说明一级缓存同步更新了
Emp emp2 = (Emp) session.get(Emp.class, 1);
System.out.println(emp2);
session.flush();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
session.close();
}
}
4.1.2 一级缓存移除
我们可以使用session.evict()方法来逐出某个对象,或者直接使用session.clear()来清空一级缓存。
@Test
public void firstLevelCacheEvictAndClearTest() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
// 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
Emp emp1 = (Emp) session.get(Emp.class, 1);
// 从一级缓存中逐出emp1对应的的结果
session.evict(emp1);
// 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
Emp emp2 = (Emp) session.get(Emp.class, 1);
// 清空一级缓存
session.session(emp1);
// 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
Emp emp3 = (Emp) session.get(Emp.class, 1);
System.out.println(emp1);
System.out.println(emp2);
System.out.println(emp3);
session.flush();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
session.close();
}
}
4.2 二级缓存EHcache
二级缓存是sessionFactory级别的,能被所有session共享(可以跨session),如果sessionFactory关闭,那么二级缓存随之消失。
哪些数据适合使用二级缓存?
- 很少被修改的数据
- 频繁被查询的数据
- 允许偶尔并发的数据
- 不是特别重要的数据
Hibernate自己有二级缓存的技术,但是没有第三方二级缓存技术EHcache好,EHcache支持在内存和硬盘上做缓存,支持查询缓存,而且EHcache缓存策略由自己决定,我们只需要配置即可。
在hibernate安装包/project/etc包下,找到ehcache.xml文件,拷贝在工程的classpath下,在这里可以修改二级缓存的一些配置。
Maven依赖
<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache-core -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-ehcache -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>4.3.11.Final</version>
</dependency>
4.2.1 开启EHcache
在hibernate-cache.cfg.xml文件中手动开启二级缓存,并指定使用ehcache作为项目的二级缓存技术
<!-- 设置hibernate开启二级缓存,默认为false -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- 指定使用EHcache作为hibernate的二级缓存机制 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
在对应Emp.hbm.xml文件中开启EHcache,代表对这个类使用二级缓存
<cache usage="read-only"/>
tips:在的首位添加标签。
对get()进行跨session测试:注意,此时一定不要跨sessionFactory
@Test
public void secondLevelClearRetrieveTest() {
Session session1 = sessionFactory.openSession();
Emp emp1 = (Emp) session1.get(Emp.class, 1);
System.out.println(emp1);
session1.flush();
session1.close();
Session session2 = sessionFactory.openSession();
Emp emp2 = (Emp) session2.get(Emp.class, 1);
System.out.println(emp2);
session2.flush();
session2.close();
}
tips:自主测试load/iterator和list方法是否都支持二级缓存?
4.2.2 二级缓存策略
二级缓存的策略由中的usage属性来控制,常用值有两个:
1.read-only[只读策略:只能读取缓存中的数据,不能修改缓存中的数据]
2.read-write[读写策略:可以修改缓存中的数据,而且一旦修改数据库数据,那么二级缓存中的对应数据就会同步更新,如果再次查询该数据,不会发送SQL查询]
测试:read-only策略下,修改二级缓存中的数据会报错吗?
@Test
public void secondLevelReadOnlyTest() {
Session session1 = sessionFactory.openSession();
// 第一遍查询:从数据库中获取结果,并备份到一二级缓存中,发SQL
Emp emp1 = (Emp) session1.get(Emp.class, 1);
System.out.println(emp1);
session1.flush();
session1.close();
// 第二遍查询:从二级缓存中获取结果,不发SQL
Session session2 = sessionFactory.openSession();
Transaction transaction = session2.beginTransaction();
Emp emp2 = (Emp) session2.get(Emp.class, 1);
System.out.println(emp2);
// 将名字修改并提交,但因为二级缓存策略是read-only,数据库和二级缓存都无法被更改
try {
emp2.setRealName("谢兰");
session2.update(emp2);
//
transaction.commit();
} catch (HibernateException e) {
e.printStackTrace();
System.out.println("回滚了");
transaction.rollback();
} finally {
session2.flush();
session2.close();
}
}
tips:如果将二级缓存策略改为"read-write",则不再报错,二级缓存和数据库中的数据会同时被更改。
4.2.3 二级缓存逐出
二级缓存也可以手动逐出,不过实际开发中不常用。
@Test
public void secondLevelEvictTest() {
Session session1 = sessionFactory.openSession();
// 第一遍查询:从数据库中获取结果,并备份到一二级缓存中,发SQL
Emp emp1 = (Emp) session1.get(Emp.class, 1);
System.out.println(emp1);
session1.flush();
session1.close();
// 在二级缓存中逐出EHCache
sessionFactory.getCache().evictEntity(Emp.class,1);
// 第二遍查询:无法从二级缓存中获取结果,发SQL
Session session2 = sessionFactory.openSession();
Emp emp2 = (Emp) session2.get(Emp.class, 1);
System.out.println(emp2);
session1.flush();
session1.close();
}
4.3 查询缓存
查询缓存可以缓存属性,也可以缓存实体对象的ID,一旦查询的数据库的表的数据发生改变,缓存就被清空。
查询缓存也需要在cfg文件中设置开启
<property name="hibernate.cache.use_query_cache">true</property>
测试:缓存Lang类型属性
@Test
public void secondLevelEvictTest() {
Session session = sessionFactory.openSession();
Query query1 = session.createQuery("select count(*) from Emp");
query1.setCacheable(true);// 开启查询缓存
Long num1 = (Long) query1.list().get(0);
System.out.println(num1);
Query query2 = session.createQuery("select count(*) from Emp");
query2.setCacheable(true);// 开启查询缓存
Long num2 = (Long) query2.list().get(0);
System.out.println(num2);
session.close();
}
tips:list方法支持查询缓存,而iterator方法不支持查询缓存。