缓存的优点在于能够加快查询的速度,在数据库查询的情景之下,在需要多次执行同一sql(sql语句相同,参数相同)的情况下,如果能将查询的结果缓存下来就不需要每次都到数据库上去真正执行查询语句,显然能加快查询速度。mybatis提供了缓存机制,分为系统缓存和开发人员自定义的缓存。
1.系统缓存
mybatis提供缓存支持,但是默认情况下只开启一级缓存,所谓的一级缓存是针对单个sqlSession(和数据的一次会话)而言的。具体来说,通过同一个sqlSession获取的同一个Mapper对象多次执行参数和SQL都一样的语句,则只有第一次执行的时候会被发送到数据库上去执行,然后将结果放到缓存中去,后面都会从mybatis的缓存中直接取结果,当然如果缓存超时还是会再去数据库执行一次。
下面的代码对一级缓存进行了测试,可以看到对于同个sqlSession获得的相同Mapper对象,多次执行同一查询结时只有第一次查询会真正走数据库;如果换个SqlSession,则又会重新走数据库进行查询。
测试代码:
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sessionFactory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"), "test");
SqlSession sqlSession = sessionFactory.openSession();
PersonDao personDao = sqlSession.getMapper(PersonDao.class);
//三次循环实际上只需要执行一次查询
for (int i = 0; i < 3; i++) {
System.out.println(personDao.queryPersonById(6));
}
//使用二级缓存时,sqlSession调用了commit才会生效
sqlSession.commit();
//换一个sqlSession,就不能再走缓存
SqlSession sqlSession1 = sessionFactory.openSession();
PersonDao personDao1 = sqlSession1.getMapper(PersonDao.class); System.out.println(personDao1.queryPersonById(6));
测试结果输出(使用log4j输出的mybatis日志):
11:37:49.543 DEBUG (JakartaCommonsLoggingImpl.java:54) - Logging initialized using 'class org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl' adapter.
11:37:49.631 DEBUG (LogFactory.java:135) - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
11:37:49.757 DEBUG (JdbcTransaction.java:136) - Opening JDBC Connection
11:37:50.026 DEBUG (JdbcTransaction.java:100) - Setting autocommit to false on JDBC Connection [387023832, URL=jdbc:mysql://127.0.0.1:3306/test, UserName=root@localhost, MySQL Connector Java]
11:37:50.041 DEBUG (BaseJdbcLogger.java:159) - ==> Preparing: select * from person where id=?
11:37:50.091 DEBUG (BaseJdbcLogger.java:159) - ==> Parameters: 6(Integer)
11:37:50.117 DEBUG (BaseJdbcLogger.java:159) - <== Total: 1
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
11:37:50.122 DEBUG (JdbcTransaction.java:136) - Opening JDBC Connection
11:37:50.130 DEBUG (JdbcTransaction.java:100) - Setting autocommit to false on JDBC Connection [1719136796, URL=jdbc:mysql://127.0.0.1:3306/test, UserName=root@localhost, MySQL Connector Java]
11:37:50.130 DEBUG (BaseJdbcLogger.java:159) - ==> Preparing: select * from person where id=?
11:37:50.131 DEBUG (BaseJdbcLogger.java:159) - ==> Parameters: 6(Integer)
11:37:50.132 DEBUG (BaseJdbcLogger.java:159) - <== Total: 1
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
一级缓存只能做到对同一个sqlSession生效,范围太窄实用性不高,如果想做到跨sqlSession,使得缓存在SqlSessionFactory层面上生效,则需要配置二级缓存。
二级缓存默认是不开启的,需要进行配置,并且开启二级缓存之后,返回的POJO对象必须是可序列化的即实现Serializable接口;具体的配置很简单,先在mybatis-config.xml配置<setting name="cacheEnabled" value="true"/>
,然后在映射文件中配置<cache/>
元素,就可以默认开启。对于上面的查询,如果开启了二级缓存之后,则最后一次查询会走二级缓存,可以看到最后一次查询命中了二级缓存(第2次 第3次是同sqlSession走的一级缓存)。输出日志如下:
12:52:22.191 DEBUG (JakartaCommonsLoggingImpl.java:54) - Logging initialized using 'class org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl' adapter.
12:52:22.279 DEBUG (LogFactory.java:135) - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
12:52:22.416 DEBUG (LoggingCache.java:62) - Cache Hit Ratio [com.sankuai.lkl.dao.PersonDao]: 0.0
12:52:22.421 DEBUG (JdbcTransaction.java:136) - Opening JDBC Connection
12:52:22.697 DEBUG (JdbcTransaction.java:100) - Setting autocommit to false on JDBC Connection [843641726, URL=jdbc:mysql://127.0.0.1:3306/test, UserName=root@localhost, MySQL Connector Java]
12:52:22.710 DEBUG (BaseJdbcLogger.java:159) - ==> Preparing: select * from person where id=?
12:52:22.756 DEBUG (BaseJdbcLogger.java:159) - ==> Parameters: 6(Integer)
12:52:22.777 DEBUG (BaseJdbcLogger.java:159) - <== Total: 1
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
12:52:22.782 DEBUG (LoggingCache.java:62) - Cache Hit Ratio [com.sankuai.lkl.dao.PersonDao]: 0.0
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
12:52:22.782 DEBUG (LoggingCache.java:62) - Cache Hit Ratio [com.sankuai.lkl.dao.PersonDao]: 0.0
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
12:52:22.795 DEBUG (LoggingCache.java:62) - Cache Hit Ratio [com.sankuai.lkl.dao.PersonDao]: 0.25
6 test3 1 swimming Thu Jan 01 12:16:35 CST 1970
映射文件中的cache元素是有很多属性可以配置的,可以配置的属性有:
eviction:缓存回收策略,目前mybatis提供的策略有:
1) LRU 最近最少使用的,移除最长时间没有使用的
2) FIFO 先进先出,按对象进入缓存的顺序移除它们
3) SOFT 软引用,移除基于垃圾回收器状态和软引用规则的对象
4) WEAK 弱引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象,这里采取的也是LRU,移除最长时间不使用的对象。
flushInterval:刷新时间间隔,单位为毫秒,如果不配置则只在sql被执行的时候才会去刷新缓存。
size:引用数目,一个正整数,设置了最多能被缓存的对象数目,如果设置过大可能会导致内存溢出。
readOnly:只读,意味着只能读取缓存,不能修改缓存,设为true可以加快读取的速度,默认值为false
下面是一个示例的配置
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
如果像最开始只设置<cache/>
不做任何配置,则意味着使用默认配置:
映射文件中的所有select语句将会被缓存
映射文件中所有insert update delete将会刷新缓存
缓存默认使用LRU(最近最少使用算法)
不会根据时间进行刷新
缓存会存储列表集合或对象的1024个引用
缓存被视为是read/write(可读/可写)的缓存,意味着对象索引不是共享的,而且可以安全的被调用者修改。
除了在映射器文件中进行全局的缓存设置,还可以在具体的sql上进行设置,主要是配置useCache和flushCache两个属性,
对于select语句可以配置useCache为true表示使用缓存,false表示不使用缓存;对于update insert delete等语句,
可以配置flushCache为true表示对缓存进行刷新或者设置为false,表示不对缓存造成影响.
2.自定义缓存
系统缓存使用的是mybatis应用机器上的本地缓存,实际上是满足不了分布式系统需求的;考虑现在有10台服务器的一个应用,如果mybatis在其中一台机器上执行了更新语句,这条更新语句影响了缓存中的某些结果,在这台服务器上执行更新语句之后刷新了缓存,所以是没问题的;但是其它九台机器上的本地缓存是没有更新的,这样打到这九台机器上的查询请求就会从缓存中拿到脏数据了。
mybatis为开发者提供了自定义缓存的机制,比如我们可以底层统一使用Redis来进行缓存,这样就可以避免上面的分布式问题。要自定义缓存就需要实现mybatis提供的org.apache.ibatis.cache.Cache接口,这个接口定义的内容如下:
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
/**
* SPI for cache providers.
*
* One instance of cache will be created for each namespace.
*
* The cache implementation must have a constructor that receives the cache id as an String parameter.
*
* MyBatis will pass the namespace as id to the constructor.
*
* <pre>
* public MyCache(final String id) {
* if (id == null) {
* throw new IllegalArgumentException("Cache instances require an ID");
* }
* this.id = id;
* initialize();
* }
* </pre>
*
* @author Clinton Begin
*/
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}
假定现在自定义了一个叫做com.sankuai.lkl.MyMybatisCache的缓存实现类,则通过下面的配置可以进行引用
<cache type="com.sankuai.lkl.MyMybatisCache"/>
还要需要注意的是mybatis的缓存是区分命名空间的,即每个映射文件都有一个独立的缓存空间,如果每个映射文件对应到了多个表,那么
缓存的更新可能不能到指定的命令空间,这时候需要通过<cache-ref/>
元素让多个表共享同一个命名空间。
mybatis缓存的粒度比较粗,没有更新缓存和缓存过期的概念,所谓的缓存刷新只是每次都删除所有的缓存。
如果想要完全禁用掉mybatis的一级缓存可以在settings属性中配置localCacheScope为Statement,默认是SESSION,即会缓存同一次会话下的所有查询。
如果想要禁掉mybatis的二级缓存,可以在settings中配置cacheEnabled为false,默认值为true。