微服務解決方案 -- Mybatis-Plus + Redis快取,如何不太優雅的使用Redis快取

2020-09-22 12:00:41

如何不太優雅的使用Redis快取

我們都知道使用redis來快取我們的資料集合,如下圖所示。

通常自己去快取資料,這樣的優點就是邏輯清晰,而且rediskeyvalue會比較規範。但是冗餘程式碼會比較多,需要自己進行判斷資料是否過期。
為了簡化業務程式碼,現在用註解的方式整合redis二級快取,但是他的keyvalue就會比較不符合規範。他的key一共包含5個部分,最重要的就是sql和這個sql的引數。他的value就是這個查詢的結果集。

準備工作

引入依賴,mybatis

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <exclusions>
        <!-- 排除 tomcat-jdbc 以使用 HikariCP -->
        <exclusion>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.6</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
    <version>3.0.6</version>
</dependency>

redis依賴

<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>

組態檔

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 這裡使用的是 ip:3336/db_order 的資料庫(一個服務一個資料庫)
    url: jdbc:mysql://localhost:3306/sys-common?useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong&useSSL=false
    username: root
    password: 123456
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
  # redis
  redis:
    host: xxx.xxx.xxx.xxx
    port: 6379
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1ms
        min-idle: 0
    database: 4

設定redisTemplate

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * ProjectName:     hello-redis-cache
 * Package:         com.laoshiren.hello.redis.cache.mybatis.configure
 * ClassName:       RedisConfiguration
 * Author:          laoshiren
 * Description:
 * Date:            2020/9/13 15:49
 * Version:         1.0
 */
@Configuration
public class RedisConfiguration {

    /**
     * 設定redisTemplate
     */
    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 使用String 序列化
//        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
//        redisTemplate.setKeySerializer(new StringRedisSerializer());
//        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

注意這裡我不使用String的序列化方式去序列化KeyValue

實現

實現Cache介面

package com.laoshiren.hello.redis.cache.mybatis.cache;

import com.laoshiren.hello.redis.cache.mybatis.configure.ApplicationContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;

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

/**
 * ProjectName:     hello-redis-cache
 * Package:         com.laoshiren.hello.redis.cache.mybatis.cache
 * ClassName:       RedisCache
 * Author:          laoshiren
 * Description:
 * Date:            2020/9/13 15:34
 * Version:         1.0
 */
@Slf4j
public class RedisCache implements Cache {

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id; // cache instance id
    private RedisTemplate redisTemplate;

    private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis過期時間

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require 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) {
        try {
            RedisTemplate redisTemplate = getRedisTemplate();
            redisTemplate.opsForValue().set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
            log.debug("Put query result to redis");
        } catch (Throwable t) {
            log.error("Redis put failed", t);
        }
    }

    /**
     * Get cached query result from redis
     *
     * @param key
     * @return
     */
    @Override
    public Object getObject(Object key) {
        try {
            RedisTemplate redisTemplate = getRedisTemplate();
            log.info("Get cached query result from redis");
//            System.out.println("****" + opsForValue.get(key).toString());
            return redisTemplate.opsForValue().get(key);
        } catch (Throwable t) {
            log.error("Redis get failed, fail over to db", t);
            return null;
        }
    }

    /**
     * Remove cached query result from redis
     *
     * @param key
     * @return
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object removeObject(Object key) {
        try {
            RedisTemplate redisTemplate = getRedisTemplate();
            redisTemplate.delete( key.toString());
            log.debug("Remove cached query result from redis");
        } catch (Throwable t) {
            log.error("Redis remove failed", t);
        }
        return null;
    }

    /**
     * Clears this cache instance
     */
    @Override
    public void clear() {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.execute((RedisCallback) connection -> {
            connection.flushDb();
            return null;
        });
        log.debug("Clear all the cached query result from redis");
    }

    /**
     * This method is not used
     *
     * @return
     */
    @Override
    public int getSize() {
        return 0;
    }

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

    private RedisTemplate getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }
}

給指定的mapper設定快取

@CacheNamespace(implementation = RedisCache.class)
public interface TbPostMapper extends BaseMapper<TbPost> {
}

測試

請求一次資料庫,使用Debug模式,它的key是一個CacheKey,無法使用使用StringRedisSerializer去序列化,所以redisTemplate 得使用預設的序列化,即 JdkSerializationRedisSerializer

開啟RDM,看一下4號庫。

發現keyvalue就不是很美觀,不過不影響使用,當然您可以使用StringRedisSerializer去實現,只不過我在嘗試的過程中獲取sql和引數的時候,會出現一點問題。希望有大佬可以指出。

帶引數的sql

特別注意! 在分頁快取的時候,Page物件的total必須自己手動查詢一次,不然返回給前端的物件裡第一次還有總頁數,第二次由於走了快取就不帶這個total,所以必須手動查詢一次。

@GetMapping("page/{pageNo}/{pageSize}")
public ResponseResult<IPage<Area>> page(@PathVariable(name = "pageNo")Integer pageNo,
                                        @PathVariable(name = "pageSize") Integer pageSize,
                                        HttpServletRequest request){
    IPage<Area> wherePage = new Page<>(pageNo, pageSize);
    String word = request.getParameter("wd");
    LambdaQueryWrapper<Area> queryWrapper = new LambdaQueryWrapper<>();
    if (StringUtils.isNotBlank(word)) {
        queryWrapper.like(Area::getAreaName,word);
    }
    int count = areaService.count(queryWrapper);
    IPage<Area> page = areaService.page(wherePage,queryWrapper);
    page.setTotal((long)count);
    return new ResponseResult<>(CodeStatus.OK,"操作成功",page);
}

後續我也會繼續更新這篇部落格。好了,最後還是借用大佬的一句話:「不經一番寒徹骨,怎知梅花撲鼻香」。