Springboot 集成 mybatis 開啓二級緩存(redis)

首先來了解下mybatis 緩存,mybatis緩存分爲一級緩存和二級緩存。一級緩存是默認開啓的,無需其他配置操作,二級緩存則需要手動設置開啓。

一級緩存原理:

Mybatis的一級緩存是指同一個SqlSession中的操作。一級緩存的作用域是一個SqlSession。
在同一個SqlSession中,執行相同的查詢SQL,第一次會去查詢數據庫,並寫到緩存中;第二次直接從緩存中取。當執行SQL時兩次查詢中間發生了增刪改操作,則SqlSession的緩存清空。

在這裏插入圖片描述

二級緩存原理:

Mybatis的二級緩存是指mapper映射文件。二級緩存是多個sqlSession共享的,其作用域是mapper下的同一個namespace。
在不同的sqlSession中,相同的namespace下,相同的查詢sql語句並且參數也相同的情況下,會命中二級緩存。如果調用相同namespace下的mapper映射文件中的增刪改SQL,並執行了commit操作。此時會清空該namespace下的二級緩存。

在這裏插入圖片描述

瞭解一些基本原理後,我們開始在springboot集成mybatis的情況下,開啓二級緩存。

  1. 在pom.xml文件中引入mybatis和redis的依賴
        <!--mybatis 依賴包-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--redis lettuce-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  1. 在application.yml文件中配置mybatis相關設置時,開啓二級緩存
### mybatis相關配置
mybatis:
  configuration:
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
      #開啓MyBatis的二級緩存
      cache-enabled: true
  mapper-locations: classpath*:mappers/*Mapper.xml

### Redis 相關配置
redis:
  host: localhost
  port: 6379
  timeout: 10000
  database: 0
  lettuce:
    pool:
      max-active: 8
      max-wait: -1
      max-idle: 8
      min-idle: 0
  1. 實體類實現序列化

我們採用的redis序列化方式是默認的jdk序列化。所以數據庫的查詢對象(比如Student類)需要實現Serializable接口。

public class Student implements Serializable {
    //採用的redis序列化方式是默認的jdk序列化。所以數據庫的查詢對象實體需要實現Serializable接口。
    private static final long serialVersionUID = 1L;

    private int id;
    private String name;
    private int age;
    private String position;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getPosition() {
        return position;
    }
    public void setPosition(String position) {
        this.position = position;
    }
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", position='" + position + '\'' +
                '}';
    }
}
  1. 先看一下Redis的配置類(這裏用的是lettuce)
@Configuration
public class RedisConfig {

    @Autowired
    private LettuceConnectionFactory connectionFactory;

    @Bean
    public RedisTemplate<String,Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        initDomainRedisTemplate(redisTemplate, connectionFactory);
        return redisTemplate;
    }

    /**
     * 設置數據存入 redis 的序列化方式
     * @param template
     * @param factory
     */
    private void initDomainRedisTemplate(RedisTemplate<String, Object> template,LettuceConnectionFactory factory) {
        // 定義 key 的序列化方式爲 string
        // 需要注意這裏Key使用了 StringRedisSerializer,那麼Key只能是String類型的,不能爲Long,Integer,否則會報錯拋異常。
        StringRedisSerializer redisSerializer = new StringRedisSerializer();
        template.setKeySerializer(redisSerializer);
        // 定義 value 的序列化方式爲 json
        @SuppressWarnings({"rawtypes", "unchecked"})
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        //hash結構的key和value序列化方式
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setEnableTransactionSupport(true);
        template.setConnectionFactory(factory);
    }
}
  1. 緩存配置類
public class MybatisRedisCache implements Cache {
    private static final Logger log = LoggerFactory.getLogger(MybatisRedisCache.class);
    private String id;
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis過期時間


    public MybatisRedisCache(String id) {
        this.id = id;
    }

    private RedisTemplate<Object, Object> getRedisTemplate(){
        return ApplicationContextHolder.getBean("redisTemplate");
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.boundHashOps(getId()).put(key, value);
        log.info("[結果放入到緩存中: " + key + "=" + value+" ]");

    }

    @Override
    public Object getObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        Object value = redisTemplate.boundHashOps(getId()).get(key);
        log.info("[從緩存中獲取了: " + key + "=" + value+" ]");
        return value;
    }

    @Override
    public Object removeObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        Object value = redisTemplate.boundHashOps(getId()).delete(key);
        log.info("[從緩存刪除了: " + key + "=" + value+" ]");
        return value;
    }

    @Override
    public void clear() {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.delete(getId());
        log.info("清空緩存!!!");
    }

    @Override
    public int getSize() {
        RedisTemplate redisTemplate = getRedisTemplate();
        Long size = redisTemplate.boundHashOps(getId()).size();
        return size == null ? 0 : size.intValue();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }
}

ps:
重點部分就是重寫這個mybatis的cache類,它只會對配置文件類型的映射文件起作用。
該接口共有以下五個方法:
String getId():mybatis緩存操作對象的標識符。一個mapper對應一個mybatis的緩存操作對象。
void putObject(Object key, Object value):將查詢結果塞入緩存。
Object getObject(Object key):從緩存中獲取被緩存的查詢結果。
Object removeObject(Object key):從緩存中刪除對應的key、value。只有在回滾時觸發。
void clear():發生更新時,清除緩存。
int getSize():可選實現。返回緩存的數量。
ReadWriteLock getReadWriteLock():可選實現。用於實現原子性的緩存操作。

上述重寫cache類中有幾個關鍵點:

  • 自定義實現的二級緩存,必須要有一個帶id的構造函數,否則會報錯。
  • 此處使用Spring封裝的redisTemplate來操作Redis。很多都是直接用jedis庫,但是現在springboot2.x 以上對lettuce的兼容更好。RedisTemplate封裝了底層的實現,使用redisTemplate會更加方便,無論是使用jedis還是使用lettuce,我們可以直接更換底層的庫,無需修改上層代碼。
  • 這裏不能通過@Autowire的方式引用redisTemplate,因爲RedisCache並不是Spring容器裏的bean。所以我們需要手動地去調用容器的getBean方法來拿到這個bean,那麼這樣,我們就需要引入ApplicationContextHolder這個類。
  1. ApplicationContextHolder.java (我們需要通過這個類得到RedisTemplate)
@Component
public class ApplicationContextHolder implements ApplicationContextAware{

    private static ApplicationContext applicationContext;

    /**
     * 實現ApplicationContextAware接口的context注入函數, 將其存入靜態變量.
     */
    public void setApplicationContext(ApplicationContext applicationContext) {
        ApplicationContextHolder.applicationContext = applicationContext; // NOSONAR
    }

    /**
     * 取得存儲在靜態變量中的ApplicationContext.
     */
    public static ApplicationContext getApplicationContext() {
        checkApplicationContext();
        return applicationContext;
    }

    /**
     * 從靜態變量ApplicationContext中取得Bean, 自動轉型爲所賦值對象的類型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        checkApplicationContext();
        return (T) applicationContext.getBean(name);
    }

    /**
     * 從靜態變量ApplicationContext中取得Bean, 自動轉型爲所賦值對象的類型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(Class<T> clazz) {
        checkApplicationContext();
        return (T) applicationContext.getBeansOfType(clazz);
    }

    /**
     * 清除applicationContext靜態變量.
     */
    public static void cleanApplicationContext() {
        applicationContext = null;
    }

    private static void checkApplicationContext() {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext未注入,請在applicationContext.xml中定義SpringContextHolder");
        }
    }
}
  1. 然後再映射文件中開啓二級緩存(使用二級緩存)
<mapper namespace="com.example.demo.dao.StudentDao">

    <!-- 開啓基於redis的二級緩存 -->
    <cache type="com.example.demo.redis.cache.MybatisRedisCache"/>
    <cache/>

    <insert id="insert" parameterType="com.example.demo.entity.Student" useGeneratedKeys="true" keyProperty="id">
        insert into
        students(name,age,position)
        values
        (#{name},#{age},#{position})
    </insert>

    <insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">
        insert into
        students(name,age,position)
        values
        <foreach collection="studentList" item="item" index="index" open="" close="" separator=",">
            (
              #{item.name},
              #{item.age},
              #{item.position}
            )
        </foreach>
    </insert>
    
    <delete id="delete" parameterType="java.lang.String">
        delete from students where name = #{name}
    </delete>

    <!--並且在update語句中,設置flushCache爲true,這樣在更新信息時,能夠自動失效緩存(本質上調用的是clear方法)-->
    <update id="update" parameterType="com.example.demo.entity.Student" flushCache="true">
        update students
          set students.position = #{position}
          where name = #{name}
    </update>
    
    <select id="findByName" resultMap="BaseResultMap">
        select *
        from students
        where name = #{name}
    </select>

    <select id="findAll" resultMap="BaseResultMap">
        select *
        from students
    </select>

    <resultMap id="BaseResultMap" type="com.example.demo.entity.Student">
        <result column="name" property="name"/>
        <result column="age" property="age"/>
        <result column="position" property="position"/>
    </resultMap>
</mapper>

下面是我在實現二級緩存過程中一些報錯問題:

  1. 在我修改了序列化問題後,報錯消失。
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章