分散式快取綜合指南:Kubernetes + Redis + Spring Boot

banq發表於2022-05-29

本文的範圍是提供全面的指南,以在 Kubernetes 叢集上啟動 Redis 主從叢集並實現支援分散式快取的 Sprinboot 應用程式。對 Kubernetes/Redis/Spring boot 的全面介紹超出了本文的範圍。

先決條件
  1. 啟動並執行 Kubernetes 叢集
  2. Node.js v16.15.0 或本地最新版本
  3. 本地首選 IDE 或文字編輯器
  4. Java 8 或本地最新版本


在 Kubernetes 上部署 Redis 叢集
以下步驟描述瞭如何在 Kubernetes 上設定 Redis 主從叢集。我強烈建議在部署到生產環境之前對 K8S Storage Class/Persistent Volume/ConfigMap 物件進行一些研究。如果您需要全面瞭解以下步驟,請閱讀本教程。

1、建立名稱空間
在您的 K8S 叢集上執行流動命令以建立名稱空間,這將允許更有效地管理您在 K8S 叢集上的物件。
kubectl create ns redis

2、定義一個儲存類
現在我們將建立一個應用於整個叢集的儲存類。在您的 K8S 叢集上執行流動命令以建立儲存類。storage-class.yaml包含配置。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: Delete


kubectl apply -f storage-class.yaml

3.建立一個持久卷
在這個解決方案中,我們在 Redis 叢集上建立了 3 個節點,所以我們需要 3 個持久卷。在您的 K8S 叢集上執行流動命令以建立持久卷。persistent-volume.yaml包含配置。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv1
spec:
  storageClassName: local-storage
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/data1"

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv2
spec:
  storageClassName: local-storage
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/data2"

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv3
spec:
  storageClassName: local-storage
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/data3"


kubectl apply -f persistent-volume.yaml

4.建立配置圖
您可以在此處獲取 ConfigMap 清單的配置。請確保更改masterauth&requirepass 值。這兩個變數是 Redis 叢集主從節點的密碼。如果您對兩者使用相同的值將很容易維護。
kubectl apply -n redis -f redis-config.yaml

5.使用StatefulSet部署Redis
StatefulSet 在需要控制主從行為時管理 pod。在您的 K8S 叢集上執行流動命令以建立持久卷。persistent-volume.yaml 包含配置。
redis-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      initContainers:
      - name: config
        image: redis:6.2.3-alpine
        command: [ "sh", "-c" ]
        args:
          - |
            cp /tmp/redis/redis.conf /etc/redis/redis.conf
            
            echo "finding master..."
            MASTER_FDQN=`hostname  -f | sed -e 's/redis-[0-9]\./redis-0./'`
            if [ "$(redis-cli -h sentinel -p 5000 ping)" != "PONG" ]; then
              echo "master not found, defaulting to redis-0"
              if [ "$(hostname)" == "redis-0" ]; then
                echo "this is redis-0, not updating config..."
              else
                echo "updating redis.conf..."
                echo "slaveof $MASTER_FDQN 6379" >> /etc/redis/redis.conf
              fi
            else
              echo "sentinel found, finding master"
              MASTER="$(redis-cli -h sentinel -p 5000 sentinel get-master-addr-by-name mymaster | grep -E '(^redis-\d{1,})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})')"
              echo "master found : $MASTER, updating redis.conf"
              echo "slaveof $MASTER 6379" >> /etc/redis/redis.conf
            fi
        volumeMounts:
        - name: redis-config
          mountPath: /etc/redis/
        - name: config
          mountPath: /tmp/redis/
      containers:
      - name: redis
        image: redis:6.2.3-alpine
        command: ["redis-server"]
        args: ["/etc/redis/redis.conf"]
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: data
          mountPath: /data
        - name: redis-config
          mountPath: /etc/redis/
      volumes:
      - name: redis-config
        emptyDir: {}
      - name: config
        configMap:
          name: redis-config
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "local-storage"
      resources:
        requests:
          storage: 500Mi


kubectl apply -n redis -f redis-statefulset.yaml

6.建立負載均衡服務
在 Kubernetes 部署的最後一步,我們將通過公共 IP 公開 Redis 伺服器。為此,我們部署了 Loadbalancer 服務。

apiVersion: v1
kind: Service
metadata:
  name: redis-service
  labels:
    app: redis
spec:
  selector:
    app: redis
  ports:
  - port: 80
    targetPort: 6379
    protocol: "TCP"
    name: redis
  type: LoadBalancer


kubectl apply -n redis -f redis-lb.yaml

6.1 檢查外部IP
部署負載均衡器後,幾分鐘後叢集將提供公共外部 IP。為了檢查在幾分鐘內執行以下命令。
kubectl get service -n redis

從本地訪問 Redis 叢集
對於此步驟,您應該必須在本地計算機上安裝 Node.js v16.15.0 或最新版本。此步驟是可選的,但為了檢查日誌/驗證連線性,最好有辦法通過redis-cli. 為此,您無需在本地計算機上安裝 Redis 伺服器。

1、安裝redis-cli
npm install -g redis-cli

2.訪問Redis叢集
一旦 npm 安裝成功,您可以redis-cli在終端上執行以下命令之後的任何支援的命令。
rdcli -h {host} -a {password} -p {port}

3.使用redis-cli監控Redis叢集
我將分享一些有用的命令來對 Redis 叢集進行基本級別的維護。您可以使用以下命令重新整理所有鍵並檢查叢集上儲存的值。

3.1檢查Redis叢集中儲存的值

rdcli -h {ip} -a {password} -p {port}
xxx.xxx.xxx.xxx:xx> KEYS *



3.2 在 Redis 叢集上重新整理快取
rdcli -h {ip} -a {password} -p {port} FLUSHALL


實現 Springboot 應用程式
現在我們要實現具有分散式快取功能的 Springboot 應用程式。此應用程式包含一個 GET 服務返回字串值,但使用Thread.sleep. 讓我們看看分散式快取解決方案如何幫助我們克服這種緩慢。

1. 將以下依賴項新增到 POM

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

<dependency>
  <groupId>io.pivotal.cfenv</groupId>
  <artifactId>java-cfenv-boot</artifactId>
  <version>2.4.0</version>
</dependency>

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>



2.Redis配置

要訪問 Redis 遠端伺服器,我們需要在屬性檔案中新增一些屬性,並在根包上實現 RedisConfiguration 類。
application.properties 檔案

spring.redis.host=xxx.xxx.xxx.xxx
spring.redis.port=xx
spring.redis.password=xxxxxxxx

spring.cache.redis.time-to-live=10000


package com.booking;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfiguration {
    @Value("${spring.redis.host:xxx.xxx.xxx.xxx}")
    private String redisHost;

    @Value("${spring.redis.port:xx}")
    private int redisPort;

    @Value("${spring.redis.password:xxxxxxxx}")
    private String redisPassword;

    @Bean
    public JedisConnectionFactory redisConnectionFactory() {

        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost,
                redisPort);
        redisStandaloneConfiguration.setPassword(redisPassword);
        return new JedisConnectionFactory(redisStandaloneConfiguration);

    }

    @Bean
    public <T> RedisTemplate<String, T> redisTemplate() {
        RedisTemplate<String, T> redisTemplate = new RedisTemplate<String, T>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}



3. 建立 RESTful Web API
至此,我們討論了屬於遠端 Redis 叢集的配置。現在我們將實現一個典型的 Spring Boot Rest API,唯一的變化是我們在主類和服務類中提供了一些註釋來配置快取啟用。
BookingServiceApplication.java(主類)

package com.booking;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableCaching
public class BookingServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(BookingServiceApplication.class, args);
    }

}


BookingController.java(控制器類)

package com.booking.contraller;

import com.booking.model.Booking;
import com.booking.service.BookingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("booking")
public class BookingController {

    @Autowired
    private BookingService bookingService;

    @GetMapping
    public String getServiceDetails(@RequestParam(name = "scope", required = false, defaultValue = "N/A") String scope){
        return bookingService.getServiceDetails(scope);
    }

}


BookingService.java(服務類)

package com.booking.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class BookingService {

    @Cacheable("echoCacheWithParam")
    public String getServiceDetails(String level) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(level.equals("ALL")){
            return "Book Service v.1.0 \n Developed By Denuwan";
        }else {
            return "Book Service v.1.0";
        }

    }
}


curl -X GET \ http://localhost:8080/booking

一旦你完成了 spring boot Rest 服務的實現,你就可以啟動伺服器並嘗試上面的 GET 方法。由於Thread sleep. 但是當您嘗試第二次響應時,應該需要 < 1Seconds,因為響應過程來自 Redis 快取伺服器,而不是命中服務層。
另外,請注意,我們將快取時間設定為application.properties檔案中的實時屬性, 因此當您在 10 秒內再次執行 GET 請求時,您會注意到該請求再次從服務層處理並花費 > 5 秒。

原始碼:這裡

相關文章