N+1问题
所谓N+1问题,使用hibernate查询数据,首先返回数据的id信息,并没有返回所有的对象信息,只有在真正使用的时候,在使用这个id对数据库中查询数据,一次查询多次调用数据库(缓存)的情况,就是所谓的N+1问题。以下方法或者策略会出现N+1问题:
使用iterate()方法
存在iterator的原因是,有可能会在一个session中查询两次数据,如果使用list每一次都会把所有的对象查询上来,如果使用iterator仅仅只会查询id,此时所有的对象已经存储在一级缓存(session的缓存)中,可以直接获取
1 | /** |
使用查询缓存策略
一级缓存,session级别缓存
首次查询数据,会查询数据库返回数据,并且保存到缓存中;再次查询该数据时,直接从缓存中获取(同一个session)。
由于一级缓存是session级别的缓存,只有在同一个session中才能起到再次查询从缓存中获取数据。
比如:list()放入缓存,load查询从缓存中获取数据。
由于Session对象的生命周期通常对应一个数据库事务或者一个应用事务,因此它的缓存是事务范围的缓存。
Session级缓存是必需的,不允许而且事实上也无法卸除。在Session级缓存中,持久化类的每个实例都具有唯一的ID
调用session的方法会加入一级缓存:save()、update()、savaeOrUpdate()、get()或load();调用查询接口的list()、iterate()或filter()方法
二级缓存(sessionFactory级别)
下载ehcache相关包
在hibernate.cfg.xml配置文件中配置我们二级缓存的一些属性
1 | <!-- 开启二级缓存 --> |
配置ehcache.xml
1 | <ehcache> |
开启缓存
- ①如果使用xml配置,我们需要在 *.hbm.xml 中加上一下配置
<cache usage="read-only"/>
②如果使用annotation配置,我们需要在类上加上这样一个注解@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)
缓存策略
read-only、nonstrict-read-write、read-write、transactional
二级缓存触发的方法
save、update、saveOrupdate、load、get、list、query、Criteria方法都会填充二级缓存
get、load、iterate会从二级缓存中取数据
session.save(user)
如果user主键使用“native”生成,则不放入二级缓存
执行顺序
- 条件查询的时候,总是发出一条
select * from table_name where
…. (选择所有字段)这样的SQL语句查询数据库,一次获得所有的数据对象。 - 把获得的所有数据对象根据ID放入到第二级缓存中。
- 当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存。
- 删除、更新、增加数据的时候,同时更新缓存。
其他
二级缓存缓存的仅仅是对象,如果查询出来的是对象的一些属性,则不会被加到缓存中去
Hibernate的二级缓存策略,是针对于ID查询的缓存策略,对于条件查询则毫无作用。为此,Hibernate提供了针对条件查询的查询缓存(Query Cache)。
解决N+1问题
当我们如果需要查询出两次对象的时候,可以使用二级缓存来解决N+1的问题
先list在iterator
查询缓存(sessionFactory级别)
开启缓存
hibernate.cfg.xml
1 | <!-- 开启查询缓存 --> |
查询中需要调用方法
.setCacheable(true) //开启查询缓存,查询缓存也是SessionFactory级别的缓存
如果使用注解方式,需要在类上加入在类上加注解:@Cacheable
其他
- 只有当 HQL 查询语句完全相同时,连参数设置都要相同,此时查询缓存才有效
- 查询缓存也能引起 N+1 的问题,需要开启二级缓存。
- 查询普通属性,会先到查询缓存中取,如果没有,则查询数据库;
- 查询实体,会先到查询缓存中取id,如果有,则根据id到缓存(一级/二级)中取实体,如果缓存中取不到实体,再查询数据库。
FlushMode与session.flush()
在Hibernate中,使用session来操作数据库,session中的存在缓存(一级缓存),当调用session.save或者session.update()等方法的时候,hibernate并不一定会将修改同步到数据库(要看具体的FlushMode),而是先将这些数据存储在session的缓存中,由hibernate自己决定何时同步刷新到数据中。正是由于hibernate的这种缓存机制,在同一个session中多次修改一个记录,最终只会向数据库发出一条update语句。由于session缓存以及脏数据检查机制,能够帮助我们尽可能少地发出SQL语句。
hibernate提供了FlushMode接口,能够让我们干预hibernate将脏数据同步到数据库的时机。Session.flush()会触发hibernate将数据同步到数据库。可以通过session.setFlushMode()来修改刷新模式。FlushMode提供了4种缓存模式:MANUAL、COMMIT、AUTO和ALWAYS。源码如下:
1 | /** |
MANUAL
我们必须在代码中**手动调用session.flush()**,hibernate才会将脏数据同步到数据库。如果我们忘记了手动刷新,那么就算是通过session.getTransaction().commit()提交了事务,也不能将修改同步到数据库。
COMMIT
当数据库事务提交的时候会刷新缓存,当然手动调用flush()肯定也是可以的,不过没有必要罢了。
AUTO(默认)
事务提交或者手动刷新,都能将脏数据同步到数据库。除此之外,某些查询出现的时候也会导致缓存刷新。
缺点
- you don’t control when Hibernate will decide to execute UPDATE/INSERT/DELETE.
- potential performance issues because every object modification may lead to dirty checking + DML statement execution.
- you are not taking advantage of batching and other optimizations that Hibernate can perform when it is not trying to avoid ‘stale’ state
ALWAYS
只要有查询出现,或者事务提交,或者手动刷新,都会导致缓存刷新。这个策略性能比较差,实际中不会使用。
总结
默认hibernate不会开启查询缓存,这是因为查询缓存只有在hql/hql语句语义完全一致的时候,才能命中。而实际查询场景下,查询条件、分页、排序等构成的复杂查询sql语句很难完全一致。可能是hibernate觉得命中率低,所以默认关闭了查询缓存。我们可以根据实际使用情况,决定是否开启查询缓存,唯一的原则就是命中率要尽可能的高。如果针对A表的查询,查询sql语句基本都是完全一致的情况,就可以针对A使用查询缓存;如果B表的查询条件经常变化,很难命中,那么就不要对B表使用查询缓存。这可能就是hibernate使用查询缓存的时候,既要在hibernate.cfg.xml中进行配置,也需要query.setCacheable(true)的原因。查询缓存只对list有用,对iterate方式无用。iterate不会读也不会写查询缓存,list会读也会写查询缓存。查询缓存中的key是sql语句(这些sql语句会被hibernate解析,保证语义相同的sql,能够命中查询缓存),缓存的value是记录的主键值。
通过开启查询缓存和二级缓存,相同的sql查询可以直接使用查询缓存中的id和二级缓存中的实体对象,可以有效的降低反复的数据库查询,可以提高查询效率。也就是说:同一时候开启查询缓存和二级缓存是有意义的。也是实际使用hibernate的最佳配置。进一步的。我们也能够看出list和iterate方法的差别。list()会将实体对象的id放入查询缓存,将实体对象本身放入二级缓存。iterate不会将实体对象的id放入查询缓存。可是会将实体对象本身存入二级缓存。假设第二次查询可以命中的情况下:list全然不须要查询数据库,可以先从查询缓存中获取到id。再从二级缓存中获取实体对象。iterate一定会发出一条查id的sql,然后去二级缓存中获取实体对象。
缓存策略提供商
org.hibernate.cache.HashtableCacheProvider(内存)
org.hibernate.cache.EhCacheProvider(内存,硬盘)
org.hibernate.cache.OSCacheProvider(内存,硬盘)
org.hibernate.cache.SwarmCacheProvider(能用于集群环境)
org.hibernate.cache.TreeCacheProvider(能用于集群环境)
org.hibernate.cache.jbc.JBossCacheRegionFactory(能用于集群环境)