什么是二级缓存?

Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

  • 如上图sqlSession1在查询时会从UserMapper的二级缓存中取,如果没有则执行数据库查询操作。
  • 然后写入到二级缓存中
  • sqlSession2则执行同样的UserMapper查询时,会从UserMapper的二级缓存中取,此时的二级缓存中已经有内容了,所以就可以直接取到,不再与数据库交互。
  • sqlSession3在执行事务操作(插入、更新、删除)时,会清空UserMapper的二级缓存

1. 开启二级缓存

如何使用二级缓存:

mybatis中,一级缓存是默认开启的,但是二级缓存需要配置才可以使用

  1. 在全局配置文件sqlMapConfig.xml中加入如下代码:

    <!--开启二级缓存-->
    <settings>
    	<setting name="cacheEnabled" value="true"/>
    </settings>
    
  2. 其次在哪个namespace中开启二级就在哪里配置,因为mybatis有注解和xml两种方式所以:

  • 注解
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    注解扩展:

    //我们默认使用的是mybatis自带的二级缓存,它的实现在PerpetualCache类中,所以可以写成
    @CacheNamespace(implementation = PerpetualCache.class)
    //如果是使用redis作为二级缓存的话,下面第二部分会讲到
    
  • xml
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    这样就开启了UserMapper的二级缓存

  1. 测试一:

    我们要根据用户id查询用户信息:

    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

    注意:将缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存的存储介质多种多样,不一定只在内存中,也可能在硬盘中,如果我们要再取出这个缓存的话,就需要反序列化了。所以mybatis的pojo都去实现Serializable接口
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    最后执行看到打印日志:
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    为什么System.out.println(user1==user2)为false ?

  2. 测试二:
    我们在测试二中进行一下事务操作,看看是否能清空二级缓存:
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

​ 增加了一个修改操作,发现执行了两个select,说明提交事务会刷新二级缓存

userCache和flushCache

还可以配置userCacheflushCache

  • userCache : 是用来设置是否禁用二级缓存的,在statement中设置可以禁用当前select语句的二级缓存,即每次查询都会发出sql。默认情况为true.

    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

  • flushCache : 在mapper的同一个namespace中,如果有其它的增删改操作后需要刷新缓存,如果部执行刷新缓存会出现脏读。
    设置statement配置中的flushCache="true",即刷新缓存,如果改成false则不会刷新,有可能出现脏读。所以一般情况下没必要改
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

2. 使用Redis实现二级缓存

Mybatis自带的二级缓存是有缺点的,就是这个缓存是单服务器进行工作的,无法实现分布式缓存。
Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

所以为了解决这个问题,必须找一个分布式缓存专门存放缓存数据。
Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

如何使用

mybatis提供了一个针对cache接口的redis实现类,在mybatis-redis包中

  1. 首先我们引入jar包

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-redis</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>
    
  2. 修改Mapper.xml文件

    //**********XML方式***********:
    <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.lagou.mapper.IUserMapper">
        //表示针对于当前的namespace开启二级缓存
        <cache type="org.mybatis.caches.redis.RedisCache" />
    
        <select id="findAll" resultType="com.lagou.pojo.User" useCache="true">
        	select * from user
        </select>
    
    //*******注解方式**********
    @CacheNamespace(implementation = RedisCache .class)
    public interface UserMapper {
        //根据id查询用户 注解使用
        @Select("select * from user where id=#{id}")
        public User findById(Integer id);
    
    

    这个类同样实现了Cache接口
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP

  3. 配置redis的配置文件

    redis.host=localhost
    redis.port=6379
    redis.connectionTimeout=5000
    redis.password=
    redis.database=0
    

    测试方法同自带的二级缓存一样。

3. Redis二级缓存源码分析

RedisCache和Mybatis二级缓存的方案都差不多,无非是实现Cache接口,并使用jedis操作缓存,不过在设计细节上有点区别。
我们带着问题分析源码:

  • 在RedisCache类中如何向redis中进行缓存值的存取 ?
  • 使用了哪种数据结构 ?
package org.mybatis.caches.redis;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

//首先其实现了Cache接口,被mybatis初始化的时候的CacheBuilder创建
//创建方式就是调用了下面的有参构造
public final class RedisCache implements Cache {
  private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
  private String id;
  private static JedisPool pool;
  //有参构造
  public RedisCache(final String id) {
    if (id == null) {
      throw new IllegalArgumentException("Cache instances require an ID");
    }
    this.id = id;
    //RedisConfigurationBuilder调用parseConfiguration()方法创建RedisConfig对象
    RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
    //构建Jedis池
	pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(),
			redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(),
			redisConfig.getDatabase(), redisConfig.getClientName());
  }

  //模板方法,下面的putObject和getObject、removeObject都会用到这个方法
  private Object execute(RedisCallback callback) {
    Jedis jedis = pool.getResource();
    try {
      return callback.doWithRedis(jedis);
    } finally {
      jedis.close();
    }
  }

  //。。。。。。。。省略部分代码


  @Override
  public void putObject(final Object key, final Object value) {
    execute(new RedisCallback() {
      @Override
      public Object doWithRedis(Jedis jedis) {
        jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
        return null;
      }
    });
  }

  @Override
  public Object getObject(final Object key) {
    return execute(new RedisCallback() {
      @Override
      public Object doWithRedis(Jedis jedis) {
        return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));
      }
    });
  }

  @Override
  public Object removeObject(final Object key) {
    return execute(new RedisCallback() {
      @Override
      public Object doWithRedis(Jedis jedis) {
        return jedis.hdel(id.toString(), key.toString());
      }
    });
  }


}

  1. RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    RedisConfig中封装了默认的Redis配置信息

    Mybatis的二级缓存、使用Redis做二级缓存-LMLPHP
    这个方法读取了我们配置在/resource/redis.properties这个文件
    RedisConfig后构建了Jedis池

  2. put方法

    private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();
        try {
          return callback.doWithRedis(jedis);
        } finally {
          jedis.close();
        }
      }
    
    public void putObject(final Object key, final Object value) {
        execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
            return null;
          }
        });
      }
    

    我们可以看到,put方法调用了模板方法得到 一个jedis链接,然后调用doWithRedis()方法

    jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
    
11-12 20:36