Featured image of post 在 Spring Boot 用 Redis 作為快取

在 Spring Boot 用 Redis 作為快取

Redis 基本介紹

Data type

String

一個 Key 對應一組資料(字串)

Hash

一個 Key ˇ對應一個 Table 裡面可以存放 ID 、Key 、Value

Lists

一個 Key ˇ對應一個 Table 裡面可以存放 ID 、Value

Set

一個 Key ˇ對應一個 Table 裡面可以存放 ID 、Value

ZSet (Sorted)

一個 Key ˇ對應一個 Table 裡面可以存放 ID 、 Score 、Value

執行的模式

主從模式

哨兵模式

有一群哨兵會監控主機的狀態,若有一隻哨兵判斷主機下線則目前狀態為「主觀離線」,需透過所有的哨兵投票判斷主機是否真的離線,若判斷為下線票數超過設定的票數,則此時主機為「客觀離線」,需經過另外一個投票決定新的主伺服器。

叢集模式

永久儲存模式

AOF

保存每一筆執行的指令到記錄檔,缺點是從備份檔案恢復到記憶體時速度較慢

RDB

將當時的記憶體內容直接保存到硬碟中,優點恢復速度較快,但因為只能保存某一時間點之前的資料,所以會遺漏那一個時間點之後的資料

AOF + RDB

結合前兩點的特性,先使用 RDB 將大部分的內容保存起來,再使用 AOF 處理 RDB 後剩下的一點內容

Install Redis

https://redis.io/docs/getting-started/installation/

On macOS

brew install redis

Start Redis

# Setting Redis password (eg: mypwd) in configure file
echo "requirepass mypwd" > redis.conf
redis-server redis.conf

Use Redis in Spring Boot

Import dependency in pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

Setting Redis configure in application.properties

spring.data.redis.database=0
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=mypwd
spring.data.redis.timeout=60000

基本快取使用

    @GetMapping("/hello")
    @Cacheable(value = "hello", key = "#p0")
    public String hello(@RequestParam("key") String key) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Key(" + key + ") is not found");
    }

    @PostMapping(value = "/hello")
    @CachePut(value = "hello", key = "#p0")
    public String updateHello(@RequestParam("key") String key, @RequestParam("value") String value) {
        return String.format("Key: %s -> Value: %s", key, value);
    }

Demo

redis_test.py

import requests as re

url = "http://localhost:8081/hello"


r = re.get(url, params={"key": "A"})
print(f"GET  A / {r.elapsed.total_seconds()}s / {r.text}")

r = re.post(url, params={"key": "A", "value": "1"})
print(f"POST A / {r.elapsed.total_seconds()}s / {r.text}")

r = re.get(url, params={"key": "A"})
print(f"GET  A / {r.elapsed.total_seconds()}s / {r.text}")

r = re.post(url, params={"key": "A", "value": "111"})
print(f"POST A / {r.elapsed.total_seconds()}s / {r.text}")

r = re.get(url, params={"key": "A"})
print(f"GET  A / {r.elapsed.total_seconds()}s / {r.text}")
user@Hello-MacBook ~ % python3 ./redis_test.py
GET  A / 0.006092s / {"timestamp":"2030-01-01T00:00:00.000+00:00","status":404,"error":"Not Found","path":"/hello"}
POST A / 0.004897s / Key: A -> Value: 1
GET  A / 0.004247s / Key: A -> Value: 1
POST A / 0.004656s / Key: A -> Value: 111
GET  A / 0.00413s  / Key: A -> Value: 111

Lua 模板使用

只允許前三名使用著訪問的功能

HelloController.java

package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.bind.annotation.*;

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(@RequestParam("key") String key) {
        String result = stringRedisTemplate.execute(flashSaleScript, Collections.singletonList("hello_result"), key.toString());
        if (result.startsWith("success-")) {
            return "Success";
        } else {
            return "Error";
        }
    }
}

myfunc.lua (Source: https://ithelp.ithome.com.tw/articles/10285557) save this Lua file to resources/script/myfunc.lua

local myZset = KEYS[1] -- 取得 KEY 名稱
local myZval = ARGV[1] -- 取得 VALUE
local keyType = redis.call('type', myZset) -- 取得 KEY 的型別
keyType = keyType.ok or keyType -- 由 TABLE 取出結果
redis.log(redis.LOG_NOTICE, 'KeyType:'..keyType..', myZset:'..myZset..', myZval:'..myZval) -- 在日誌中印出
-- 若 KEY 存在但不為 ZSET 則回應錯誤
if keyType ~= 'none' and keyType ~= 'zset' then
  return 'error-輸入的 KEY 應為 zset 或不存在'
end
-- 若 VALUE 不存在 則回應錯誤
if myZval == nil then
  return 'error-未輸入 '..myZset..' 對應的值'
end
-- 若 VALUE 已加入 KEY 中,則回應重覆
local myScore = redis.call('zscore',myZset,myZval)
if myScore then
  return 'duplicated-'..myScore
end
-- 若 KEY 內的數目已達 3 則回應無額度
local count = redis.call('zcard', myZset)
if count >= 3 then
  return 'no quota'
end
-- 將 VALUE 加入 KEY 中,並回應其排名
redis.call('zadd',myZset,count+1, myZval)
return 'success-'..(count+1)

MyBean.java

package com.example.demo.bean;

import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

@Component
public class MyBean {
    @Bean
    public RedisScript<String> flashSaleScript() {
        return RedisScript.of(new ClassPathResource("scripts/myfunc.lua"), String.class);
    }
}

RedisConfig.java

package com.example.demo.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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.StringRedisTemplate;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(factory);
        stringRedisTemplate.setEnableTransactionSupport(true);
        return stringRedisTemplate;
    }
}

Demo

# 測試 User A 訪問,應成功
~ % curl "http://localhost:8081/hello?key=A"
Success%

# 測試 User A 訪問,應失敗(重複訪問)
~ % curl "http://localhost:8081/hello?key=A"
Error%

# 測試 User B 訪問,應成功
~ % curl "http://localhost:8081/hello?key=B"
Success%

# 測試 User C 訪問,應成功
~ % curl "http://localhost:8081/hello?key=C"
Success%

# 測試 User D 訪問,應失敗(超過上限)
~ % curl "http://localhost:8081/hello?key=D"
Error%   

 

Licensed under CC BY-NC-SA 4.0