優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

米奇羅發表於2019-03-26

1. 前言

一個系統在於資料庫互動的過程中,記憶體的速度遠遠快於硬碟速度,當我們重複地獲取相同資料時,我們一次又一次地請求資料庫或遠端服務,者無疑時效能上地浪費(這會導致大量時間被浪費在資料庫查詢或者遠端方法呼叫上致使程式效能惡化),於是有了“快取”。 本文將介紹在spring boot專案開發中怎樣使用spring提供的Spring Cache 與最近很火的 Redis 資料庫來實現資料的快取。

2. SpringCache簡介

Spring CacheSpring框架提供的對快取使用的抽象類,支援多種快取,比如RedisEHCache等,整合很方便。同時提供了多種註解來簡化快取的使用,可對方法進行快取。

2.1 關於SpringCache 註解的簡單介紹

  • @Cacheable:標記在一個方法上,也可以標記在一個類上。主要是快取標註物件的返回結果,標註在方法上快取該方法的返回值,標註在類上,快取該類所有的方法返回值。 引數: value快取名、 key快取鍵值、 condition滿足快取條件、unless否決快取條件

@Cacheable

  • @CacheEvict:從快取中移除相應資料。

@CacheEvict

  • @CachePut:方法支援快取功能。與@Cacheable不同的是使用@CachePut標註的方法在執行前不會去檢查快取中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的快取中。

@CachePut

  • @Caching:多個Cache註解使用,比如新增使用者時,刪除使用者屬性等需要刪除或者更新多個快取時,集合以上三個註解。

2.2 SpEL上下文資料

Spring Cache提供了一些供我們使用的SpEL上下文資料,下表直接摘自Spring官方文件:

名字 位置 描述 示例
methodName root物件 當前被呼叫的方法名 #root.methodName
method root物件 當前被呼叫的方法 #root.method.name
target root物件 當前被呼叫的目標物件 #root.target
targetClass root物件 當前被呼叫的目標物件類 #root.targetClass
args root物件 當前被呼叫的方法的引數列表 #root.args[0]
caches root物件 當前方法呼叫使用的快取列表(如@Cacheable(value={"cache1", "cache2"})),則有兩個cache #root.caches[0].name
argument name 執行上下文 當前被呼叫的方法的引數,如findById(Long id),我們可以通過#id拿到引數 #user.id
result 執行上下文 方法執行後的返回值(僅當方法執行之後的判斷有效,如‘unless’,'cache evict'的beforeInvocation=false) #result

其他關於 Cache 詳細配置或註解,請參考文章基於Redis的Spring cache 快取介紹或spring官方文件

3. Redis簡介

Redis 是完全開源免費的,遵守BSD協議,是一個高效能的key-value資料庫。

Redis 與其他 key - value 快取產品有以下三個特點:

  • Redis支援資料的持久化,可以將記憶體中的資料儲存在磁碟中,重啟的時候可以再次載入進行使用。
  • Redis不僅僅支援簡單的key-value型別的資料,同時還提供list,set,zset,hash等資料結構的儲存。
  • Redis支援資料的備份,即master-slave模式的資料備份。

Redis 的安裝和使用請自行Google 。

4. 實踐--SpringCache和Redis整合

4.1 步驟

我們要把一個查詢函式加入快取功能,大致需要三步。

  • 一、在函式執行前,我們需要先檢查快取中是否存在資料,如果存在則返回快取資料。
  • 二、如果不存在,就需要在資料庫的資料查詢出來。
  • 三、最後把資料存放在快取中,當下次呼叫此函式時,就可以直接使用快取資料,減輕了資料庫壓力。

本例項沒有存入MySQL資料庫,主要是為了方便實踐,實際使用中大家可以把service層中的方法改為資料庫操作程式碼即可。

4.2 具體操作

新增依賴

<!-- springboot redis依賴-->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
複製程式碼

注: 其實我們從官方文件可以看到spring-boot-starter-data-redis 已經包含了jedis客戶端,我們在使用jedis連線池的時候不必再新增jedis依賴。

jedis

配置SpringCache,Redis連線等資訊

  • SpringCache配置--RedisConfig.java

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
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;

/**
 * @Author: MaoLin
 * @Date: 2019/3/26 17:04
 * @Version 1.0
 */

@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 申明快取管理器,會建立一個切面(aspect)並觸發Spring快取註解的切點(pointcut)
     * 根據類或者方法所使用的註解以及快取的狀態,這個切面會從快取中獲取資料,將資料新增到快取之中或者從快取中移除某個值

     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.create(redisConnectionFactory);
    }

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 建立一個模板類
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 將剛才的redis連線工廠設定到模板類中
        template.setConnectionFactory(factory);
        // 設定key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 設定value的序列化器
        //使用Jackson 2,將物件序列化為JSON
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //json轉物件類,不設定預設的會將json轉成hashmap
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        return template;
    } 
}

複製程式碼
  • redis配置--application.yml
server:
  port: 8080
spring:
  # redis相關配置
  redis:
    database: 0
    host: localhost
    port: 6379
    password: 123456
    jedis:
      pool:
        # 連線池最大連線數(使用負值表示沒有限制)
        max-active: 8
        # 連線池最大阻塞等待時間(使用負值表示沒有限制)
        max-wait: -1ms
        # 連線池中的最大空閒連線
        max-idle: 8
        # 連線池中的最小空閒連線
        min-idle: 0
    # 連線超時時間(毫秒)預設是2000ms
    timeout: 2000ms
  cache:
    redis:
      ## Entry expiration in milliseconds. By default the entries never expire.
      time-to-live: 1d
      #寫入redis時是否使用鍵字首。
      use-key-prefix: true
複製程式碼

編寫實體類

import lombok.Data;
import java.io.Serializable;

/**
 * @Author: MaoLin
 * @Date: 2019/3/24 14:36
 * @Version 1.0
 */

@Data
//lombok依賴,可省略get set方法
public class User implements Serializable {

    private int userId;

    private String userName;

    private String userPassword;

    public User(int userId, String userName, String userPassword) {
        this.userId = userId;
        this.userName = userName;
        this.userPassword = userPassword;
    }
}

複製程式碼

service簡單操作

import com.ml.demo.entity.User;
import org.springframework.stereotype.Service;

/**
 * @Author: MaoLin
 * @Date: 2019/3/24 14:38
 * @Version 1.0
 */
@Service
public class UserDao {

    public User getUser(int userId) {
        System.out.println("執行此方法,說明沒有快取,如果沒有走到這裡,就說明快取成功了");
        User user = new User(userId, "沒有快取_"+userId, "password_"+userId);
        return user;
    }

    public User getUser2(int userId) {
        System.out.println("執行此方法,說明沒有快取,如果沒有走到這裡,就說明快取成功了");
        User user = new User(userId, "name_nocache"+userId, "nocache");
        return user;
    }
}
複製程式碼

控制層

  • 在方法上新增相應的方法即可操作快取了,SpringCache 物件可以對redis自行操作,減少了很多工作啊,還是那個開箱即用的Spring
import com.ml.demo.dao.UserDao;
import com.ml.demo.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

/**
 * @Author: MaoLin
 * @Date: 2019/3/26 17:03
 * @Version 1.0
 */


@RestController
public class testController {
    @Resource
    private UserDao userDao;

    /**
     * 查詢出一條資料並且新增到快取
     *
     * @param userId
     * @return
     */
    @RequestMapping("/getUser")
    @Cacheable("userCache")
    public User getPrud(@RequestParam(required = true) String userId) {
        System.out.println("如果沒有快取,就會呼叫下面方法,如果有快取,則直接輸出,不會輸出此段話");
        return userDao.getUser(Integer.parseInt(userId));
    }

    /**
     * 刪除一個快取
     *
     * @param userId
     * @return
     */
    @RequestMapping(value = "/deleteUser")
    @CacheEvict("userCache")
    public String deleteUser(@RequestParam(required = true) String userId) {
        return "刪除成功";
    }

    /**
     * 新增一條儲存的資料到快取,快取的key是當前user的id
     *
     * @param user
     * @return
     */
    @RequestMapping("/saveUser")
    @CachePut(value = "userCache", key = "#result.userId +''")
    public User saveUser(User user) {
        return user;
    }


    /**
     * 返回結果userPassword中含有nocache字串就不快取
     *
     * @param userId
     * @return
     */
    @RequestMapping("/getUser2")
    @CachePut(value = "userCache", unless = "#result.userPassword.contains('nocache')")
    public User getUser2(@RequestParam(required = true) String userId) {
        System.out.println("如果走到這裡說明,說明快取沒有生效!");
        User user = new User(Integer.parseInt(userId), "name_nocache" + userId, "nocache");
        return user;
    }


    @RequestMapping("/getUser3")
    @Cacheable(value = "userCache", key = "#root.targetClass.getName() + #root.methodName + #userId")
    public User getUser3(@RequestParam(required = true) String userId) {
        System.out.println("如果第二次沒有走到這裡說明快取被新增了");
        return userDao.getUser(Integer.parseInt(userId));
    }
}

複製程式碼

接下來最重要的工作:跑起來

執行結果

  • 存入資料:

    優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

  • 從快取讀取資料:

    優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

  • 刪除快取:

    優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

  • 再讀取:

    優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

  • 此時沒有快取,呼叫方法,並存入快取

    優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

  • 此為cache中的條件:含有nocache字元時不存入快取。自己去探索就好。

    優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

5. 小結&參考資料

小結

為了實現快取,在網上參考了很多部落格、資料,但是都不盡人意,後來經過幾天的學習,發現Spring提供了快取物件,我結合redis,優雅地實現了快取。學習程式碼是個艱辛的過程,我在學習這部分時看了好多書,找了好多部落格資料,終於找到了合適的快取方案,很開心,不過這還只是一小步啊,加油!!!

參考資料

相關文章