分布式缓存综合指南:Kubernetes + Redis + Spring Boot


本文的范围是提供全面的指南,以在 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 秒。

源码:这里