SpringBoot+Mybatis整合Redis做MySQL的二級緩存

SpringBoot+Mybatis整合Redis做MySQL的二級緩存

第一次接觸Redis有說的不對的地方歡迎大家指正,代碼親測可以食用。文章是在vscode中用markdown插件寫完的,後來想想還是發在csdn上吧,就直接站題過來了,排版或者什麼可能會有些問題,就先說這麼多叭。

1. 什麼是Redis

  • Redis是一個NoSQL key-value型數據庫,在項目中使用Redis主要考慮性能和併發兩個方面。redis還可以做簡單的消息中間件,分佈式鎖等

2. 緩存機制

  • 一級緩存和二級緩存
    Mybatis默認開啓一級緩存,MyBatis框架定義了一個Cache接口來支持二級(第三方)緩存。

3. 整合

3.1 在application.yml中配置mysql、druid連接池(不配也能用)、服務器端口號

spring:
  profiles:
    active: prod
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/db_nenu_market?autoReconnect=true&useSSL=false
    username: root
    password: xy111225
    type: com.alibaba.druid.pool.DruidDataSource
    # 下面爲連接池的補充設置,應用到上面所有數據源中
    # 初始化大小,最小,最大
    initialSize: 1
    minIdle: 3
    maxActive: 20
    # 配置獲取連接等待超時的時間
    maxWait: 60000
    # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一個連接在池中最小生存的時間,單位是毫秒
    minEvictableIdleTimeMillis: 30000
    validationQuery: select 'x'
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打開PSCache,並且指定每個連接上PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    # 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
    filters: stat,wall,slf4j
    # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    # 合併多個DruidDataSource的監控數據
    useGlobalDataSourceStat: true
  server:
      port:8080
  servlet:
      path"/springboot
  • 開啓MyBatis二級緩存、配置相關路徑
mybatis:
  configuration:
    cache-enabled: true
  #實體類所做包
  type-aliases-package: com.join.nenuscfx.entity
  #mapper.xml所在位置
  mapper-locations: classpath:mapper/*/*.xml
  • 配置Redis
redis:
  host: 127.0.0.1
  port: 6379
  timeout: 5000
  database: 0
  #我的redis沒有密碼
  password:

3.2 實現cache接口

  • Cache接口實際上定義了你希望MyBatis在增刪改查時進行操作的規則
    所以我們需要編寫一個類來實現這個接口,定義這些規則
    RedisCache.class
import com.join.nenuscfx.util.ApplicationContextHolder;
import org.apache.ibatis.cache.Cache;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class RedisCache implements Cache {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id;
    private RedisTemplate redisTemplate;
    //redis過期時間
    private static final long EXPIRE_TIME_IN_MINUTES = 30;

    public RedisCache(String id){
        if (id == null){
            throw  new IllegalArgumentException("Cache instance required an ID");
        }
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }
    /**
     * Put query result to redis
     * @Param key
     * @Param value
     * */
    @Override
    public void putObject(Object key, Object value) {
        RedisTemplate redisTemplate = getRedisTemplate();
        ValueOperations opsForValue = redisTemplate.opsForValue();
        System.out.println(key+": key");
        System.out.println(key.toString()+": key.toString()");
        System.out.println(value+": value");
        System.out.println(value.toString()+": value.toString()");
        opsForValue.set(key.toString(),value,EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
        System.out.println("結果成功放入緩存 and "+"key = " +"\n"+ key + "value = " + value);
        System.out.println(opsForValue.get(key.toString()));
    }
    /**
     * Get cached query result to redis
     * @Param key
     * @Return
     * */
    @Override
    public Object getObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
//        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        ValueOperations opsForValue = redisTemplate.opsForValue();
        System.out.println("結果從緩存中獲取");
        return opsForValue.get(key.toString());
    }
    /**
     * Remove cached query result to redis
     * @Param key
     * @Return
     * */
    @Override
    public Object removeObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.delete(key);
        System.out.println("從緩存中刪除");
        return null;
    }
    /**
     * Clear this cache instance
     * */
    @Override
    public void clear() {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.execute((RedisCallback) connection -> {
            connection.flushDb();
            return null;
        });
        System.out.println("清空緩存");
    }

    @Override
    public int getSize() {
        Long size = (Long) redisTemplate.execute((RedisCallback) connection -> connection.dbSize());
        return size.intValue();
    }

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

    private RedisTemplate getRedisTemplate() {
        if(redisTemplate == null){
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }
}
  • 由於redisTemplate無法通過註解交給spring容器託管,所以我們需要手動注入。創建ApplicationContextHolder類來完成這個功能。
    ApplicationContextHolder
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {
    private static ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext(){
        checkApplicationContext();
        return applicationContext;
    }

    public static <T> T getBean(String name){
        checkApplicationContext();
        return (T) applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz){
        checkApplicationContext();
        return (T) applicationContext.getBeansOfType(clazz);
    }

    public static void cleanApplicationContext(){
        applicationContext = null;
    }

    private static void checkApplicationContext(){
        if(applicationContext ==null){
            if (applicationContext == null){
                throw new IllegalStateException("applicationContext未注入,請在applicationContext.xml中定義SpringContext");
            }
        }
    }

    @Override
    public void destroy() throws Exception {

    }
}
  • 注意一下,當我們的數據存儲到Redis的時候,我們的key和value都是通過Spring提供的Serializer序列化到redis數據庫的,RedisTemplate默認使用的是JdkSerializationRedisSerializer,StringRedisTemplate默認使用的是StringRedisSerializer。如果想要自己定義序列化方式,可以自己配製RedisTemplate並定義Serializer。這裏提供一個通過Jackson2JsonRedisSerializer來序列化value以及通過StringRedisSerializer來序列化key的示例代碼。
    既然是使用序列化方式,那麼實體類一定要實現Serializable接口,請自己自行在實體類後寫上 implements Serializable

注:這段代碼是在網上博客上找到的

package com.join.nenuscfx.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    /**
     * 重寫Redis序列化方式,使用Json方式:
     * 當我們的數據存儲到Redis的時候,我們的鍵(key)和值(value)都是通過Spring提供的Serializer序列化到數據庫的。RedisTemplate默認使用的是JdkSerializationRedisSerializer,StringRedisTemplate默認使用的是StringRedisSerializer。
     * Spring Data JPA爲我們提供了下面的Serializer:
     * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
     * 在此我們將自己配置RedisTemplate並定義Serializer。
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 設置值(value)的序列化採用Jackson2JsonRedisSerializer。
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // 設置鍵(key)的序列化採用StringRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

3.3 在mapper.xml文件中指定使用RedisCache作爲緩存

<mapper namespace="com.join.nenuscfx.mapper.usermapper.UserMapper">
    <cache type="com.join.nenuscfx.redis.RedisCache"/>
</mapper>

4 最後想說的話以及踩過的坑

  • 序列化在我看來就是將對象以字節序列形式存儲,反序列化就是從字節序列中直接獲得一個對象。
  • 在get(key)時如果不是寫成get(key.toString())會報:CacheKey can’t cast to java.lang.String
  • 我在剛寫好這個項目時,出現錯誤:
    無法反序列化,非法的反序列化header 000000
    具體的錯誤信息忘記了,但翻譯過來基本是這個意思。找了很多博客都沒實際解決問題,有說序列化與反序列化方式不一致的,有說不能使用jdk自帶的序列化的,最後好像還是key.toString()的問題。當時也不知道怎麼就謎一樣的解決掉了,以後遇到再說吧。Over。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章