缓存是指内存中的数据,常常来自于对数据库查询结果的缓存,为什么需要缓存?因为频繁的直接对数据库操作是很消耗系统资源的。我们将频繁需要获取的数据进行缓存,可以避免频繁的数据库交互,可以提高系统的响应速度。
mybatis对缓存的支持有一级缓存和二级缓存:

  • 一级缓存是SqlSession级别,也就是在操作数据库的时候要构造SqlSeesion对象,对象中有一个数据结构(HashMap)用于存储数据,不同的SqlSession之间的缓存区域是互不影响的。
  • 二级缓存为mapper级别缓存,多个SqlSession去操作同一个Mapper的sql语句,这个时候多个SqlSession是共享二级缓存的,二级缓存是跨SqlSession的。

先附上代码链接:https://gitee.com/wzlove521/lagou_study/tree/master/mybatis/mybatis_multitable

一级缓存

一级缓存的测试以及缓存失效,首先要确认一级缓存的存在,我们先配置mybatis的打印sql,之后进行查看结果会更清晰明了。在config的配置文件中添加

  1. <settings>
  2. <setting name="logImpl" value="STDOUT_LOGGING"/>
  3. </settings>

编写测试类进行测试:

  1. public class CacheTest {
  2. private UserMapper userMapper;
  3. private SqlSession sqlSession;
  4. @Before
  5. public void createUserMapper() throws IOException {
  6. InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapperConfig.xml");
  7. sqlSession = new SqlSessionFactoryBuilder().build(resourceAsStream).openSession();
  8. userMapper = sqlSession.getMapper(UserMapper.class);
  9. }
  10. @Test
  11. public void testFirstLevelCache() {
  12. User user = userMapper.selectById(2);
  13. User user1 = userMapper.selectById(2);
  14. System.out.println(user == user1);
  15. }
  16. }

debug测试可以看到在 testFirstLevelCache 中,执行第一个查询的时候会打开连接,然后查询数据库;第二次查询的时候是直接返回结果的,并且最后的输出为true。
缓存的生成就是这么一个过程,第一次查询,会去一级缓存中查询有没有缓存,没有去数据库查询,将查询结果进行缓存;如果存在缓存,则直接返回。

缓存失效

缓存失效或者说手动清除缓存,手动清除缓存直接使用 sqlSession.cleanCache(); 手动清除缓存。那什么情况下会自动清除缓存呢,那就是出现了增删改操作并进行了事务提交。如下

  1. @Test
  2. public void testFirstLevelCacheClean() {
  3. User user = userMapper.selectById(2);
  4. // sqlSession.clearCache();
  5. User user2 = new User();
  6. user.setId(1);
  7. user.setName("王智");
  8. userMapper.updateUser(user2);
  9. sqlSession.commit();
  10. User user1 = userMapper.selectById(2);
  11. System.out.println(user == user1);
  12. }

可以看到只要有增删改操作就会清除一级缓存,无论你的操作是否影响了当前缓存的数据,只要发生,就会清除,这样做的目的是避免脏读。
额外说一点,SqlSession中保存一级缓存使用的数据结构是HashMap,存在的键值对的值肯定是查询的结果,那键呢?键的组成比较复杂,包括下面几个部分:

  • statementId:唯一标识:namespace + id
  • params:参数
  • boundSql:封装了当前执行的sql
  • rowBounds:分页对象

源码分析

一级缓存是基于SqlSession的,所以源码的分析也从SqlSession开始。
这就是源码查找的整个流程图,下面展示一下比较重要大代码:

  1. // 首先SqlSeesion的接口就不用看了,从实现类的代码看起
  2. public class DefaultSqlSession implements SqlSession {
  3. private final Executor executor;
  4. @Override
  5. public void clearCache() {
  6. executor.clearLocalCache();
  7. }
  8. }
  9. // Executor是执行器,实现类看BaseExecutor,该类实现了clearLocalCache方法
  10. public abstract class BaseExecutor implements Executor {
  11. protected PerpetualCache localCache;
  12. @Override
  13. public void clearLocalCache() {
  14. if (!closed) {
  15. localCache.clear();
  16. localOutputParameterCache.clear();
  17. }
  18. }
  19. }
  20. // PerpetualCache就是最后一层了,可以看到具体的数据结构
  21. public class PerpetualCache implements Cache {
  22. private final String id;
  23. private final Map<Object, Object> cache = new HashMap<>();
  24. @Override
  25. public void clear() {
  26. // 执行的就是HashMap的清空方法
  27. cache.clear();
  28. }
  29. }

根据clean方法找到了整个线路,那具体的缓存的实现在哪一步呢?我们在执行查询的时候才会放缓存,所以缓存的实现肯定是放在执行器的步骤,接下来肯定看执行器的查询方法:

  1. // 查看query方法
  2. public abstract class BaseExecutor implements Executor {
  3. @Override
  4. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  5. BoundSql boundSql = ms.getBoundSql(parameter);
  6. CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  7. return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  8. }
  9. }
  10. // 看到生成了CacheKey,接下来看看CacheKey是如何生成的,包含了哪几个部分
  11. @Override
  12. public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  13. if (closed) {
  14. throw new ExecutorException("Executor was closed.");
  15. }
  16. CacheKey cacheKey = new CacheKey();
  17. // statementId = namespace.id
  18. cacheKey.update(ms.getId());
  19. // 分页数据
  20. cacheKey.update(rowBounds.getOffset());
  21. cacheKey.update(rowBounds.getLimit());
  22. // 执行的sql
  23. cacheKey.update(boundSql.getSql());
  24. // 存放参数
  25. List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  26. TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  27. // mimic DefaultParameterHandler logic
  28. for (ParameterMapping parameterMapping : parameterMappings) {
  29. if (parameterMapping.getMode() != ParameterMode.OUT) {
  30. Object value;
  31. String propertyName = parameterMapping.getProperty();
  32. if (boundSql.hasAdditionalParameter(propertyName)) {
  33. value = boundSql.getAdditionalParameter(propertyName);
  34. } else if (parameterObject == null) {
  35. value = null;
  36. } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
  37. value = parameterObject;
  38. } else {
  39. MetaObject metaObject = configuration.newMetaObject(parameterObject);
  40. value = metaObject.getValue(propertyName);
  41. }
  42. cacheKey.update(value);
  43. }
  44. }
  45. if (configuration.getEnvironment() != null) {
  46. // issue #176 当前使用的数据库的id
  47. cacheKey.update(configuration.getEnvironment().getId());
  48. }
  49. return cacheKey;
  50. }
  51. // 最后再看query方法
  52. @Override
  53. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  54. ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  55. if (closed) {
  56. throw new ExecutorException("Executor was closed.");
  57. }
  58. if (queryStack == 0 && ms.isFlushCacheRequired()) {
  59. clearLocalCache();
  60. }
  61. List<E> list;
  62. try {
  63. queryStack++;
  64. list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  65. if (list != null) {
  66. handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  67. } else {
  68. list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  69. }
  70. } finally {
  71. queryStack--;
  72. }
  73. if (queryStack == 0) {
  74. for (DeferredLoad deferredLoad : deferredLoads) {
  75. deferredLoad.load();
  76. }
  77. // issue #601
  78. deferredLoads.clear();
  79. if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  80. // issue #482
  81. clearLocalCache();
  82. }
  83. }
  84. return list;
  85. }
  86. // query方法的操作就很熟悉,先查询缓存,缓存为空,查询数据库,在 queryFromDatabase 方法中查询库并进行了返回。

以上就是缓存的使用,至于清除缓存我们可以看到commit方法中调用了 clearLocalCache() 方法,所以只要进行了增删改触发了事务的提交,那就一定会清空缓存。

二级缓存

二级缓存是跨sqlSession的,是基于mapper接口的,准确的来说是基于namespace的,只要namespace相同,就共用一个二级缓存。二级缓存需要手动开启,开启的方式根据注解开发和xml开发是不同的。
注解开发,需要在接口上添加注解: @CacheNamespace
xml开发,需要在xml文件中添加标签cache:
其次,需要在配置文件Configuration中添加setting:

  1. <settings>
  2. <setting name="cacheEnabled" value="true"/>
  3. </settings>

成功开启二级缓存之后,就可以正常使用。原理同一级缓存相同,首先查询缓存,没有命中,查库,保存到缓存。

一级缓存和二级缓存的执行顺序

对于这里只能说我自己验证的结果,如果存在误差还请和我一起交流。
经过验证,实际的执行顺序是:二级缓存 - 一级缓存 - 数据库。操作流程是先查询二级缓存,没有查询到结果,会查询一级缓存,没有查询到结果会查询数据库。从数据库中查询到结果会放到一级缓存,二级缓存中是没有的,只有当前的sqlSession关闭或者commit之后,才会放到二级缓存,并且当数据存在在二级缓存中时,cleanCache是无法清空二级缓存的
这里需要注意,也正是我开始了解缓存的时候所疑惑的地方。验证代码如下:

  1. @Test
  2. public void testSecondLevelCache() {
  3. SqlSession sqlSession1 = build.openSession();
  4. UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
  5. User user = mapper1.findById(2);
  6. // sqlSession1不关闭,sqlSession2无法命中缓存,
  7. sqlSession1.close();
  8. SqlSession sqlSession2 = build.openSession();
  9. UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
  10. User user1 = mapper2.findById(2);
  11. System.out.println(user == user1);
  12. // sqlSession2清空缓存只能清空自己的一级缓存
  13. sqlSession2.clearCache();
  14. SqlSession sqlSession3 = build.openSession();
  15. UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
  16. User user2 = mapper3.findById(2);
  17. }

既然cleanCache无法清除二级缓存,那怎样清除呢?两个办法

  • 配置入手,flushCache设置为true
  • 进行增删改操作并进行了commit

    测试

    需要注意的是在进行二级缓存操作的时候,需要是pojo类实现Serializable接口,因为缓存可能存在在硬盘上。
    准备工作做好之后编写测试类: ```java @Test public void testSecondLevelCache() { SqlSession sqlSession1 = build.openSession(); UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); User user = mapper1.findById(2); // sqlSession1不关闭,sqlSession2无法命中缓存,那一级缓存和二级缓存的顺序是咋样的呢?是不是意味着一级缓存在关闭的时候会将数据放入二级缓存呢? // 研究一下 sqlSession1.close(); SqlSession sqlSession2 = build.openSession(); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); User user1 = mapper2.findById(2); System.out.println(user == user1); }

// 需要说明的是,运行完成之后可以看到 Cache Hit Ratio [com.wangzhi.mapper.UserMapper]: 0.5,说明二级缓存生效。 // 但是会发现最后的输出结果是false,也就说明了二级缓存存入的是数据,而不是对象,这也是为什么pojo要实现Serializable。

  1. 至于缓存失效也会有所不同,使用clearCache方法并不能清除二级缓存,需要SqlSession进行了增删改并提交了事务,或者通过配置 flushCache = true
  2. <a name="EaYDj"></a>
  3. #### 其他配置
  4. useCacheflushCache,这两个配置是用来给单独的方法或者标签使用的。
  5. ```java
  6. // xml中使用
  7. <select id="selectById" useCache="true" flushCache="true" resultMap="userMap" parameterType="int">
  8. SELECT
  9. user.id AS userId,
  10. user.name AS userName,
  11. orders.id AS orderId,
  12. orders.name AS orderName,
  13. orders.user_id AS orderUserId,
  14. orders.create_time AS orderCreateTime
  15. FROM user LEFT JOIN orders ON user.id = orders.user_id WHERE user.id = #{id}
  16. </select>
  17. // 注解使用
  18. @Options(useCache = true, flushCache = Options.FlushCachePolicy.DEFAULT)
  19. @Select("SELECT id, name FROM user WHERE id = #{id} ")
  20. User findById(int id);

useCache = true表示是否从缓存中读取? true为是,false会直接查询数据库,跳过一级缓存和二级缓存。
flushCache为是否刷新缓存,一定要设置为true,不能设置为false,不然会出现脏读。注解使用的时候flushCache有三个可选值

  • DEFAULT:默认,在进行查询的时候不会刷新缓存,进行增删改操作会刷新缓存。
  • TRUE:进行任何操作都会户刷新缓存
  • FALSE:不会刷新缓存

刷新缓存的意思就是查询数据库。

redis实现二级缓存

既然mybatis本身支持二级缓存,为什么还需要使用redis来进行实现呢?原因就在于分布式环境,mybatis自身的二级缓存并不能支持分布式环境,所以就需要使用支持分布式环境的数据库redis或者其它实现。mybatis提供了redis的实现,我们只需要使用对应的jar包就好。
引入依赖:

  1. <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-redis -->
  2. <dependency>
  3. <groupId>org.mybatis.caches</groupId>
  4. <artifactId>mybatis-redis</artifactId>
  5. <version>1.0.0-beta2</version>
  6. </dependency>
  7. // 确认实现类,接口上注解实现
  8. @CacheNamespace(implementation = RedisCache.class)
  9. // xml标签实现
  10. <cache type="org.mybatis.caches.redis.RedisCache"/>

其它地方不需要进行操作。

源码查看

  1. // 首先,如果不进行redis的配置文件的编写会默认连接本地的redis,默认的配置如下:
  2. public final class Protocol {
  3. private static final String ASK_RESPONSE = "ASK";
  4. private static final String MOVED_RESPONSE = "MOVED";
  5. private static final String CLUSTERDOWN_RESPONSE = "CLUSTERDOWN";
  6. public static final String DEFAULT_HOST = "localhost";
  7. public static final int DEFAULT_PORT = 6379;
  8. public static final int DEFAULT_SENTINEL_PORT = 26379;
  9. public static final int DEFAULT_TIMEOUT = 2000;
  10. public static final int DEFAULT_DATABASE = 0;
  11. ......
  12. }
  13. // 其次,如果需要配置远端redis,就需要配置文件,为什么配置文件的名字一定要是redis.properties呢
  14. final class RedisConfigurationBuilder {
  15. /**
  16. * This class instance.
  17. */
  18. private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();
  19. private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";
  20. private static final String REDIS_RESOURCE = "redis.properties";
  21. ......
  22. }
  23. // 接下来就看具体的实现
  24. public final class RedisCache implements Cache {
  25. // 读写锁
  26. private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
  27. // hset表示使用的数据结构是哈希
  28. @Override
  29. public void putObject(final Object key, final Object value) {
  30. execute(new RedisCallback() {
  31. @Override
  32. public Object doWithRedis(Jedis jedis) {
  33. jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
  34. return null;
  35. }
  36. });
  37. }
  38. @Override
  39. public Object getObject(final Object key) {
  40. return execute(new RedisCallback() {
  41. @Override
  42. public Object doWithRedis(Jedis jedis) {
  43. return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));
  44. }
  45. });
  46. }
  47. }

基本上实现就是这样,如果需要自定义实现缓存,操作的流程就是实现Cache接口,完成实现方法的编写就好。