- <settings>
- <setting name="cacheEnabled" value="true"/>
- </settings>
- "1.0" encoding="UTF-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ys.mybatis.mapper.SecondCacheMapper">
-
- <cache/>
-
- <select id="getEmployeeById" resultType="com.ys.mybatis.DO.EmployeeDO" >
- select * from employee where id = #{id}
- select>
-
- mapper>
- @CacheNamespace
- public interface SecondCacheMapper {
-
- @Select("select * from employee")
- List
listAllEmployee(); -
- }
当我们一部分sql写在java文件中,一部分sql写在xml中,并且都希望开启二级缓存,则需要用到
- "1.0" encoding="UTF-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ys.mybatis.mapper.SecondCacheMapper">
-
- <cache/>
-
- <select id="getEmployeeById" resultType="com.ys.mybatis.DO.EmployeeDO" >
- select * from employee where id = #{id}
- select>
-
- mapper>
- @CacheNamespaceRef(name = "com.ys.mybatis.mapper.SecondCacheMapper")
- public interface SecondCacheMapper {
-
- @Select("select * from employee")
- List
listAllEmployee(); -
- EmployeeDO getEmployeeById(Integer id);
-
- }
type : 缓存类型。默认PerpetualCache
eviction : 清除策略。默认的清除策略是 LRU
flushInterval : 刷新间隔。属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size : 引用数目 。属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly : 只读,属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
blocking : 是否阻塞,默认false。阻塞的情况下,如果一个sqlSession获取指定cacheKey的二级缓存为null时,在其实时查询数据、填充缓存之前,如果有其他sqlSession也尝试获取该cacheKey的二级缓存,则该sqlSession将处于blocking状态,直到上一个sqlSession将缓存填充完毕
MapperBuilderAssistant#useNewCache
- public Cache useNewCache(Class<? extends Cache> typeClass,
- Class<? extends Cache> evictionClass,
- Long flushInterval,
- Integer size,
- boolean readWrite,
- boolean blocking,
- Properties props) {
- Cache cache = new CacheBuilder(currentNamespace)
- // cache默认的类型是PerpetualCache
- .implementation(valueOrDefault(typeClass, PerpetualCache.class))
- // 如果未指定装饰器,则添加一个默认的装饰器(LruCache)
- .addDecorator(valueOrDefault(evictionClass, LruCache.class))
- .clearInterval(flushInterval)
- .size(size)
- .readWrite(readWrite)
- .blocking(blocking)
- .properties(props)
- .build();
- configuration.addCache(cache);
- currentCache = cache;
- return cache;
- }
通过源码我们证实了:默认缓存类型是PerpetualCache,默认清除策略是LRU
CacheBuilder#build
- public Cache build() {
- // 如果未指定默认缓存类型,则设置默认实现为PerpetualCache
- // 如果未指定装饰器,则添加一个负责清除缓存的装饰器(LruCache)
- setDefaultImplementations();
- // 将namespace作为缓存的id,实例化默认缓存对象
- Cache cache = newBaseCacheInstance(implementation, id);
- // 设置属性
- setCacheProperties(cache);
- // issue #352, do not apply decorators to custom caches
- if (PerpetualCache.class.equals(cache.getClass())) {
- for (Class<? extends Cache> decorator : decorators) {
- // 循环实例化装饰器Cache,并装饰当前cache
- cache = newCacheDecoratorInstance(decorator, cache);
- setCacheProperties(cache);
- }
- // 添加系统默认的装饰器
- // ScheduledCache : 如果设置了flushInterval,则添加该装饰器
- // SerializedCache : 默认添加
- // LoggingCache : 默认添加
- // SynchronizedCache : 默认添加
- // BlockingCache : 如果设置了blocking,则添加该装饰器
- cache = setStandardDecorators(cache);
- // 如果cache的最外层的装饰器不是LoggingCache,则cache的最外层再套一个LoggingCache
- } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
- cache = new LoggingCache(cache);
- }
- return cache;
- }
由上述源码,我们得出默认情况下的Cache构造,如下图

可配置的装饰器

各装饰器的作用
相关源码 : CachingExecutor#query


PS : cacheKey相同才有命中缓存的可能,这个cacheKey的组成存在environment,表明不同数据源,二级缓存不会命中

和我们上述分析一致,默认配置下的二级缓存是五级的
XMLStatementBuilder#parseStatementNode

TransactionalCache管理器。其getObject()、putObject()、clear()方法都会调用getTransactionalCache方法将原始的cache再套一个装饰器(TransactionalCache),然后再进行管理
开启二级缓存后,并不是所有的查询结果立刻放入二级缓存,而是将其放入暂存区,等执行commit方法后,才会将暂存区的数据put到二级缓存中。TransactionalCache结构如下:
- public class TransactionalCache implements Cache {
-
- private static final Log log = LogFactory.getLog(TransactionalCache.class);
-
- private final Cache delegate;
- private boolean clearOnCommit;
- private final Map<Object, Object> entriesToAddOnCommit;
- private final Set<Object> entriesMissedInCache;
-
- }

- private void flushCacheIfRequired(MappedStatement ms) {
- Cache cache = ms.getCache();
- if (cache != null && ms.isFlushCacheRequired()) {
- tcm.clear(cache);
- }
- }
-
- public void clear(Cache cache) {
- getTransactionalCache(cache).clear();
- }
-
- private TransactionalCache getTransactionalCache(Cache cache) {
- return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
- }
-
- @Override
- public void clear() {
- clearOnCommit = true;
- entriesToAddOnCommit.clear();
- }
通过上述的方法调用,我们可以得出以下两点 :
只有useCache为true的查询,才能可能使用到二级缓存,并且增删改不可以设置其值为true。
- public Object getObject(Cache cache, CacheKey key) {
- return getTransactionalCache(cache).getObject(key);
- }
-
- private TransactionalCache getTransactionalCache(Cache cache) {
- return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
- }
-
- @Override
- public Object getObject(Object key) {
- // issue #116
- Object object = delegate.getObject(key);
- if (object == null) {
- entriesMissedInCache.add(key);
- }
- // issue #146
- if (clearOnCommit) {
- return null;
- } else {
- return object;
- }
- }
cache的getObject()方法也会将原本的cache再套一层装饰器(TransactionalCache),如果之前执行过 flushCacheIfRequired 方法(该方法会将 clearOnCommit 设置为true)则会返回 null,即使命中二级缓存(防止脏数据)
根据我们对CachingExecutor的query方法进行分析,我们总结一下可以命中二级缓存的条件 :
创建mybatis配置文件mybatis-config.xml
- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE configuration
- PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-config.dtd">
- <configuration>
-
- <properties>
- <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
- <property name="url" value="jdbc:mysql://127.0.0.1/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"/>
- <property name="username" value="root"/>
- <property name="password" value="123456"/>
- </properties>
-
- <settings>
- <setting name="cacheEnabled" value="true"/>
- <setting name="mapUnderscoreToCamelCase" value="true"/>
- </settings>
-
- <environments default="default">
- <environment id="default">
- <transactionManager type="JDBC"/>
- <dataSource type="POOLED">
- <property name="driver" value="${driver}"/>
- <property name="url" value="${url}"/>
- <property name="username" value="${username}"/>
- <property name="password" value="${password}"/>
- </dataSource>
- </environment>
- </environments>
-
- <mappers>
- <mapper resource="mapper/SecondCacheMapper.xml" />
- </mappers>
-
- </configuration>
创建SecondCacheMapper.xml
- "1.0" encoding="UTF-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ys.mybatis.mapper.SecondCacheMapper">
-
- <cache />
-
- <select id="getEmployeeById" resultType="com.ys.mybatis.DO.EmployeeDO" >
- select * from employee where id = #{id}
- select>
-
- mapper>
创建SecondCacheMapper
- @CacheNamespaceRef(name = "com.ys.mybatis.mapper.SecondCacheMapper")
- public interface SecondCacheMapper {
-
- @Select("select * from employee")
- List
listAllEmployee(); -
- EmployeeDO getEmployeeById(Integer id);
-
- }
创建测试类SecondCacheTest
- @Slf4j
- public class SecondCacheTest {
- private SqlSessionFactory sqlSessionFactory;
-
- @BeforeEach
- public void before() {
- InputStream inputStream = ConfigurationTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
- sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- }
-
- @Test
- public void hitL2Cache() {
- SqlSession firstSession = sqlSessionFactory.openSession();
- SecondCacheMapper firstMapper = firstSession.getMapper(SecondCacheMapper.class);
- EmployeeDO firstResult = firstMapper.getEmployeeById(1);
-
- firstSession.commit();
-
- SqlSession secondSession = sqlSessionFactory.openSession();
- SecondCacheMapper secondMapper = secondSession.getMapper(SecondCacheMapper.class);
- EmployeeDO secondResult = secondMapper.getEmployeeById(1);
-
- System.out.println(firstResult == secondResult);
- }
-
- }
执行测试方法

LoggingCache打印了命中率 : 0.5
但是我们发现,第一次查询的结果和第二次查询的结果并不相等,那是因为
我们将
<cache readOnly="true" />

二级缓存存在多个装饰器,每个装饰器的属性可能存在默认值,那如何修改这些默认值,我们来一起探究一下。假如我们将清除策略修改为FIFO,size设置为1(默认1024)
- <cache readOnly="true" eviction="FIFO">
- <property name="size" value="1"/>
- </cache>
- @Test
- public void testCacheSize() {
- SqlSession firstSession = sqlSessionFactory.openSession();
- SecondCacheMapper firstMapper = firstSession.getMapper(SecondCacheMapper.class);
- EmployeeDO firstResult = firstMapper.getEmployeeById(1);
- // 填充第一个缓存
- firstSession.commit();
-
- SqlSession secondSession = sqlSessionFactory.openSession();
- SecondCacheMapper secondMapper = secondSession.getMapper(SecondCacheMapper.class);
- EmployeeDO secondResult = secondMapper.getEmployeeById(1);
- System.out.println(firstResult == secondResult);
-
- SqlSession thirdSession = sqlSessionFactory.openSession();
- SecondCacheMapper thirdMapper = thirdSession.getMapper(SecondCacheMapper.class);
- thirdMapper.getEmployeeById(2);
- // 填充第二个缓存,覆盖第一个缓存
- thirdSession.commit();
-
- SqlSession fourthSession = sqlSessionFactory.openSession();
- SecondCacheMapper fourthMapper = fourthSession.getMapper(SecondCacheMapper.class);
- EmployeeDO fourthResult = fourthMapper.getEmployeeById(1);
- // 看看size是否为1 (需不需要查询数据库)
- System.out.println(firstResult == fourthResult);
- }

通过日志我们可以发现,默认属性覆盖成功
如果系统允许一定时间内的数据不一致问题,我们才有可能使用二级缓存,如果是高实时性的业务,最好不要使用二级缓存。除了数据一致性问题,我们再来讨论一下二级缓存可能存在的其他问题:
默认情况下,存储数据的是PerpetualCache,该Cache是把数据存储在内存中,给内存带来了一定的挑战
考虑到数据安全的问题,mybatis中存在BlockingCache、SynchronizedCache。如果在更新缓存的时候,出现高并发问题,阻塞、同步方式的请求可能会拖垮服务器
默认情况下,我们只要执行了增删改操作,该命名空间下的所有二级缓存都将被清空。有可能我们刚填充完二级缓存,下一步就会执行增删改操作,将该命名空间下二级缓存都清空了