Spring Boot中集成Redis与MySQL

1. 环境准备与依赖配置 

1.1 Maven 依赖管理

为了在 Spring Boot 项目中使用 Redis 和 MySQL,我们需要在 pom.xml 中添加必要的依赖。主要包括以下几个依赖:

  • Spring Data Redis:用于在 Spring Boot 中集成 Redis,提供 RedisTemplate 进行操作。
  • MySQL JDBC 驱动:用于连接 MySQL 数据库。
  • Spring Data JPA:用于简化与 MySQL 数据库的交互,提供面向对象的数据库操作支持。

具体依赖代码:

<!-- Spring Data Redis 依赖,用于集成 Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- MySQL JDBC 驱动,用于连接 MySQL 数据库 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- Spring Data JPA 依赖,用于简化数据库访问 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

添加完这些依赖后,Spring Boot 项目就可以利用 Spring Data Redis 来操作 Redis,并通过 Spring Data JPA 连接和操作 MySQL 数据库。

1.2. 配置文件:设置 application.properties 文件

Spring Boot 项目中通常使用 application.properties 文件来配置应用所需的参数。以下是 Redis 和 MySQL 的详细连接参数设置说明。

Redis 配置

# Redis 主机地址,默认情况下是 localhost,本地使用无需修改
spring.redis.host=localhost

# Redis 服务端口,默认 6379
spring.redis.port=6379

# Redis 密码,如果没有设置密码,可以留空
spring.redis.password=your_redis_password

配置说明:

  • spring.redis.host:Redis 服务器的主机地址,通常为 localhost(即本地)或 Redis 所在的服务器 IP。
  • spring.redis.port:Redis 服务器的端口号,默认是 6379,可以根据实际 Redis 配置进行调整。
  • spring.redis.password:Redis 的访问密码。如果 Redis 设置了密码保护(通常用于远程访问或安全性较高的场景),则在此处填写对应的密码。

MySQL 配置

# MySQL 数据库连接 URL,格式为:jdbc:mysql://[host]:[port]/[database_name]?参数
spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC

# MySQL 数据库用户名
spring.datasource.username=your_mysql_username

# MySQL 数据库用户密码
spring.datasource.password=your_mysql_password

# JDBC 驱动类名称,Spring Boot 2.x 及以上版本中使用 com.mysql.cj.jdbc.Driver
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA 和 Hibernate 配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

配置说明:

  • spring.datasource.url:数据库连接 URL。其格式为:
    • jdbc:mysql://:指定使用 MySQL 数据库的 JDBC 驱动。
    • [host]:MySQL 服务器主机地址,本地为 localhost,如果是远程则填写 IP 地址。
    • [port]:MySQL 服务端口号,默认是 3306,可以根据实际情况调整。
    • [database_name]:具体使用的数据库名称,需要在 MySQL 中提前创建好。
    • ?useSSL=false:指定是否启用 SSL 连接(一般本地开发设为 false)。
    • &serverTimezone=UTC:设置服务器的时区为 UTC,防止可能的时区问题。
  • spring.datasource.username:MySQL 数据库用户名,用于连接数据库。
  • spring.datasource.password:对应的用户名密码,确保输入正确。
  • spring.datasource.driver-class-name:指定 JDBC 驱动类。MySQL 使用 com.mysql.cj.jdbc.Driver(这是 MySQL 8.0 及以上版本的驱动类)。
  • spring.jpa.hibernate.ddl-auto=update:表示在应用启动时,自动根据实体类更新数据库表结构。
  • spring.jpa.show-sql=true:设置为 true 会打印 Hibernate 生成的 SQL 语句,便于调试。
  • spring.jpa.properties.hibernate.dialect:定义 Hibernate 使用的 MySQL 方言(MySQL8Dialect),这会优化 Hibernate 与 MySQL 的交互。

1.3. 本地与远程服务搭建

1.3.1. Redis 服务启动

(1)本地 Redis 启动

安装 Redis:首先,你需要在本地环境中安装 Redis。根据操作系统的不同,可以通过 apt-get(Linux)、brew(macOS)或直接下载 Redis 可执行文件来安装。

启动 Redis 服务

  • 安装完成后,可以通过命令行启动 Redis 服务:
redis-server
  • Redis 默认在 localhost6379 端口上监听请求。
  • 启动后,你可以通过 Redis 客户端(redis-cli)测试连接。在终端输入以下命令:
redis-cli
  • 然后执行 PING 命令。如果返回 PONG,表示 Redis 已经正常启动。

(2)远程 Redis 服务器配置

安全性设置:如果 Redis 部署在远程服务器上,建议为 Redis 设置密码。在 Redis 的配置文件(通常是 redis.conf)中设置 requirepass your_password,然后重新启动服务以生效。

防火墙配置:确保 Redis 端口(默认 6379)在服务器的防火墙中开放,以允许远程访问。

网络连接测试

  • 确保你的应用主机能够通过 IP 地址和端口访问远程 Redis 服务器,可以在命令行测试连接:
redis-cli -h <远程IP> -p 6379 -a <密码>
  • 如果连接成功,执行 PING 返回 PONG,则说明网络连接和认证都已正确配置。

(3)Redis 的远程访问限制

  • 为了安全起见,Redis 的配置文件中默认限制了公网访问。可以在 redis.conf 文件中设置 bind 0.0.0.0 以允许所有 IP 连接 Redis。
  • 也可以在同一文件中设置 protected-mode yes 以启用受保护模式,如果不打算开放公网访问,可以禁用远程访问。

1.3.2. MySQL 服务启动

(1)本地 MySQL 启动

安装 MySQL:根据操作系统不同,可以通过 apt-getyumbrew 或下载安装包来安装 MySQL。

启动 MySQL 服务

  • 安装后,启动 MySQL 服务。在不同系统上启动 MySQL 的命令可能不同,如在 Linux 中:
sudo service mysql start
  • 使用以下命令行测试 MySQL 是否正常启动并连接:
mysql -u root -p
  • 输入密码后,如果成功进入 MySQL 命令行界面,表示 MySQL 已正常运行。

(2)远程 MySQL 服务器配置

1.配置远程访问权限:默认情况下,MySQL 只允许本地连接。要开放远程访问权限,你需要修改 MySQL 的用户权限。在 MySQL 控制台执行以下命令:

CREATE USER 'your_user'@'%' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON your_database.* TO 'your_user'@'%';
FLUSH PRIVILEGES;

2.配置文件修改:修改 MySQL 配置文件(通常是 my.cnfmy.ini),找到 [mysqld] 配置块,设置 bind-address0.0.0.0 以允许所有 IP 地址访问:

[mysqld]
bind-address = 0.0.0.0

重新启动 MySQL 使配置生效。

3.防火墙配置:确保服务器上开放 MySQL 的默认端口(3306),允许远程访问。

4.测试远程连接

使用以下命令测试远程连接(在另一台主机上):

mysql -h <远程IP> -P 3306 -u your_user -p

如果成功连接,表示 MySQL 的远程访问配置正确。

1.3.3. 网络连通性和认证设置

网络连通性测试:从应用主机上测试到 Redis 和 MySQL 服务的连通性。可以使用 pingtelnet 命令检查 IP 和端口的连接状态:

# 测试 Redis 连接
telnet <redis_ip> 6379

# 测试 MySQL 连接
telnet <mysql_ip> 3306

如果连接成功说明网络连通性没有问题。

认证配置

  • 确保 Redis 和 MySQL 的认证信息(如用户名、密码)正确配置在 Spring Boot 项目中,并且可以成功访问。
  • 对于 Redis,使用密码认证的配置项是 spring.redis.password
  • 对于 MySQL,认证配置项包括 spring.datasource.usernamespring.datasource.password

2.Spring Boot 与 Redis 的集成

2.1. RedisTemplate 配置

RedisTemplate 是一个通用的模板类,适用于操作 Redis 中的多种数据结构,包括字符串、哈希、列表、集合等。我们可以通过 RedisTemplate 来方便地执行 Redis 的增删查改操作。为了确保数据的可读性和兼容性,我们通常需要自定义 RedisTemplate 的序列化配置。

你的 Redis 配置中,GenericJackson2JsonRedisSerializer 作为 value 和 hashValue 的序列化器,它的行为如下:

  • 如果 value 不是 String 类型(例如 Integer, Long, List, Map, Object 等),会被序列化为 JSON 格式存入 Redis。
  • 如果 valueString 类型,Redis 不会额外再做 JSON 序列化,而是 直接存入 Redis(与 StringRedisSerializer 的效果相同)。

(1)创建 RedisTemplate Bean

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.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 创建 RedisTemplate 实例
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        
        // 设置连接工厂
        template.setConnectionFactory(redisConnectionFactory);

        // 配置 key 的序列化器
        template.setKeySerializer(new StringRedisSerializer());

        // 配置 value 的序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 配置 hash key 的序列化器
        template.setHashKeySerializer(new StringRedisSerializer());

        // 配置 hash value 的序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 初始化 RedisTemplate 配置
        template.afterPropertiesSet();
        
        return template;
    }
}
  • StringRedisSerializer

    • 用途:将键或简单的值序列化为字符串。
    • 适用场景:通常用于键的序列化,确保键在 Redis 中以字符串存储,以便于直接查看和管理。
  • GenericJackson2JsonRedisSerializer

    • 用途:将对象序列化为 JSON 格式的字符串,并支持 JSON 反序列化回对象。
    • 适用场景:通常用于值的序列化,尤其是需要存储复杂对象的情况。它可以确保数据的可读性,且 JSON 格式数据跨系统兼容性好。

(2)创建 StringRedisTemplate Bean

StringRedisTemplateRedisTemplate 的一个变种,它专门用于存储字符串数据。由于它默认的键和值的序列化方式都是 StringRedisSerializer,可以直接用于存储和读取字符串数据。

StringRedisTemplate 配置代码

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
public class RedisConfig {

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

配置解析

  • StringRedisTemplate 继承自 RedisTemplate,主要用于操作 Redis 中的字符串类型数据。
  • StringRedisTemplate 的键和值的序列化方式都是 StringRedisSerializer,这使得所有存储在 Redis 中的数据都是可读的字符串格式。
  • 适用于数据都是简单字符串的场景,由于 StringRedisTemplate 默认的序列化方式已满足大多数字符串操作的需求,不需要额外配置序列化器。
  • 注册为 Spring Bean:同样地,@Bean 注解将 StringRedisTemplate 注册为 Spring 容器中的一个 Bean,其他类可以使用 @Autowired 注入 StringRedisTemplate 实例,进行字符串数据的操作。

2.2 RedisTemplate 常用方法

通用方法

除了特定数据结构的 opsFor...() 方法,RedisTemplate 还提供了一些通用的 Redis 操作方法:

  • delete(String key):删除键 key 及其对应的值,返回是否成功删除。
  • expire(String key, long timeout, TimeUnit unit):设置键 key 的过期时间。
  • hasKey(String key):检查键 key 是否存在,返回 truefalse
  • keys(String pattern):获取所有符合模式 pattern 的键。
  • persist(String key):移除键 key 的过期时间,使其永久有效。
  • rename(String oldKey, String newKey):将键 oldKey 重命名为 newKey
  • type(String key):返回键 key 的数据类型。

2.2.1. opsForValue() - 操作字符串(String)类型的数据

opsForValue() 用于操作 Redis 中的字符串数据,适用于简单的键值对操作。

常用方法

  • set(String key, Object value):将键 key 的值设置为 value,如果键已存在,则覆盖旧值。这个set方法不指定过期时间会永久保存。

  • set(String key, Object value, long timeout, TimeUnit unit):将键 key 的值设置为 value,并指定过期时间 timeout 和时间单位 unit

  • get(String key):获取键 key 对应的值。

  • increment(String key, long delta):将键 key 的值增加 delta,返回增加后的值(适用于整数类型的值)。

  • decrement(String key, long delta):将键 key 的值减少 delta,返回减少后的值。

2.2.2. opsForHash() - 操作哈希(Hash)类型的数据

opsForHash() 用于操作 Redis 中的 Hash 数据结构,Hash 结构是一个键值对集合,每个键值对被称为一个字段(field)。这种结构适用于存储和管理对象数据,可以把对象的属性当作字段,进行方便的增删查改。

 在 Spring Data Redis 中,BoundHashOperations<K, HK, HV> 是一个接口,用于 绑定 Redis 的哈希(Hash)数据结构,并提供对该哈希结构的各种操作。

BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(Key);
泛型参数作用你这里的具体类型
KStringRedis 的键(Key)类型,用于存储 Hash 结构的键CartConstant.CART_PREFIX+userKey(购物车的 Redis Key)
HKObjectHash 结构的字段(Hash Key)类型购物车商品 ID(或者其他字段)
HVObjectHash 结构的值(Hash Value)类型购物车数据的 JSON 字符串
BoundHashOperations 是一种对特定哈希的封装,它提供了一些对特定哈希键的操作,但 opsForHash() 已经足够处理常规的哈希操作。

opsForHash() 方法

opsForHash()RedisTemplate 中的一个方法,用于操作 Redis 哈希类型的数据。它返回一个 HashOperations 接口实例,允许你执行多种常见的哈希操作。

常用方法:

  • put(K key, HK hashKey, HV value)将一个键值对(hashKeyvalue)插入到 Redis 哈希中。

  • putAll(K key, Map<? extends HK, ? extends HV> map)将多个键值对一次性插入到 Redis 哈希中。

  • get(K key, HK hashKey)根据哈希键获取值。

  • delete(K key, Object... hashKeys)删除 Redis 哈希中的一个或多个哈希键。

  • entries(K key)获取整个哈希中的所有键值对。

// 使用 opsForHash() 操作 Redis 哈希结构
Map<String, String> map = new HashMap<>();
map.put("field1", "value1");
map.put("field2", "value2");

// 将整个 map 存入 Redis 哈希
redisTemplate.opsForHash().putAll("myHashKey", map);

// 获取某个字段的值
String value = (String) redisTemplate.opsForHash().get("myHashKey", "field1");

BoundHashOperations

BoundHashOperations 是对 Redis 哈希结构的封装,它与 opsForHash() 不同,它专注于某个特定哈希键。这种封装让你能够对特定的哈希结构进行更细粒度的操作。BoundHashOperations 使得某个特定的哈希可以在多个操作中被绑定和复用。

常用方法:

  • put( HK hashKey, HV value) :设置哈希表中指定字段的值,如果字段存在则覆盖。

  • putAll(Map<? extends HK, ? extends HV> map) :批量将 Map 数据存入哈希表中。

  • putIfAbsent(HK hashKey, HV value) :仅在字段不存在时设置值。

  • get( Object hashKey) :获取哈希表中指定字段的值。

  • multiGet(Collection<HK> hashKeys) :批量获取哈希表中多个字段的值。

  • delete(Object... hashKeys) :删除哈希表中的一个或多个字段。

  • hasKey( Object hashKey) :判断哈希表中是否存在指定字段。

  • size(String key) :获取哈希表中字段的数量。

  • entries(String key) :获取哈希表中的所有键值对,以 Map 形式返回。

  • keys(String key) :获取哈希表中的所有字段名。

  • values(String key) :获取哈希表中的所有字段值。

  • increment(String key, HK hashKey, long delta) :对哈希表中指定字段的数值进行原子性递增或递减。

// 通过 BoundHashOperations 操作特定哈希键
BoundHashOperations<String, String, String> boundHashOps = redisTemplate.boundHashOps("myHashKey");

// 向绑定的哈希结构中添加一个字段
boundHashOps.put("field1", "value1");

// 获取绑定哈希中某个字段的值
String value = boundHashOps.get("field1");

// 删除绑定哈希中的一个字段
boundHashOps.delete("field1");

2.2.3. opsForList() - 操作列表(List)类型的数据

quicklist 是 Redis 从 3.2 版本起,用来实现 List 类型的新数据结构。

它是 Redis 为了兼顾节省内存、提升性能而设计的一种结构,本质上是一个“双向链表,每个节点是一个压缩列表(ziplist)”

quicklist 的底层结构组成:

(1)quicklist 节点(quicklistNode):

每个节点包含:

  • 一个 ziplist(压缩列表):可以包含多个 list 元素。

  • 前驱/后继指针:形成双向链表。

  • 元素个数、ziplist 总字节数等元信息。

(2)ziplist(压缩列表):

  • 是一种紧凑的连续内存结构,类似数组。

  • 用于节省内存。

  • 存储的是具体的字符串值或整数值。

opsForList() 用于操作 Redis 中的列表结构,列表数据类型可以存储一个有序的字符串列表(支持从左、右两端插入和弹出)。

常用方法

  • 正数索引(从左到右):

    • 0 表示列表的第一个元素(最左侧)。

    • 1 表示第二个元素,依此类推。

    • 如果索引超过列表长度,返回 null(或抛出异常,取决于客户端)。

  • 负数索引(从右到左):

    • -1 表示列表的最后一个元素(最右侧)。

    • -2 表示倒数第二个元素,依此类推。

    • 如果负数索引超出范围(如列表长度为 3,但使用 -4),返回 null

  • leftPush(String key, Object value):将 value 插入到列表 key 的左侧。
  • rightPush(String key, Object value):将 value 插入到列表 key 的右侧。
  • leftPop(String key):移除并返回列表 key 的左侧第一个元素。
  • rightPop(String key):移除并返回列表 key 的右侧第一个元素。
  • index(String key, long index)获取列表 key 中指定索引位置的元素(支持负数索引,如 -1 表示末尾元素)。

  • range(String key, long start, long end):获取列表 key 中从 startend 范围内的元素。redis的约定是0表示列表第1个元素,-1表示列表末尾元素
  • size(String key):获取列表 key 的长度。
  • set(String key, long index, Object value):将列表 key 中指定索引 index 的元素设置为 value

2.2.4. opsForSet() - 操作集合(Set)类型的数据

opsForSet() 用于操作 Redis 中的集合结构,集合中的元素是唯一的,且无序。

常用方法

  • add(String key, Object... values):向集合 key 中添加一个或多个 values,返回添加的元素数量。
  • remove(String key, Object... values):从集合 key 中移除一个或多个元素。
  • members(String key):获取集合 key 中的所有元素。
  • isMember(String key, Object value):判断 value 是否是集合 key 的成员。
  • size(String key):获取集合 key 的元素个数。
  • intersect(String key, String otherKey):返回集合 keyotherKey 的交集。
  • difference(String key, String otherKey):返回集合 keyotherKey 的差集。
  • union(String key, String otherKey):返回集合 keyotherKey 的并集。

2.2.5. opsForZSet() - 操作有序集合(Sorted Set/ZSet)类型的数据

opsForZSet() 用于操作 Redis 中的有序集合结构。有序集合中的每个元素都关联一个分数,按分数从小到大排序。

常用方法

  • add(String key, Object value, double score):将 value 添加到有序集合 key 中,并设置分数 score
  • remove(String key, Object... values):从有序集合 key 中移除一个或多个元素。
  • score(String key, Object value):获取有序集合 key 中元素 value 的分数。
  • Double incrementScore(String key, Object member, double delta):对指定成员的 score 进行原子性增减。delta作为分数增量(可正可负,支持小数),正就是增,负就是键。
  • size(String key):获取有序集合 key 的元素个数。
  • rank(String key, Object value):返回有序集合 key 中元素 value 的排名(从小到大)。
  • reverseRank(String key, Object value):返回有序集合 key 中元素 value 的排名(从大到小)。
  • range(String key, long start, long end):根据索引范围 startend 获取有序集合 key 中的元素(按照升序排序好的,start对应分数最小的)。这里返回的是一个Set(准确来说是LinkedHashSet),Set里面存放的是你存的元素。reverseRange(String key, long start, long end)是降序。
  • rangeByScore(String key, double min, double max):根据分数范围 minmax 获取有序集合 key 中的元素(按照升序排序好的,min对应最小的分数)。reverseRangeByScore(String key, double max, double min)是降序
  • Long count(K key, double min, double max);用于统计 有序集合分数(score) 在给定的最小值(min)和最大值(max)之间的成员数量。
  • reverseRangeByScoreWithScores(String key, double min, double max):返回指定分数范围内的元素,并且按分数从高到低排序。返回一个Set<ZSetOperations.TypedTuple<V>>类型的集合,集合中的每个元素包含了ZSet中的一个成员和它的分数。

ZSetOperations.TypedTuple<V>

  • 这个类型包含了ZSet集合中的元素以及它对应的分数。你可以通过getValue()方法获取元素,通过getScore()方法获取元素的分数。

2.2.6. Bitmaps(位图)和 HyperLogLog 和 Geo(地理位置)和 Streams(流数据)

(1)Bitmaps(位图)

通过 opsForValue() 来操作,是 String 类型数据的一种特殊应用。位图(Bitmap)是通过对字符串类型的位进行操作来表示和管理大量的二进制数据。例如,你可以将一个比特位表示为布尔值(truefalse),并且对其进行操作。

常用方法:

  • setBit(String key, long offset, boolean value):设置 key 对应字符串中指定偏移量(offset)的位的值,valuetruefalse
  • 当你首次通过 setBit 方法设置某个位置的位时,Redis 会自动扩展该字符串的长度以适应你设置的位。因此,在初次设置时,你并不需要事先知道该字符串已经包含多少位。Redis 会根据设置的 offset 自动调整字符串的长度(通过在需要时添加零位):如果 offset 超过了当前字符串的最大可设置位(即当前字节数 * 8),Redis 会扩展字符串并在新增的字节中填充零,直到能够容纳该 offset 所指向的位置。

  • getBit(String key, long offset):获取 key 对应字符串中指定偏移量(offset)的位的值。
  • List<Long> bitField(K key, BitFieldSubCommands subCommands):它通过位偏移量来获取或设置字符串中某些位的值。bitField 允许你在 一个操作 中批量获取或设置多个位字段。
  • BitFieldSubCommands 是 RedisTemplate 提供的一个用于构建位字段子命令的类。它允许你创建不同的位操作,如获取(get)、设置(set)、和改变值等。valueAt(offset) 表示从 Redis 字符串值的第 offset 位开始读取。
  • bitCount(String key):返回 key 中比特位为 1 的个数。
  • bitOp(BitOperation op, String destKey, String... keys):执行位操作(如 AND、OR、XOR),对多个 key 执行按位操作并将结果存储到 destKey

(2)HyperLogLog

通过 opsForValue() 来操作,是 String 类型数据的一种特殊应用。HyperLogLog 是一种用于 基数估算 的数据结构,能够快速估算不重复元素的数量,并且占用很小的内存空间。

常用方法:

  • pfAdd(String key, Object... values):将一个或多个值加入到 HyperLogLog 中。
  • pfCount(String... keys):返回一个或多个 HyperLogLog 的基数估算值。
  • pfMerge(String destKey, String... sourceKeys):将多个 HyperLogLog 合并到一个目标 HyperLogLog 中。

(3)Geo(地理位置)

opsForGeo():操作 Geo(地理位置) 类型,支持基于经纬度进行操作。Geo 是 Redis 用于存储和查询地理位置的专用数据结构。它支持通过经纬度信息来查询和处理地理数据。

常用方法:

  • add(String key, Point point, Object... values):将指定的经纬度(point)和成员值加入到地理位置数据中。
  • radius(String key, Point point, double radius):根据给定的经纬度和半径查询范围内的成员。
  • distance(String key, Object member1, Object member2):计算两个地理位置成员之间的距离。
  • position(String key, Object... values):获取一个或多个成员的经纬度信息。

(4)Streams(流数据)

opsForStream():操作 Stream 类型,支持流数据操作。Streams 是 Redis 5.0 引入的一种新的数据结构,用于处理实时数据流。它可以作为消息队列使用,并且支持高级特性,如消费者组和消息确认等。

常用方法:

  • add(String key, Map<String, String> fields):将一个消息添加到 Stream。
  • range(String key, Range range):获取指定范围内的消息。
  • read(String key, StreamOffset<String> offset):从指定的 Stream 中读取消息。
  • acknowledge(String key, String group, String... messageIds):确认消费的消息。

2.3.RedisTemplate 中的 Lua 脚本执行方法

方法使用场景
execute(RedisScript<T> script, List<K> keys, Object... args)适用于 简单的字符串、数值操作
execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args)适用于 复杂数据类型(JSON、对象)

2.3.1. DefaultRedisScript<T> 

(1)什么是 DefaultRedisScript<T>

DefaultRedisScript<T> 是 Spring Data Redis 提供的一个类,用于封装 Lua 脚本并指定脚本的返回类型。它是 RedisScript<T> 接口的默认实现类。

  • 作用:将 Lua 脚本包装成一个可执行的对象,并指定脚本的返回类型。

  • 泛型 T:表示脚本执行结果的类型,例如 StringLongList 等。

(2)DefaultRedisScript<T> 的构造方法

DefaultRedisScript<T> 提供了以下常用的构造方法:

  • 无参构造方法

    DefaultRedisScript<T> script = new DefaultRedisScript<>();

    需要手动设置脚本内容和返回类型。

  • 带脚本内容和返回类型的构造方法

    DefaultRedisScript<T> script = new DefaultRedisScript<>(luaScript, resultType);
    • luaScript:Lua 脚本内容(字符串)。

    • resultType:脚本返回值的类型(Class<T>)。

(3)DefaultRedisScript<T> 的常用方法

  • setScriptText(String scriptText):设置 Lua 脚本内容。

  • setResultType(Class<T> resultType):设置脚本返回值的类型。

  • getScriptAsString():获取 Lua 脚本内容

  • getResultType():获取脚本返回值的类型

(4)DefaultRedisScript<T> 的使用示例

import org.springframework.data.redis.core.script.DefaultRedisScript;

public class DefaultRedisScriptExample {

    public static void main(String[] args) {
        // 1. 创建 DefaultRedisScript 对象
        DefaultRedisScript<String> script = new DefaultRedisScript<>();

        // 2. 设置 Lua 脚本内容
        script.setScriptText("return redis.call('get', KEYS[1])");

        // 3. 设置脚本返回类型
        script.setResultType(String.class);

        // 4. 获取脚本内容和返回类型
        System.out.println("Script: " + script.getScriptAsString());
        System.out.println("Result Type: " + script.getResultType());
    }
}

2.3.2. execute(RedisScript<T> script, List<K> keys, Object... args) 详解

(1)方法说明

  • 作用:执行 Lua 脚本,并返回脚本的执行结果。

  • 参数

    • scriptRedisScript<T> 对象,表示要执行的 Lua 脚本。

    • keysList<K> 类型,表示脚本中使用的 Redis 键(对应 Lua 脚本中的 KEYS 表)。

    • args:可变参数,表示传递给脚本的参数(对应 Lua 脚本中的 ARGV 表)。

  • 返回值:脚本的执行结果,类型由 RedisScript<T> 的泛型 T 决定。

在 Spring Data Redis 的 redisTemplate.execute() 方法中,所有 Redis Lua 脚本所操作的 key(即 KEYS 列表)都必须传递为 List<String> 类型,而 Collections.singletonList(key)创建单个元素的 List 的一种简洁方式。

另一种创建 List 的方式是:Arrays.asList(key)

Collections.singletonList() 相比 Arrays.asList() 更高效

  • Collections.singletonList() 返回的是固定大小的 List,更轻量。
  • Arrays.asList() 返回的 List 仍然是基于数组的包装类,性能稍逊色。
  • 如果只需要一个 key,singletonList() 是最佳选择

(2)使用步骤

  1. 定义 Lua 脚本。

  2. 创建 DefaultRedisScript<T> 对象,并设置脚本内容和返回类型。

  3. 调用 execute 方法,传入脚本、键和参数。

(3)示例代码

示例 1:原子递增

// 创建 Lua 脚本
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText("return redis.call('incrby', KEYS[1], ARGV[1])");
script.setResultType(Long.class);

// 执行 Lua 脚本
Long result = redisTemplate.execute(script, Collections.singletonList("counter"), 10);

System.out.println("计数器值:" + result);
  • counter 是 Redis key。
  • ARGV[1] = 10 表示 递增 10
  • 返回 新的计数器值

 示例 2:防重令牌(去重操作)

DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end");
script.setResultType(Long.class);

// 订单防重令牌
Long result = redisTemplate.execute(script, Collections.singletonList("order:token:12345"), "token123");

if (result == 1) {
    System.out.println("防重令牌校验成功");
} else {
    System.out.println("防重令牌无效或已被使用");
}
  • 如果 Redis 存储的 令牌KEYS[1])和 用户提交的令牌ARGV[1])相等,则删除令牌并返回 1
  • 如果不匹配,则返回 0,表示令牌无效。

2.3.3. execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) 详解

(1)方法说明

  • 作用:执行 Lua 脚本,并支持自定义参数的序列化和结果的序列化。

  • 参数

    • scriptRedisScript<T> 对象,表示要执行的 Lua 脚本。

    • argsSerializer:用于序列化脚本参数的序列化器(比如 StringRedisSerializer)。。

    • resultSerializer:用于反序列化脚本结果的序列化器(比如 Jackson2JsonRedisSerializer<T>)。

    • keysList<K> 类型,表示脚本中使用的 Redis 键。

    • args:可变参数,表示传递给脚本的参数。

  • 返回值:脚本的执行结果,类型由 RedisScript<T> 的泛型 T 决定。

(2)使用步骤

  1. 定义 Lua 脚本。

  2. 创建 DefaultRedisScript<T> 对象,并设置脚本内容和返回类型。

  3. 调用 execute 方法,传入脚本、序列化器、键和参数。

(3)示例代码

示例 1:存储 JSON 并查询

// 创建 Lua 脚本
DefaultRedisScript<String> script = new DefaultRedisScript<>();
script.setScriptText("return redis.call('get', KEYS[1])");
script.setResultType(String.class);

// 定义序列化器
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();

// 存储 JSON 数据
redisTemplate.opsForValue().set("user:1001", "{ \"name\": \"张三\", \"age\": 25 }");

// 执行 Lua 脚本获取数据
String userData = redisTemplate.execute(script, stringSerializer, stringSerializer, Collections.singletonList("user:1001"));

System.out.println("查询到的用户数据:" + userData);
  • 使用 StringRedisSerializer 处理 JSON 字符串,保证数据完整性。
  • get user:1001 获取存储的 JSON 数据。

2.4.Caffeine本地缓存的使用

Caffeine 是一个高性能、本地内存缓存库,专门用于 Java 应用程序。

它就像你在程序中加了一个小型的“内存数据库”,用来临时保存经常用到的数据(比如热门商品、字典项等),避免频繁访问数据库或外部接口。

Caffeine 和 Redis 的区别

对比点Caffeine(本地缓存)Redis(远程缓存)
📍存储位置JVM 本地内存(Heap)Redis 服务端(内存+网络)
🚀访问速度非常快(纳秒级)快(微秒级)但需网络通信
👥共享性仅当前应用可见(单节点)多节点共享(分布式)
🧠功能专注本地缓存分布式缓存、消息队列、锁等多功能
💥容错性JVM 崩溃则缓存消失Redis 重启可配置持久化
📊性能瓶颈受限于 JVM 内存大小可横向扩展 Redis 集群
🛠使用复杂度简单(引入库即用)需部署 Redis 服务、网络配置等
🔁适用场景热点数据、本地缓存、低延迟访问共享缓存、分布式系统、跨服务缓存

添加依赖

<!-- pom.xml 中添加 Caffeine 依赖 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

<!-- Spring Boot Cache 支持(通常已有) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

在主类或配置类加上注解

@SpringBootApplication
@EnableCaching  // 开启缓存功能
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

自定义缓存配置类

如果需要为不同缓存区域(如usersproducts)设置不同的策略:

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 默认全局配置(可被具体缓存覆盖)
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .recordStats());

        // 为特定缓存自定义配置
        cacheManager.registerCustomCache("users", Caffeine.newBuilder()
                .maximumSize(500)
                .expireAfterWrite(1, TimeUnit.HOURS)
                .build());
        return cacheManager;
    }

Caffeine.newBuilder()链式调用中可配置:

​Caffeine.newBuilder()
    .initialCapacity(100)    // 初始容量
    .maximumSize(1000)       // 最大条目数
    .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后过期时间
    .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后过期时间
    .refreshAfterWrite(1, TimeUnit.MINUTES) // 写入后自动刷新(需AsyncCache)
    .recordStats();          // 开启统计(命中率等)

监控缓存状态

通过Cache对象获取统计信息:

import com.github.benmanes.caffeine.cache.stats.CacheStats;
import org.springframework.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;

public class CacheMonitor {

    @Autowired
    private CacheManager cacheManager;

    public void printStats() {
        CaffeineCache caffeineCache = (CaffeineCache) cacheManager.getCache("users");
        com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache = 
            caffeineCache.getNativeCache();
        
        CacheStats stats = nativeCache.stats();
        System.out.println("命中率: " + stats.hitRate());
        System.out.println("平均加载时间: " + stats.averageLoadPenalty() + "ns");
    }
}

2.4.1. @Cacheable 注解

@Cacheable 注解用于将方法的返回值缓存起来。下次调用该方法时,如果参数相同,Spring 会直接从缓存中返回结果,而不再执行方法体。这可以减少对数据库或外部服务的重复访问,提升性能。

使用方法

@Cacheable(value = "userCache", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
    System.out.println("Executing getUserById for id: " + id);
    return findUserInDatabase(id); // 模拟数据库查询
}

参数详解

  • value:指定缓存区域的名称,相当于缓存的命名空间或前缀(例如 userCache)。所有 userCache 缓存区的键都会以 userCache:: 开头。

  • key:指定缓存的键。使用 #参数名 来引用方法的参数,如 #id 表示将 id 参数作为缓存键的一部分。最终 Redis 中的键可能是 userCache::1(假设 id=1)。

  • unless(可选):指定条件,当条件成立时不缓存结果。例如,unless = "#result == null" 表示如果返回值为 null,则不缓存。

缓存的工作原理

  • 第一次调用:当调用 getResourceSet("123") 时,Spring 会检查缓存区域 "resource" 中是否存在键为 123 的缓存数据。如果不存在,则执行方法并将返回的 Set<String> 存入缓存。
  • 后续调用:当再次调用 getResourceSet("123") 时,Spring 会直接从缓存中获取数据,而不会执行方法。


2.4.2. @CachePut 注解

@CachePut 注解用于更新缓存数据,与 @Cacheable 不同的是,@CachePut 每次都会执行方法体,并将返回值更新到缓存中。它主要用于在更新数据的同时刷新缓存。

使用方法

@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
    System.out.println("Executing updateUser for id: " + user.getId());
    return updateUserInDatabase(user); // 模拟更新数据库
}

参数详解

  • value:指定缓存区域名称,通常为与查询操作相同的命名空间(例如 userCache)。

  • key:指定缓存的键,通常是方法参数的某个属性,如 #user.id 表示将 user 对象的 id 作为缓存键。

应用场景

  • 数据更新操作:适用于更新数据库信息的场景。在数据库更新成功后,@CachePut 会自动更新缓存中的数据,保证缓存和数据库的数据一致性。

  • 缓存同步@CachePut 保证每次都更新缓存,适合在需要同步缓存与数据库的情况下使用。


2.4.3. @CacheEvict 注解

@CacheEvict 注解用于清除缓存中的数据。可以指定删除特定的缓存项,也可以通过配置 allEntries 参数清空整个缓存区域。

使用方法

@CacheEvict(value = "userCache", key = "#id", beforeInvocation = true)
public void deleteUserById(Long id) {
    System.out.println("Executing deleteUserById for id: " + id);
    deleteUserFromDatabase(id); // 模拟从数据库中删除用户
}

参数详解

  • value:指定缓存区域的名称(如 userCache)。

  • key:指定要清除的缓存项的键。例如,key = "#id" 表示删除 userCache 中以 id 为键的缓存项。

  • allEntries(可选):当设置为 true 时,清除整个缓存区域内的所有缓存项。默认为 false

  • beforeInvocation(可选):当设置为 true 时,在方法执行前清除缓存。默认值是 false,即方法成功执行后再清除缓存。这在方法可能抛出异常时非常有用,避免方法失败导致缓存不一致。

应用场景

  • 数据删除操作:适用于在从数据库删除数据时,确保清除对应的缓存项。

  • 清空缓存allEntries = true 时,适用于清空整个缓存区域。例如,定期清空用户缓存区中的所有数据,以确保数据的时效性。

  • 防止缓存不一致beforeInvocation = true 可以在方法执行前清除缓存,适合可能因异常导致数据不一致的场景。

3. 数据库与 Redis 的数据一致性保障

选择缓存数据的关键考量

在选择数据缓存到 Redis 时,需要平衡以下几个方面:

  • 数据访问的频率:高频访问的数据更适合缓存,因为缓存可以显著降低频繁访问对数据库的负担。
  • 数据更新的频率:频繁更新的数据可能不适合缓存,除非有专门的机制保持缓存数据和源数据的一致性。
  • 数据的大小和结构:缓存的大小需要考虑 Redis 的内存限制。对于特别大的数据或复杂的数据结构,缓存设计需要小心,以避免 Redis 内存不足。

对于缓存的更新也要预防多线程问题。

问题 1:数据库更新成功,Redis 更新失败(如 Redis 宕机、网络异常)

场景

  • 业务逻辑先更新数据库,然后更新 Redis
  • 数据库更新成功,但Redis 更新失败(如 Redis 宕机、网络异常)。
  • 这导致 Redis 仍然存储旧数据,下次读取时可能返回错误的数据。

方案 3.1.1:先更新数据库,再删除缓存

流程

  1. 先更新数据库
  2. 再删除 Redis 缓存
  3. 下次查询时,重新从数据库加载,并更新 Redis
@Transactional
public void updateData(Long id, String newValue) {
    // 1. 更新数据库
    dataMapper.updateValue(id, newValue);

    // 2. 更新 Redis
    redisTemplate.opsForValue().set("data:" + id, newValue, 10, TimeUnit.MINUTES);
}

优点

  • 读请求不会命中旧缓存,确保数据准确。
  • 不会有数据丢失风险(缓存删除失败时,仍然能从数据库查询)。

缺点

  • 存在 缓存瞬间失效 问题,高并发下可能导致大量请求直击数据库缓存击穿)。

方案 3.1.2:延迟双删策略

流程

  1. 先删除 Redis 缓存
  2. 等待一定时间(如 500ms),再删除一次
  3. 期间如果有读请求,则从数据库加载并更新缓存
@Transactional
public void updateData(Long id, String newValue) {
    // 1. 先删除缓存
    redisTemplate.delete("data:" + id);

    // 2. 更新数据库
    dataMapper.updateValue(id, newValue);

    // 3. 延迟再删除一次,防止并发问题
    new Thread(() -> {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        redisTemplate.delete("data:" + id);
    }).start();
}

适用于:高并发场景,避免并发读写导致缓存脏数据。

方案 3.1.3 基于消息队列的最终一致性

  • 事务提交后,发送一条消息到消息队列(如 Kafka、RabbitMQ、RocketMQ)。
  • 监听者(消费者) 订阅该消息,异步更新 Redis。
  • 如果 Redis 更新失败,可通过重试机制(消息重试或补偿机制)重新同步。

问题 2:Redis 更新成功,数据库更新失败(事务回滚)

场景

  • 先更新 Redis,再更新数据库(有些业务为了提高读性能会这样做)。
  • 数据库更新失败(事务回滚、数据库宕机),Redis 已更新。
  • 这样 Redis 里是新数据,但数据库里仍然是旧数据,导致数据不一致。

方案 3.2.1:TCC(Try-Confirm-Cancel)模式

TCC(Try-Confirm-Cancel)分布式事务模式可以帮助解决Redis 和数据库事务不一致的问题,关键点在于 Cancel 逻辑可以补偿 Redis 操作

TCC 事务的执行流程

  • Try
    • 预先执行数据库和 Redis 操作,但不提交
  • Confirm
    • 如果事务执行成功,则提交数据库和 Redis 变更
  • Cancel
    • 如果事务失败,则回滚数据库,并撤销 Redis 变更

适用场景高并发订单、支付、库存等强一致性需求的业务

调用 TCC 事务

在业务层,可以这样调用:

List<Resource> resourceList = trySyncResourceFavoriteToDB();
try {
    confirmSyncResourceFavoriteToDB(resourceList);
} catch (Exception e) {
    cancelSyncResourceFavoriteToDB(resourceList); // 事务失败时回滚
}

TCC 事务的 Try 阶段 需要一个标记 “该数据尚未最终确认”pending 字段的作用就是:

  1. Try 阶段:预写数据库,但 只标记为 “待提交”,不算正式更新。
  2. Confirm 阶段:标记 pending = false,表示数据 确认成功
  3. Cancel 阶段:回滚 pending = true 的数据,避免错误的 favorite_count 被错误提交。

TCC的三个方法必需都单独持有@Transactional注解

  • TCC 要求每个阶段(Try、Confirm、Cancel)都能独立执行、独立回滚。如果把它们全都放在一个 @Transactional 方法中,一旦事务提交或回滚,就只会有“一个整体结果”,无法对不同阶段分别进行处理。

  • 调度/入口方法(这里是 syncResourceFavoriteToDB())放在一个类(例如 Scheduler 类)里,不加 @Transactional
  • Try / Confirm / Cancel 三个方法放在 另一个类(例如 FavoriteTccService),并且 分别加上 @Transactional
  • 这样,当调度类调用这三个有事务注解的方法时,就能触发 Spring AOP 去创建 独立的事务,从而实现 TCC 所需的多阶段提交/回滚。

✅ 1. Try 阶段(预执行数据库和 Redis 操作)

@Transactional
public List<Resource> trySyncResourceFavoriteToDB() {
    // 1. 获取 Redis 中的“待处理消息”集合,并移除(Try 阶段先拉后删)
    String luaScript =
            "local result = redis.call('SMEMBERS', KEYS[1]) " +
            "redis.call('DEL', KEYS[1]) " +
            "return result";
    List<Object> recordSet = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, List.class),
            Collections.singletonList(SystemConstants.PENDING_FAVORITE_MODIFY_KEY)
    );

    if (recordSet == null || recordSet.isEmpty()) {
        return Collections.emptyList(); // 没有待处理数据,直接返回
    }

    // 2. 解析 JSON 数据
    List<Map<String, Object>> recordList = recordSet.stream()
            .map(json -> JSON.parseObject((String) json, new TypeReference<Map<String, Object>>() {}))
            .toList();

    // 3. 分组存储 subjectId -> List<resourceId>
    Map<Long, List<Long>> subjectResourceMap = recordList.stream()
            .collect(Collectors.groupingBy(
                    r -> Long.parseLong(r.get("subjectId").toString()),
                    Collectors.mapping(r -> Long.parseLong(r.get("resourceId").toString()), Collectors.toList())
            ));

    List<Resource> resourceToUpdateList = new ArrayList<>();

    // 4. 预取 Redis 最新的 favorite_count
    for (Map.Entry<Long, List<Long>> entry : subjectResourceMap.entrySet()) {
        Long subjectId = entry.getKey();
        String subjectFavoriteKey = String.format(SystemConstants.SUBJECT_RESOURCES_FAVORITE_KEY, subjectId);

        for (Long resourceId : entry.getValue()) {
            Double favoriteCountDouble = redisTemplate.opsForZSet().score(subjectFavoriteKey, resourceId.toString());
            if (favoriteCountDouble == null) continue;

            int favoriteCount = favoriteCountDouble.intValue();

            // 预存数据库,标记 "待提交"
            Resource resource = new Resource();
            resource.setId(resourceId);
            resource.setFavoriteCount(favoriteCount);
            resource.setPending(true); // 额外字段,标记数据是“待确认”状态
            resourceToUpdateList.add(resource);
        }
    }

    if (!resourceToUpdateList.isEmpty()) {
        resourceService.updateBatchById(resourceToUpdateList); // 预写数据库,但未确认
    }

    return resourceToUpdateList; // 返回 Try 阶段数据,供 Confirm/Cancel 使用
}

✅ 2. Confirm 阶段(提交数据库变更)

@Transactional
public void confirmSyncResourceFavoriteToDB(List<Resource> resourceList) {
    if (resourceList == null || resourceList.isEmpty()) {
        return;
    }

    // 1. 取消 "待提交" 标记,并正式更新数据库
    for (Resource resource : resourceList) {
        resource.setPending(false); // 标记数据为“已确认”
    }
    resourceService.updateBatchById(resourceList);

    // 2. 清理 Redis 过时的 favorite_count 记录
    for (Resource resource : resourceList) {
        String subjectFavoriteKey = String.format(SystemConstants.SUBJECT_RESOURCES_FAVORITE_KEY, resource.getSubjectId());
        redisTemplate.opsForZSet().remove(subjectFavoriteKey, resource.getId().toString());
    }
}

✅ 3. Cancel 阶段(回滚数据库变更 & 恢复 Redis 记录)

@Transactional
public void cancelSyncResourceFavoriteToDB(List<Resource> resourceList) {
    if (resourceList == null || resourceList.isEmpty()) {
        return;
    }

    // 1. 回滚数据库变更(只回滚 “待提交” 状态的数据)
    for (Resource resource : resourceList) {
        resource.setFavoriteCount(null); // 取消变更
        resource.setPending(false); // 取消“待提交”标记
    }
    resourceService.updateBatchById(resourceList);

    // 2. 恢复 Redis 过期的数据(补偿)
    for (Resource resource : resourceList) {
        String subjectFavoriteKey = String.format(SystemConstants.SUBJECT_RESOURCES_FAVORITE_KEY, resource.getSubjectId());
        redisTemplate.opsForZSet().add(subjectFavoriteKey, resource.getId().toString(), resource.getFavoriteCount());
    }
}

问题 3:并发更新,导致缓存和数据库状态不一致

场景

  • 多个线程同时修改同一条数据,导致数据库和 Redis 状态不一致。

方案 3.3.1:Redis 分布式锁

加锁防止并发写冲突

public void updateData(Long id, String newValue) {
    String lockKey = "lock:data:" + id;
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

    if (Boolean.TRUE.equals(lock)) {
        try {
            dataMapper.updateValue(id, newValue);
            redisTemplate.delete("data:" + id);
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        throw new RuntimeException("更新中,请稍后再试");
    }
}

适用场景

  • 高并发写入,防止数据错乱。

方案 3.3.2:乐观锁(版本号)

public boolean updateData(Long id, String newValue, int version) {
    return dataMapper.updateWithVersion(id, newValue, version) > 0;
}

适用场景

  • 适用于高并发读写,不适用于高并发写入。

4. 分布式锁的实现

1. ReentrantLock(本地锁)

  • 机制:基于 JVM 内存的互斥锁,同一线程可重复获取锁(可重入)。

  • 适用场景

    • 单机应用内的线程同步(如避免并发修改共享资源)。

    • 无需跨进程或分布式环境的高性能场景。

  • 局限性

    • 无法跨 JVM,分布式环境下无效。

    • 无自动续期或超时机制(需手动避免死锁)。

2. RedisTemplate 实现的分布式锁

  • 机制:通过 Redis 的 SET key value NX PX 命令实现锁的获取与超时。

  • 核心问题

    • 原子性:需用 Lua 脚本确保“获取锁+设置超时”和“释放锁+校验持有者”的原子性。

    • 锁续期:若业务执行时间超过锁超时时间,需手动实现续期(如定时任务)。

    • 误删锁:需存储唯一标识(如线程 ID),释放时校验避免误删其他线程的锁。

3. RedissonClient 实现的分布式锁

     Redisson 是一个在 Redis 基础之上封装的高水平客户端,它不只是提供简单的操作 API,还提供各种基于 Redis 的分布式工具(如分布式 Map、分布式锁、分布式队列、AtomicLong 等)。

  • 可重入:同一个线程多次加锁不会被阻塞。
  • 自动续期:默认情况下,如果加锁线程还活着,锁的过期时间会被自动续期,避免锁意外过早过期。
  • 看门狗机制:通过“锁看门狗”监控锁的持有情况,使得在一定超时时间后能安全释放等。

同时,Redisson 在集群环境下也实现了比如 RedLock、主从模式、哨兵模式、Cluster 模式等多种部署方式。它还可以保证 pub/sub 通知,Redis 宕机或节点故障时能及时感知并做相应动作。

4.1.使用 SETNXEXPIRE 命令手动实现分布式锁 

1. 分布式锁的原理

Redis 的 SETNXEXPIRE 命令可以实现基础的分布式锁功能:

  • SETNX(SET if Not Exists):尝试设置一个键(代表锁),如果该键不存在,则设置成功,表示锁被成功获取;如果键已存在,表示锁被占用。
  • EXPIRE:为锁设置过期时间,确保锁在持有方崩溃或出错时能自动释放,避免死锁。

实现步骤如下:

  1. 获取锁:使用 SETNX 设置一个锁键。如果返回成功,表示当前客户端获取到锁;否则获取锁失败。
  2. 设置过期时间:在获取锁成功后,使用 EXPIRE 为锁设置一个过期时间,防止锁因意外情况无法释放。
  3. 释放锁:在操作完成后,客户端主动删除锁,释放资源。

2. 在 Spring Boot 中实现 Redis 分布式锁

我们可以通过 StringRedisTemplate 来操作 Redis,创建一个分布式锁服务类 RedisLockService,用于管理锁的获取和释放。

代码示例

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class RedisLockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String LOCK_KEY = "lock_key"; // 锁的键名称
    private static final int LOCK_EXPIRE = 10; // 锁的过期时间,10秒

    // 尝试获取锁
    public boolean tryLock() {
        // 使用 SETNX 命令尝试获取锁
        Boolean success = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "1");
        if (Boolean.TRUE.equals(success)) {
            // 获取锁成功后,使用 EXPIRE 设置锁的过期时间
            redisTemplate.expire(LOCK_KEY, LOCK_EXPIRE, TimeUnit.SECONDS);
            return true; // 返回 true 表示成功获取到锁
        }
        return false; // 获取锁失败,返回 false
    }

    // 释放锁
    public void unlock() {
        redisTemplate.delete(LOCK_KEY); // 删除锁键,释放锁资源
    }
}

代码详细解释

  • tryLock() 方法:用于尝试获取锁

    • setIfAbsent(LOCK_KEY, "1"):使用 SETNX 命令尝试设置锁。如果成功返回 true,表示当前客户端获取到锁。如果返回 false,表示锁已被占用。
    • expire(LOCK_KEY, LOCK_EXPIRE, TimeUnit.SECONDS):设置锁的过期时间为 10 秒,防止锁在客户端意外退出时长期占用。
  • unlock() 方法:用于释放锁

    • delete(LOCK_KEY):删除锁键,释放锁资源。确保操作完成后锁被释放,让其他客户端可以继续获取该锁。

3. 使用示例:库存扣减操作

假设我们有一个商品库存扣减的场景,为了防止多个请求同时扣减库存导致超卖问题,可以通过分布式锁来确保同一时间只有一个请求可以扣减库存。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class InventoryService {

    @Autowired
    private RedisLockService redisLockService;

    public String deductInventory(String productId) {
        // 尝试获取锁
        if (redisLockService.tryLock()) {
            try {
                // 执行扣减库存的逻辑
                boolean success = reduceStock(productId);
                return success ? "扣减成功" : "库存不足";
            } finally {
                // 确保操作完成后释放锁
                redisLockService.unlock();
            }
        } else {
            return "请稍后再试"; // 如果获取锁失败,提示用户稍后重试
        }
    }

    private boolean reduceStock(String productId) {
        System.out.println("扣减库存,产品ID: " + productId);
        return true; // 假设库存扣减成功
    }
}

使用示例的代码解释

  • tryLock():调用 tryLock() 方法尝试获取锁,如果成功,则执行扣减库存操作。
  • 业务逻辑:调用 reduceStock(productId) 方法执行库存扣减操作,确保在持有锁的情况下完成操作。
  • 释放锁:操作完成后无论成功与否,调用 unlock() 方法释放锁。

4. 锁的有效期与安全性

由于 SETNXEXPIRE 不是原子操作,存在并发情况下锁的过期时间可能未设置的问题。虽然可以通过 Lua 脚本来实现原子性,但这种实现较为复杂。为确保分布式锁的可靠性和安全性,通常建议在高并发场景中使用 Redisson 来简化操作和提升安全性。

4.2.使用Redisson的RLock实现分布式锁

Redisson 是一个 Redis 客户端,封装了 Redis 的锁机制,并提供了 RLock 接口来管理分布式锁。相比使用 SETNXEXPIRE 组合手动管理锁的方式,Redisson 的分布式锁具备以下优势:

  1. 自动续期:Redisson 的 RLock 支持锁的自动续期,确保在长时间持有锁时不意外失效。
  2. 可重入性:同一线程可以多次获取同一锁,不会造成死锁。
  3. 高可靠性:Redisson 提供了丰富的 API,可以灵活管理锁的超时和等待时间,适合高并发场景。
  • Redisson 的自动续期机制设计的核心是确保锁在“正常持有”时不会意外释放,但一旦持有锁的线程中断或崩溃,续期停止,锁在过期时间到达后自动释放。这样既保证了锁在正常情况下的稳定性,也确保了线程异常时的自动释放。 
  • 在业务逻辑中,可能会出现递归调用或嵌套调用的情况,即一个持有锁的方法在调用的过程中又尝试获取同一锁。支持可重入性后,同一线程在持有锁的情况下可以再次获取锁,不会发生阻塞或死锁
  • 在分布式系统中,业务逻辑通常分为多个层次,每一层可能都有自己的锁需求。可重入性使得在多个层次调用同一锁时不需要担心冲突,同一线程可以在各个层次持有同一把锁

4.2.1.使用步骤 

1. 引入 Redisson 依赖

首先,在 Spring Boot 项目中添加 Redisson 的依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.15.3</version> <!-- 请使用最新版本 -->
</dependency>

2. 配置 RedissonClient Bean

application.properties 文件中配置 Redis 的连接信息:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=yourpassword   # 如果没有设置密码,可以省略这一行
spring.redis.timeout=3000            # 连接超时时间(毫秒)

接下来,创建一个 RedissonConfig 配置类,将 RedissonClient 配置为一个 Spring Bean。通过这个 Bean,可以在项目中方便地获取 RLock 对象,实现分布式锁。

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        return Redisson.create(config);
    }
}

配置解释

  • Config config = new Config();:创建 Redisson 的配置对象。
  • config.useSingleServer().setAddress("redis://localhost:6379");:设置 Redis 服务器的地址。
  • return Redisson.create(config);:通过配置创建 RedissonClient 实例,并注册为 Spring Bean。

3. 获取 RLock 对象

在业务代码中,可以通过 RedissonClient 获取 RLock 对象。 RLock 是一个分布式锁接口,提供了丰富的锁管理方法,支持可重入和自动续期功能。

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LockService {

    @Autowired
    private RedissonClient redissonClient;

    public void lockExample() {
        RLock lock = redissonClient.getLock("my_lock");
        // 使用 lock 进行分布式锁操作
    }
}

4.2.2. RLock 的常用方法和使用示例

RLock 提供了多种方法来管理分布式锁的获取和释放,以下是常用方法及其使用示例:

(1)lock():阻塞获取锁

该方法会阻塞直到获取到锁,适合在没有超时等待要求的场景。

public void lockExample() {
    RLock lock = redissonClient.getLock("my_lock");
    lock.lock(); // 阻塞获取锁
    try {
        // 执行同步的业务逻辑
    } finally {
        lock.unlock(); // 释放锁
    }
}

解释

  • lock.lock():阻塞方式获取锁,如果锁已经被其他线程持有,则等待直至锁被释放。
  • lock.unlock():释放锁,让其他线程可以获取该锁。

(2)tryLock():非阻塞获取锁

tryLock() 方法用于尝试获取锁。如果锁已经被持有,则立即返回 false,不阻塞等待。

public boolean tryLockExample() {
    RLock lock = redissonClient.getLock("my_lock");
    boolean locked = lock.tryLock(); // 非阻塞获取锁
    if (locked) {
        try {
            // 执行同步的业务逻辑
            return true;
        } finally {
            lock.unlock(); // 释放锁
        }
    } else {
        System.out.println("获取锁失败,锁已被持有");
        return false;
    }
}

解释

  • tryLock():立即尝试获取锁,如果锁被持有则返回 false,表示获取锁失败;如果锁可用则返回 true,表示获取锁成功。

(3)tryLock(long waitTime, long leaseTime, TimeUnit unit):带超时的获取锁

这个方法支持等待和超时设置,可以在锁被其他线程持有时等待一定时间。如果在等待时间内未获取到锁则放弃请求,同时设置锁的持有时间。

  • 等待时间:当前线程等待的最大时间。
  • 持有时间:锁成功获取后自动释放的时间。
public boolean tryLockWithTimeoutExample() {
    RLock lock = redissonClient.getLock("my_lock");
    try {
        // 等待时间为5秒,锁的持有时间为10秒
        if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
            try {
                // 执行同步的业务逻辑
                return true;
            } finally {
                lock.unlock(); // 释放锁
            }
        } else {
            System.out.println("获取锁超时,未能成功获取锁");
            return false;
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        System.out.println("获取锁时发生异常");
        return false;
    }
}

解释

  • tryLock(5, 10, TimeUnit.SECONDS):尝试获取锁,等待最多 5 秒,如果在 5 秒内获取到锁,持有 10 秒后自动释放。

实际应用示例:在库存扣减中使用 RLock

假设一个电商系统中有一个库存扣减的操作,为了防止超卖,需要确保每次只有一个线程能够扣减库存,可以通过 RLock 实现这一功能。

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class InventoryService {

    @Autowired
    private RedissonClient redissonClient;

    private static final String LOCK_KEY = "inventory_lock"; // 锁的键名称

    public String deductInventory(String productId) {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        try {
            // 尝试加锁,等待时间5秒,锁定时间10秒
            if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
                try {
                    // 执行扣减库存的逻辑
                    boolean success = reduceStock(productId);
                    return success ? "扣减成功" : "库存不足";
                } finally {
                    lock.unlock(); // 释放锁
                }
            } else {
                return "请稍后再试"; // 获取锁失败
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "获取锁失败";
        }
    }

    private boolean reduceStock(String productId) {
        System.out.println("扣减库存,产品ID: " + productId);
        return true; // 假设库存扣减成功
    }
}

代码解释

  • redissonClient.getLock(LOCK_KEY):获取一个分布式锁对象 RLock,锁的名称为 "inventory_lock"
  • tryLock(5, 10, TimeUnit.SECONDS):尝试获取锁,等待时间 5 秒,持有时间 10 秒。
  • 释放锁:通过 lock.unlock() 确保在业务逻辑执行完成后释放锁。

4.3. 加锁后为什么缓存数据仍可能被其他线程修改

这是因为Redis 分布式锁和缓存并没有直接联系,它们是两个独立的系统。锁的机制只是确保在同一时间只有一个线程可以执行某段代码,并不保证锁住的内容不会被其他线程修改。

在分布式锁场景中,缓存和锁是分离的:

  • 锁控制代码逻辑的独占访问:通过 lock.tryLock(),确保只有一个线程可以访问数据库和缓存更新逻辑(即从数据库获取数据并更新缓存)。
  • 缓存并没有被锁住:在 Redis 中,缓存键的读写是独立的,任何线程都可以访问缓存并进行修改,锁并不能限制其他线程对缓存的访问。

5. 缓存穿透、击穿与雪崩的防范

5.1. 缓存穿透

5.1.1什么是缓存穿透

缓存穿透指的是请求的数据在缓存和数据库中都不存在,这种请求直接穿过缓存访问数据库。当恶意请求大量涌入时,缓存层无法拦截这些无效请求,导致数据库承受大量压力,从而影响性能。比如,用户可能不断请求一个不存在的商品 ID,Redis 缓存中不存在此数据,数据库中也没有。当此类请求频繁出现时,会绕过缓存层直接请求数据库,造成缓存穿透。

5.1.2.什么是布隆过滤器

布隆过滤器(Bloom Filter)是一种概率性数据结构,用于判断一个元素是否存在于集合中。它可以在较少的内存占用下实现快速判断,并且在大多数情况下准确可靠。布隆过滤器能够有效防止缓存穿透,拦截无效请求,从而减少数据库访问压力。

布隆过滤器的工作原理

布隆过滤器由一个长度为 m 的位数组(bit array)和 k 个不同的哈希函数组成。布隆过滤器的基本操作步骤如下:

  • 插入元素:当插入一个元素(如 key)时,布隆过滤器会对该元素通过 k 个哈希函数分别计算出 k 个哈希值,然后将位数组中对应的 k 个位置设为 1

  • 查询元素:当需要判断一个元素是否存在时,对该元素进行相同的 k 个哈希计算,检查位数组中对应的 k 个位置。如果所有位置的值都是 1,则表示该元素“可能存在”;如果有任意一个位置的值为 0,则可以确定该元素不存在。

布隆过滤器的误判问题

由于布隆过滤器的概率性,如果查询结果是“存在”,并不能完全保证该数据确实存在(存在一定的误判概率);但如果查询结果为“不存在”,则可以确定该数据绝对不存在。因此,布隆过滤器非常适合用于防止缓存穿透,因为它能高效筛除不存在的数据请求。

布隆过滤器的优势

  • 拦截无效请求:布隆过滤器可以有效拦截不存在的数据请求,将大量无效请求挡在缓存层之外,避免对数据库的冲击。

  • 高效、低成本:布隆过滤器占用的空间非常小,通过位数组和哈希函数实现,查询速度快且内存占用少,适合大规模数据场景。

  • 减少数据库负担:通过提前过滤掉不存在的数据请求,可以显著降低数据库的负载,提升系统性能。 

5.1.3 BloomFilterConfig 负责创建 bloomFilter(仅初始化布隆过滤器)

Spring Boot 中可以使用 Redisson 提供的布隆过滤器来实现防止缓存穿透。Redisson 是一个强大的 Redis 客户端,支持布隆过滤器等高级功能。我们将以下列代码来演示如何使用布隆过滤器。

代码示例

首先,确保项目中已经添加了 Redisson 依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.15.3</version> <!-- 使用最新版本 -->
</dependency>

然后,我们实现一个布隆过滤器服务 BloomFilterService。在该服务中初始化布隆过滤器,将所有合法的 key 添加进去,并提供判断方法来检测 key 是否存在。

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BloomFilterConfig {

    private static final String RESOURCE_BLOOM_FILTER = "resource_bloom_filter";
    @Autowired
    private RedissonClient redissonClient;
    @Bean
    public RBloomFilter<String> bloomFilter() {
        RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter( RESOURCE_BLOOM_FILTER);

        if (!bloomFilter.isExists()) {
            bloomFilter.tryInit(10000000, 0.01);
        }
        return bloomFilter;
    }
}

初始化布隆过滤器

  • 使用 redissonClient.getBloomFilter(BLOOM_FILTER_NAME) 获取布隆过滤器实例。
  • 调用 tryInit 方法设置布隆过滤器的大小(即预期的插入量)和误判率。例如设置为 100 万个 key,误判率为 1%。
  • 误判率越低,布隆过滤器的空间开销越大。因此误判率的选择要在内存占用和准确性之间找到平衡。

5.1.4 ResourceServiceImpl@PostConstruct 里完成数据库查询并填充布隆过滤器

package com.example.bloomfilter.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.bloomfilter.entity.Resource;
import com.example.bloomfilter.mapper.ResourceMapper;
import com.example.bloomfilter.service.ResourceService;
import org.redisson.api.RBloomFilter;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.List;

@Service
public class ResourceServiceImpl extends ServiceImpl<ResourceMapper, Resource> implements ResourceService {

    @Autowired
    private RBloomFilter<String> bloomFilter;


    /**
     * 应用启动后,将数据库中的 `resourceId` 预加载到布隆过滤器
     */
   @PostConstruct
    public void initBloomFilter() {
        List<String> resourceIds = this.lambdaQuery()
                .select(Resource::getId) // 查询 id 字段
                .list()
                .stream()
                .map(resource -> String.valueOf(resource.getId())) // 确保 Long -> String
                .toList();

        // ✅ 批量添加到布隆过滤器
        resourceIds.forEach(bloomFilter::add);

        System.out.println("布隆过滤器初始化完成,已加载 " + resourceIds.size() + " 个 resourceId");
    }

    /**
     * 查询资源
     */
    public boolean checkResource(String resourceId) {
        if (!bloomFilter.contains(resourceId)) {
            return false; // 直接返回,减少数据库查询
        }
        return lambdaQuery().eq(Resource::getResourceId, resourceId).one() != null;
    }

    /**
     * 添加资源(数据库 & 布隆过滤器)
     */
    public void addResource(Resource resource) {
        save(resource); // 存入数据库
        bloomFilter.add(resource.getResourceId()); // 同步存入布隆过滤器
    }
}
  • ResourceServiceImpl 负责查询数据库,并在 @PostConstruct 里填充 bloomFilter,保证 resourceId 被正确加载。
  • Bloom Filter 已经初始化时,bloomFilter.isExists() 避免重复初始化

5.2.缓存击穿 

5.2.1什么是缓存击穿?

缓存击穿指的是缓存中某个热点数据失效,导致大量请求直接访问数据库的情况。通常在高并发的场景下,一个热点数据被大量访问,突然失效后所有请求同时涌入数据库,增加数据库负担,可能导致性能瓶颈或崩溃。

典型场景

假设一个电商平台有一个非常热门的商品,所有用户都在查询该商品信息。当该商品的缓存突然过期后,大量请求会直接查询数据库,给数据库带来巨大压力。


缓存击穿的防范措施

  • 加锁机制:在缓存失效时,通过加锁确保只有一个线程可以查询数据库并重建缓存,其他线程等待锁释放后再读取缓存。此方法适用于并发访问较高的热点数据。

  • 热点数据永不过期:对极少数热点数据设置永不过期,手动更新这些数据的缓存,确保高频数据不会因缓存过期而直接冲击数据库。此方法适用于访问频繁且数据更新较少的场景。


5.2.2. 使用加锁机制解决缓存击穿

在缓存失效时,可以通过分布式锁确保只有一个线程访问数据库并更新缓存,其他线程等待锁释放后再读取缓存。这样可以避免大量并发请求直接冲击数据库。

实现原理

  1. 查询缓存:先查询缓存,如果缓存命中则直接返回结果。
  2. 加锁:如果缓存未命中,尝试获取分布式锁,确保只有一个线程可以访问数据库。
  3. 双重检查缓存:在获取锁后再次检查缓存,防止在获取锁期间缓存已被其他线程更新。
  4. 查询数据库并更新缓存:如果缓存仍然未命中,查询数据库并将数据写入缓存。
  5. 释放锁:数据库查询和缓存更新完成后,释放锁,让其他线程可以直接读取缓存。

这里为什么加锁之后还能让其他线程修改缓存:因为加锁的意思是对这段代码进行加锁,这段代码的操作只有被加锁的线程能够进行,但缓存是任何线程都可以修改,加锁只是对这段操作加锁,并不是对缓存加锁。 

实现代码示例

以下是一个在 Spring Boot 中使用 Redisson 的加锁机制实现缓存击穿的防范示例:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    private static final String LOCK_KEY = "product_lock_";
    private static final String CACHE_KEY = "product_cache_";

    public String getProduct(String productId) {
        // 1. 查询缓存
        String cacheValue = redisTemplate.opsForValue().get(CACHE_KEY + productId);
        if (cacheValue != null) {
            return cacheValue; // 缓存命中,直接返回
        }

        // 2. 缓存未命中,尝试加锁
        RLock lock = redissonClient.getLock(LOCK_KEY + productId);
        try {
            if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { // 获取锁,等待5秒,锁定10秒
                // 3. 双重检查缓存是否已经被其他线程更新
                cacheValue = redisTemplate.opsForValue().get(CACHE_KEY + productId);
                if (cacheValue != null) {
                    return cacheValue; // 如果缓存已经被其他线程更新,直接返回
                }

                // 4. 查询数据库
                String dbValue = queryDatabase(productId);

                // 5. 更新缓存,设置过期时间
                redisTemplate.opsForValue().set(CACHE_KEY + productId, dbValue, 10, TimeUnit.MINUTES);

                return dbValue;
            } else {
                // 获取锁失败,等待缓存更新完成后重新获取
                Thread.sleep(100); // 可以增加重试机制
                return redisTemplate.opsForValue().get(CACHE_KEY + productId);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            lock.unlock(); // 6. 释放锁
        }
    }

    private String queryDatabase(String productId) {
        // 模拟数据库查询
        System.out.println("查询数据库,商品ID:" + productId);
        return "Product data for " + productId;
    }
}

代码详细解释

  • 查询缓存:首先从缓存中读取数据。如果缓存命中则直接返回结果,避免了不必要的锁操作。

  • 加锁查询数据库

    • 如果缓存未命中,使用 Redisson 获取分布式锁 lock,确保只有一个线程可以查询数据库并更新缓存。
    • 双重检查缓存:在成功加锁后再次检查缓存,防止在等待锁的过程中其他线程已经更新了缓存。
  • 重建缓存:持有锁的线程从数据库中读取数据并更新到缓存中,同时设置缓存的过期时间。

  • 释放锁:缓存更新完成后,释放锁,确保其他线程可以从缓存读取最新数据。

优势

  • 防止缓存击穿:加锁确保在缓存失效时,只有一个线程查询数据库并更新缓存,避免大量并发请求直接冲击数据库。
  • 双重检查减少重复操作:锁内再次检查缓存,确保其他线程不会重复查询数据库,减少数据库压力。

5.2.3. 设置热点数据永不过期

对于少数非常高频访问的热点数据,可以将其缓存设置为永不过期,这样确保数据不会失效,从而避免缓存击穿。

实现原理

  • 热点数据初始化:系统启动时将热点数据加载到缓存中,设置为永不过期。
  • 数据更新策略:通过数据库更新事件或定时任务主动更新热点缓存数据,而不依赖过期机制。

这种方式适合稳定且访问频繁的数据,例如电商首页的推荐商品、分类信息等,可以有效减少缓存失效的概率。

代码示例

以下示例展示如何在 Spring Boot 中将一些热点数据设置为永不过期,并通过手动更新的方式维护缓存数据。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class HotDataCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String CACHE_KEY = "hot_product_cache_";

    public String getHotProduct(String productId) {
        // 查询缓存
        String cacheValue = redisTemplate.opsForValue().get(CACHE_KEY + productId);
        if (cacheValue != null) {
            return cacheValue; // 缓存命中,直接返回
        }

        // 缓存未命中,加载数据库并更新缓存(永不过期)
        String dbValue = queryDatabase(productId);

        // 设置热点数据永不过期
        redisTemplate.opsForValue().set(CACHE_KEY + productId, dbValue); // 不设置过期时间

        return dbValue;
    }

    private String queryDatabase(String productId) {
        // 模拟数据库查询
        System.out.println("查询数据库,商品ID:" + productId);
        return "Hot product data for " + productId;
    }

    // 手动更新缓存
    public void updateHotProductCache(String productId, String productData) {
        redisTemplate.opsForValue().set(CACHE_KEY + productId, productData); // 不设置过期时间
    }
}

代码详细解释

  1. 查询缓存:首先尝试读取缓存数据。如果缓存命中则直接返回结果,减少数据库访问。

  2. 更新缓存:如果缓存未命中,查询数据库并更新缓存,不设置过期时间,确保该热点数据不会因过期而失效。

  3. 手动更新缓存:提供 updateHotProductCache 方法,以便在热点数据有变动时手动更新缓存内容。可以通过数据库更新事件或定时任务主动更新缓存。

优势和适用场景

  • 适合高频访问的热点数据:特别是访问频率很高、不会频繁变化的数据,例如首页推荐商品、热门分类等。
  • 避免缓存失效带来的冲击:热点数据不会因为缓存失效而直接查询数据库,有效减少数据库负载。

5.3.缓存雪崩 

5.3.1.什么是缓存雪崩?

缓存雪崩指的是在某个时间点,大量缓存同时失效,导致所有请求直接访问数据库,给数据库带来巨大的压力,甚至可能导致系统崩溃。这种情况通常发生在以下场景:

  • 缓存服务器宕机:整个缓存服务不可用,所有请求直接落到数据库。
  • 大量缓存集中在同一时间过期:大量缓存设置了相同的过期时间,导致在某一时刻同时失效。

为防止缓存雪崩,可以采取以下措施:

  • 缓存数据的过期时间设置随机化:在缓存过期时间的基础上,加上一个随机值,避免大量缓存同时失效。

  • 缓存预热:在系统启动或高峰期到来之前,提前将热点数据加载到缓存中。

  • 多级缓存:在本地增加一级缓存,如 Guava Cache,减轻对远程缓存的依赖。

  • 限流和降级:在缓存失效时,对数据库的访问进行限流,必要时进行服务降级。


5.3.2. 设置不同的缓存失效时间

原理

为每个缓存数据设置一个基础过期时间,并在此基础上添加一个随机的时间偏移量,使缓存的过期时间分布在一定的范围内。这样可以避免大量缓存数据在同一时刻失效,分散过期时间点,从而减轻数据库的瞬时访问压力。

实现代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String CACHE_KEY_PREFIX = "product_cache_";
    private static final int BASE_EXPIRE_TIME = 10; // 基础过期时间,单位:分钟

    public void cacheProduct(String productId, String productData) {
        // 生成随机过期时间,范围在 BASE_EXPIRE_TIME 到 BASE_EXPIRE_TIME + 5 分钟之间
        int expireTime = BASE_EXPIRE_TIME + new Random().nextInt(5);
        
        // 将数据存入 Redis,并设置不同的过期时间
        redisTemplate.opsForValue().set(CACHE_KEY_PREFIX + productId, productData, expireTime, TimeUnit.MINUTES);
    }

    public String getProduct(String productId) {
        // 从 Redis 中获取数据
        return redisTemplate.opsForValue().get(CACHE_KEY_PREFIX + productId);
    }
}

代码解释

  • 基础过期时间BASE_EXPIRE_TIME 设置为 10 分钟,表示缓存的基础过期时间。

  • 随机过期时间:在 BASE_EXPIRE_TIME 基础上,加上一个 0 到 4 分钟的随机数 new Random().nextInt(5),使得缓存的失效时间在 10 到 14 分钟之间。这样不同的缓存条目会有略微不同的过期时间,避免在同一时刻集中失效。

  • 缓存数据设置redisTemplate.opsForValue().set 方法将数据写入 Redis,并指定过期时间 expireTime。不同的缓存条目会有不同的失效时间,有效降低缓存雪崩的风险。


5.3.3. 缓存预热

缓存预热是指在系统启动时,提前将部分热点数据加载到缓存中,避免在系统运行后产生大量的缓存未命中情况。通过缓存预热,可以保证在系统启动或高峰期来临时,热点数据已经存在于缓存中,减少对数据库的访问压力。

实现思路

  • 在系统启动时:加载一些常用的、访问频率高的数据到缓存中。
  • 定时任务:定期更新缓存中的热点数据,确保缓存中的数据始终有效。

实现代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;

@Service
public class CacheWarmUpService {

    @Autowired
    private CacheService cacheService;

    @PostConstruct
    public void warmUpCache() {
        // 获取热点数据的ID列表
        List<String> hotProductIds = getHotProductIds();

        // 将每个热点数据加载到缓存中
        for (String productId : hotProductIds) {
            String productData = queryDatabase(productId);
            cacheService.cacheProduct(productId, productData); // 将数据预先缓存
        }
    }

    private List<String> getHotProductIds() {
        // 从数据库或配置中获取热点数据ID列表
        return Arrays.asList("1001", "1002", "1003"); // 示例数据
    }

    private String queryDatabase(String productId) {
        // 模拟数据库查询
        return "Product data for " + productId;
    }
}

代码解释

  • @PostConstruct 注解:该注解会在 Spring Boot 容器启动后自动调用 warmUpCache() 方法。这可以确保在系统启动时,热点数据就已经加载到缓存中。

  • 获取热点数据getHotProductIds 方法用于获取高访问量的商品 ID 列表,可以从数据库或配置文件中获取。
  • 缓存热点数据:遍历热点商品 ID,通过 queryDatabase 方法查询数据库,然后调用 cacheService.cacheProduct 方法将数据缓存起来。

 5.3.4. 其他防范方法

1.限流与降级

在缓存雪崩发生时,即大量缓存突然失效,访问量瞬间增加,可以通过限流和降级的方式来保护数据库,避免数据库被大量请求压垮。

限流

限流可以控制进入数据库的请求量,将超出部分的请求暂时阻止或延迟,避免瞬时高并发对数据库造成过大的压力。常用的限流算法包括令牌桶漏桶算法等。

降级

当缓存不可用时,可以考虑提供简化的数据或延迟响应,避免数据库压力过大。例如:

  • 返回缓存中的旧数据,虽然不够实时,但可以减少数据库的访问。
  • 对非核心业务进行降级,暂时返回空结果或简单提示,确保核心业务的正常运行。

2.多级缓存

多级缓存通过本地缓存和远程缓存结合使用的方式,进一步减轻远程缓存的压力,提升系统的访问速度。多级缓存常见的实现方式是本地缓存(如 Caffeine 或 Guava Cache) + Redis 远程缓存

  • 本地缓存:将热点数据存储在应用服务器本地的内存中,访问速度极快,可以应对短期的缓存雪崩。
  • 远程缓存:使用 Redis 作为分布式缓存,缓存更多数据。

6.Redis 高可用架构方案

在 Redis 的高可用技术方案中,高可用指的是在出现节点故障或网络波动时,系统能够持续提供缓存服务,避免服务中断或性能严重下降。高可用的设计保证了即使某个缓存节点故障,缓存服务仍然可以通过故障转移等机制正常工作,减少了单点故障的风险,保证了系统的可靠性和稳定性。

6.1. Redis Sentinel哨兵模式(主从复制故障监控)

Redis Sentinel 是 Redis 官方提供的一种高可用解决方案,主要通过主从复制故障监控来实现。在 Redis Sentinel 架构中,Sentinel 负责监控 Redis 集群中的主从节点,当主节点出现故障时,Sentinel 会自动将一个从节点提升为主节点,保证服务的持续运行。这种架构适用于不需要数据分片的场景,适合数据量较小且对高可用性有要求的系统。


6.1.1.Redis Sentinel 高可用架构原理

Redis Sentinel 通过三个主要功能来实现 Redis 集群的高可用性:

  • 主从复制:在 Sentinel 架构中,Redis 主节点负责处理写请求,并将数据同步到从节点。多个从节点是主节点的备份,只负责同步数据,不参与写操作。当主节点故障时,Sentinel 可以将从节点提升为主节点,从而实现高可用。

  • 故障转移:每个 Sentinel 实例不断发送 PING 命令来检测 Redis 主节点和其他 Sentinel 的状态。如果在设定的时间范围内没有收到主节点的响应,Sentinel 会认为主节点故障,发起故障转移(Failover),选举某个从节点为新主节点。

  • 通知应用程序:当主节点发生变化时,Sentinel 会将新主节点的地址信息推送给 Redis 客户端(应用程序),让客户端感知主节点的变化,避免因主节点切换而导致的连接错误。


6.1.2.在 Spring Boot 中集成 Redis Sentinel

在 Spring Boot 项目中集成 Redis Sentinel 包括以下几个主要步骤:

  • 配置 Redis Sentinel 集群:配置 Redis 主从节点并启动多个 Sentinel 实例监控主节点的状态。
  • 在 Spring Boot 中配置 Sentinel 集群:在 application.properties 中指定 Sentinel 集群的节点信息和主节点名称。Spring Boot 会自动管理主节点切换。
  • 应用程序使用:在 Spring Boot 中直接使用 RedisTemplateStringRedisTemplate 进行 Redis 操作,Sentinel 会自动切换主节点,无需手动干预。
1.搭建并配置 Redis Sentinel 集群

配置 Redis 主从复制

首先需要配置 Redis 的主从复制,使得数据可以从主节点同步到从节点。

  • 主节点配置:假设 Redis 主节点运行在 127.0.0.1:6379,无需额外配置。

  • 从节点配置:在 Redis 从节点的配置文件(如 redis-slave.conf)中,添加如下配置,将该节点设置为主节点的从节点。

    replicaof 127.0.0.1 6379
    

    这样,从节点会自动从主节点复制数据,保证数据一致性。

配置并启动 Redis Sentinel

接着需要在 Redis 服务器上配置 Redis Sentinel。创建或编辑 sentinel.conf 文件,添加以下配置来监控 Redis 主节点。

# 监控的主节点名称和地址
sentinel monitor mymaster 127.0.0.1 6379 2

# 在 5 秒内未响应则判定节点不可达
sentinel down-after-milliseconds mymaster 5000

# 故障转移最大超时 10 秒
sentinel failover-timeout mymaster 10000

# 故障转移时同步新主节点的从节点数量
sentinel parallel-syncs mymaster 1
  • sentinel monitormymaster 是主节点名称,127.0.0.1:6379 是主节点地址,2 表示至少两个 Sentinel 节点判定主节点不可达时才进行故障转移。
  • sentinel down-after-milliseconds:指定 Sentinel 判断主节点失效的超时时间(5 秒)。
  • sentinel failover-timeout:设置故障转移的最大等待时间(10 秒)。
  • sentinel parallel-syncs:指定故障转移完成后,允许多个从节点并行同步新主节点。

启动 Redis Sentinel 服务

在每台需要运行 Sentinel 的服务器上启动 Sentinel 服务。一般至少配置三个 Sentinel 节点,以保证高可用性和故障切换的准确性。

redis-server /path/to/sentinel.conf --sentinel

2.在 Spring Boot 中配置 Redis Sentinel 集成

在 Spring Boot 项目中,直接通过配置文件指定 Redis Sentinel 信息,Spring Boot 会自动识别并连接到 Redis Sentinel 集群。

配置 application.properties

application.properties 中配置 Redis Sentinel 的主节点名称和 Sentinel 节点地址,Spring Boot 将会自动管理主节点切换,无需手动更改主节点地址。

# Redis Sentinel 集群的主节点名称和 Sentinel 节点地址
spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
spring.redis.password=yourpassword
  • spring.redis.sentinel.master:指定 Redis 主节点名称,必须与 sentinel.conf 中的名称一致。
  • spring.redis.sentinel.nodes:配置所有 Sentinel 节点的 IP 和端口,Spring Boot 会自动连接并监控这些 Sentinel 节点。
  • spring.redis.password:如果 Redis 设置了密码,可以在此处配置。

3.在 Spring Boot 中使用 Redis Sentinel

完成以上配置后,Spring Boot 会自动连接 Redis Sentinel 集群。当主节点发生故障时,Redis Sentinel 会自动将某个从节点提升为新的主节点,Spring Boot 无需手动干预,客户端会自动连接到新的主节点。

示例代码

在 Spring Boot 中,可以使用 StringRedisTemplateRedisTemplate 来操作 Redis 数据。以下是一个简单的 Redis 服务类,展示了如何在应用中使用 Redis Sentinel:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class RedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public void saveData(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    public String getData(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }
}

RedisService 中,StringRedisTemplate 负责与 Redis Sentinel 集群交互,执行数据的读写操作。当 Redis Sentinel 发生故障转移时,Spring Boot 会自动更新主节点连接,因此无需额外的逻辑来处理主节点变化。


6.1.3.Redis Sentinel 的工作流程总结

  1. 故障检测:每个 Sentinel 节点不断向主节点、从节点和其他 Sentinel 节点发送 PING 命令,检测节点是否正常响应。

  2. 故障判定:当某个 Sentinel 节点在设定时间内未收到主节点的响应时,会将该主节点标记为主观下线状态(Subjectively Down,简称 SDOWN)。当多个 Sentinel 节点认为主节点不可达时,主节点会被标记为客观下线(Objectively Down,简称 ODOWN),并触发故障转移。

  3. 故障转移:在主节点被判定故障后,Sentinel 集群会选举出一个健康的从节点,将其提升为新的主节点,并重新配置其他从节点连接到该新主节点。

  4. 通知客户端:故障转移完成后,Sentinel 将新主节点的信息通知给 Redis 客户端,使客户端应用自动切换到新的主节点。


6.1.4.Redis Sentinel 的特点

优势:

  • 高可用性:当主节点故障时,Redis Sentinel 可以自动选择从节点进行故障转移,保证服务的持续可用。
  • 自动主节点发现:应用程序无需手动调整主节点地址,Redis Sentinel 在主节点发生变化后自动通知客户端,Spring Boot 会自动连接新的主节点。
  • 适合非分片场景:Redis Sentinel 不支持数据分片,因此适用于数据量相对较小、不需要水平扩展的场景。

局限性:

  • 不支持分片:Redis Sentinel 只适用于单一主从结构,无法分片,数据量过大时需要 Redis Cluster 等其他分布式方案。
  • 哨兵节点的高可用性依赖:为了确保故障切换的准确性和服务的稳定性,通常需要至少三个 Sentinel 节点。

6.2 Redis Cluster(数据分片和自动故障转移)

Redis Cluster 是 Redis 官方提供的一种高可用和高性能的分布式缓存方案,通过数据分片和自动故障转移来实现高可用性。Redis Cluster 将数据分片存储在多个主从节点上,支持水平扩展,适合大数据量和高并发需求的场景。在 Redis Cluster 中,每个主节点负责一部分数据,并有一个或多个从节点作为备份,当某个主节点发生故障时,从节点会自动提升为新的主节点,以确保缓存服务的正常运行。

6.2.1 Redis Cluster 高可用架构原理

Redis Cluster 通过以下几个主要功能来实现高可用性:

  • 数据分片:Redis Cluster 将数据划分为 16384 个哈希槽(Hash Slots),每个节点负责一部分哈希槽,支持水平扩展。
  • 自动故障转移:每个主节点拥有一个或多个从节点。当主节点出现故障时,从节点自动提升为主节点,确保数据的高可用性。
  • 高并发支持:由于 Redis Cluster 分散数据存储,可以处理大规模数据请求,适合高并发场景。

6.2.2 在 Spring Boot 中集成 Redis Cluster

在 Spring Boot 项目中集成 Redis Cluster 包括以下几个主要步骤:

  1. 配置 Redis Cluster 集群:配置并启动多个 Redis 主从节点,创建 Redis Cluster 集群。
  2. 在 Spring Boot 中配置 Cluster 集群信息:在 application.properties 中指定 Redis Cluster 节点信息,Spring Boot 会自动管理节点连接和数据分片。
  3. 应用程序使用:在 Spring Boot 中直接使用 RedisTemplateStringRedisTemplate 进行 Redis 操作,Redis Cluster 会自动进行分片存储和主从切换。
1. 搭建并配置 Redis Cluster 集群

配置 Redis Cluster 的主从节点

在 Redis 集群中,每个节点需要独立的配置文件(例如 redis-7000.confredis-7001.conf 等),以下是示例配置:

# redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
appendonly yes

其他节点(7001、7002 等)配置类似,只需修改端口号即可。

启动 Redis 节点

启动所有 Redis 节点,例如 7000 至 7005 端口的 Redis 实例:

redis-server /path/to/redis-7000.conf
redis-server /path/to/redis-7001.conf
# ...依次启动其他节点

创建 Redis 集群

使用 redis-cli 命令行工具将这些节点添加到集群,并指定主从关系:

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

此命令会创建 3 个主节点(7000、7001、7002)和 3 个从节点(7003、7004、7005),实现主从配置和数据分片。

2. 在 Spring Boot 中配置 Redis Cluster 集成

在 Spring Boot 中,通过配置文件指定 Redis Cluster 的节点信息和连接属性,Spring Boot 会自动管理 Redis Cluster 集群。

配置 application.properties

# 配置 Redis Cluster 集群节点
spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005

# 最大重定向次数(适用于分片数据重定向)
spring.redis.cluster.max-redirects=3

# Redis 密码(如果 Redis 设置了密码)
spring.redis.password=yourpassword
  • spring.redis.cluster.nodes:指定 Redis Cluster 集群中所有节点的 IP 和端口,Spring Boot 会自动连接到这些节点。
  • spring.redis.cluster.max-redirects:设置最大重定向次数,用于分片数据的重定向处理。
  • spring.redis.password:如果 Redis 设置了密码,可以在这里配置。
3. 在 Spring Boot 中使用 Redis Cluster

完成配置后,开发者可以直接使用 RedisTemplateStringRedisTemplate 操作数据。Spring Boot 会根据 Redis Cluster 的节点信息自动进行分片管理和主从切换。

示例代码

以下是一个 Redis 服务类示例,展示了如何在 Spring Boot 中使用 Redis Cluster 进行数据的增删改查操作:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class RedisClusterService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 存储数据到 Redis Cluster
    public void saveData(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    // 从 Redis Cluster 获取数据
    public String getData(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    // 删除 Redis Cluster 中的数据
    public void deleteData(String key) {
        stringRedisTemplate.delete(key);
    }
}
  • 数据存储:使用 StringRedisTemplateopsForValue().set() 方法将数据存储到 Redis Cluster,数据会自动分配到对应的哈希槽。
  • 数据读取:使用 opsForValue().get() 方法读取数据,Redis Cluster 会根据数据的哈希槽位置定位并返回数据。
  • 自动故障转移:当某个主节点发生故障时,Redis Cluster 会自动将从节点提升为主节点,Spring Boot 无需额外配置即可继续使用。

6.2.3 Redis Cluster 的特点

优势

  • 数据分片:Redis Cluster 将数据分片存储在多个节点上,支持水平扩展,适合大数据量场景。
  • 自动故障转移:当主节点故障时,Redis Cluster 会自动提升从节点为主节点,实现自动故障转移,保障服务的高可用性。
  • 高并发支持:分片结构允许 Redis Cluster 处理更多的并发请求,是高并发场景的理想选择。

局限性

  • 跨节点事务支持有限:Redis Cluster 不支持多键操作的跨节点事务,分布在不同节点上的键无法进行原子操作。
  • 节点之间的网络通信:Redis Cluster 的节点间需要互相通信,因此在网络不稳定的环境中可能导致集群不一致。
  • 最低节点要求:Redis Cluster 要求至少 6 个节点(3 个主节点和 3 个从节点)才能实现高可用。

适用场景

  • 大规模缓存系统:在需要处理大数据量的缓存场景中,Redis Cluster 提供数据分片和自动容错机制。
  • 高并发的会话管理:适合处理大量用户会话,特别是在高并发应用中。
  • 需要水平扩展的场景:Redis Cluster 通过分片实现了水平扩展,适合随着业务增长扩展数据容量的场景。

6.2.4.关于槽点的重新分配

1. 添加或删除节点后的槽点分配

当 Redis Cluster 中添加或删除节点时,Redis 不会自动重新分配槽点,因此需要手动进行槽点迁移。解决方法如下:

  • 手动重新分配槽点
    • 使用 redis-trib.rbredis-cli --cluster reshard 命令,将槽点重新分配到新的节点或从即将删除的节点迁出。
    • 该操作需要管理员手动执行,可以通过指定源节点、目标节点和槽数进行槽点迁移。
  • 自动化方案(可选):
    • 通过 Spring Boot 中的定时任务(@Scheduled)检查集群状态,检测到节点增减后,自动调用 redis-trib.rbredis-cli 执行槽点重新分配操作。

2. 负载均衡的槽点分配

Redis Cluster 不支持自动基于负载的槽点分配调整,因此实现负载均衡也需要手动操作。解决方法如下:

  • 手动负载均衡
    • 定期使用 Redis 客户端(如 Jedis)或监控工具检查各节点的负载情况。
    • 如果发现某些节点负载过高,可以手动执行 redis-trib.rbredis-cli --cluster reshard 命令,将部分槽点从负载较高的节点迁移至负载较低的节点,达到负载均衡。
  • 自动化方案(可选):
    • 在 Spring Boot 中通过定时任务(@Scheduled)自动监控节点负载。
    • 结合负载监控逻辑,使用 ProcessBuilder 调用 redis-trib.rb 脚本或 redis-cli 执行槽点迁移,从而实现更灵活的负载均衡自动化。

7. Redis Streams消息队列

7.1 Redis Streams 基础概念

Redis Streams 是Redis 5.0引入的一种日志型数据结构,专为消息队列场景设计。它支持:

  • 持久化消息存储:消息按时间顺序追加到Stream,支持范围查询。

  • 消费者组(Consumer Groups):允许多个消费者协同消费同一Stream,实现负载均衡。

  • 消息确认(ACK)机制:确保消息被正确处理,避免丢失。

  • 阻塞读取:消费者可以阻塞等待新消息,减少轮询开销。


7.2 Redis Streams 底层原理

(1)数据结构组成

Stream:由多个 消息条目(Entry) 组成,每个条目包含:

  • 唯一ID:格式为<时间戳>-<序列号>(如1630000000000-0)。

  • 键值对数据:存储实际消息内容(如{"resourceId": "123", "action": "like"})。

消费者组(Consumer Group)

  • Last Delivered ID:记录组内最后发送给消费者的消息ID。

  • Pending Entries List (PEL):已发送但未确认的消息列表。

  • 消费者(Consumer):组内每个消费者维护自己的待处理消息列表。

(2)存储结构:Radix树(Rax)

Redis Streams使用 Radix树(基数树) 存储消息,这是一种压缩前缀树,具有以下特点:

  • 高效范围查询:通过消息ID快速定位区间内的消息。

  • 内存优化:共享相同前缀的消息条目节省存储空间。

  • 快速插入和删除:时间复杂度为O(log N),适合高吞吐场景。

(3)消息ID生成规则

  • 自动生成:使用*作为ID时,Redis自动生成<当前毫秒时间戳>-<序列号>

  • 手动指定:需保证ID大于前一条消息的ID(否则报错)。

7.3 生产者

StringRecord 和 ObjectRecord 的作用

StringRecord 和 ObjectRecord 是 Spring Data Redis 中用于构建 Stream 消息的两种数据类型:

  • StringRecord:用于存储 字符串类型 的消息值。

    StringRecord record = StreamRecords.string()
        .withStreamKey("my_stream")
        .withId("1526919030474-0")  // 可选,手动指定ID
        .ofObject("value");         // 消息内容为字符串
  • ObjectRecord:用于存储 Java 对象 的消息值(需配置 Redis 序列化器)。

    Map<String, String> message = new HashMap<>();
    message.put("resourceId", "123");
    message.put("action", "like");
    
    ObjectRecord<String, Map<String, String>> record = StreamRecords.newRecord()
        .withStreamKey("my_stream")
        .ofObject(message);  // 消息内容为对象

选择建议

  • 如果消息内容是简单键值对,使用 StringRecord

  • 如果消息内容是复杂对象,使用 ObjectRecord(需确保对象可序列化)。

7.3.1 发送消息(自动生成ID)

@Component
public class StreamProducer {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 发送消息(自动生成消息ID)
     */
    public void sendMessage(String streamKey, Map<String, String> message) {
        ObjectRecord<String, Map<String, String>> record = StreamRecords.newRecord()
            .ofObject(message)
            .withStreamKey(streamKey);
        String messageId = redisTemplate.opsForStream().add(record);
        System.out.println("消息发送成功,ID: " + messageId);
    }
}

7.3.2 发送消息(手动指定ID)

public void sendMessageWithId(String streamKey, String messageId, Map<String, String> message) {
    StringRecord record = StreamRecords.string()
        .withId(messageId)
        .ofObject(message)
        .withStreamKey(streamKey);
    redisTemplate.opsForStream().add(record);
}

查询 Stream 长度

public Long getStreamSize(String streamKey) {
    return redisTemplate.opsForStream().size(streamKey);
}

7.4 消费者组

7.4.1 消费者组初始化

@Component
public class StreamConsumerInitializer {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void initConsumerGroups() {
        // 初始化资源更新流的消费者组
        createGroupIfNotExists("resource_stream", "resource_group");
    }

    private void createGroupIfNotExists(String streamKey, String groupName) {
        try {
            redisTemplate.opsForStream().createGroup(
                streamKey, 
                ReadOffset.from("0-0"),  // 从第一条消息开始消费
                groupName
            );
        } catch (RedisSystemException e) {
            System.out.println("消费者组已存在: " + groupName);
        }
    }
}

7.4.2 消费者实现(批量消费+双阈值)

  • ReadOffset 类型

    • from("0-0"):从最早消息开始

    • lastConsumed():读取未消费的新消息(对应 >

    • from("$"):只读取未来到达的消息

  • StreamReadOptions 配置项

    • block(Duration):阻塞等待时间

    • count(long):每次读取最大消息数

    • noack():不自动确认消息(需手动 ACK)

@Component
public class ResourceStreamConsumer {

    private static final String STREAM_KEY = "resource_stream";
    private static final String GROUP_NAME = "resource_group";
    private static final String CONSUMER_NAME = "consumer_1";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 启动消费者(异步线程池)
     */
    @Async("streamConsumerPool")
    public void startConsumer() {
        while (true) {
            try {
                List<MapRecord<String, Object, Object>> batch = fetchMessages();
                if (!batch.isEmpty()) {
                    processBatch(batch);
                    acknowledgeMessages(batch);
                }
            } catch (Exception e) {
                handleConsumerError(e);
            }
        }
    }

    /**
     * 批量拉取消息(满足数量阈值100条或时间阈值5秒)
     */
    private List<MapRecord<String, Object, Object>> fetchMessages() {
        List<MapRecord<String, Object, Object>> messages = new ArrayList<>();
        long startTime = System.currentTimeMillis();

        Consumer consumer = Consumer.from(GROUP_NAME, CONSUMER_NAME);
        StreamOffset<String> offset = StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed());

        while (messages.size() < 100 && (System.currentTimeMillis() - startTime) < 5000) {
            List<MapRecord<String, Object, Object>> chunk = redisTemplate.opsForStream().read(
                consumer,
                StreamReadOptions.empty()
                    .count(100 - messages.size())
                    .block(Duration.ofSeconds(1)),
                offset
            );
            if (chunk != null) messages.addAll(chunk);
        }

        return messages;
    }

    /**
     * 批量处理消息(合并更新)
     */
    private void processBatch(List<MapRecord<String, Object, Object>> messages) {
        Map<String, Integer> likeUpdates = new HashMap<>();
        Map<String, Integer> downloadUpdates = new HashMap<>();

        for (MapRecord<String, Object, Object> record : messages) {
            String resourceId = (String) record.getValue().get("resourceId");
            int deltaLike = Integer.parseInt((String) record.getValue().get("likeDelta"));
            int deltaDownload = Integer.parseInt((String) record.getValue().get("downloadDelta"));

            likeUpdates.merge(resourceId, deltaLike, Integer::sum);
            downloadUpdates.merge(resourceId, deltaDownload, Integer::sum);
        }

        // 原子更新数据库
        likeUpdates.forEach((id, total) -> 
            jdbcTemplate.update("UPDATE resources SET likes = likes + ? WHERE id = ?", total, id)
        );
        downloadUpdates.forEach((id, total) -> 
            jdbcTemplate.update("UPDATE resources SET downloads = downloads + ? WHERE id = ?", total, id)
        );
    }

    /**
     * 确认消息处理完成
     */
    private void acknowledgeMessages(List<MapRecord<String, Object, Object>> messages) {
        String[] ids = messages.stream()
            .map(record -> record.getId().getValue())
            .toArray(String[]::new);
        redisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP_NAME, ids);
    }

    /**
     * 异常处理(记录日志 + 告警)
     */
    private void handleConsumerError(Exception e) {
        System.err.println("消费者处理异常: " + e.getMessage());
        // 发送告警通知
        // alertService.send("Stream消费异常", e);
    }
}

查询待处理消息

(1)什么是待处理消息(Pending Messages)?

  • 当消费者从消费者组读取消息后,若未发送 ACK 确认,这些消息会进入 Pending Entries List (PEL)

  • PEL 中的消息表示“已分配但未完成处理”的状态,可能因以下原因滞留:

    • 消费者崩溃未发送 ACK。

    • 消息处理失败需人工介入。

    • 消费者处理时间过长导致超时。

(2)查询 PEL 的典型场景

  • 监控告警:检测是否有消息长时间未被处理(如超过 30 分钟)。

  • 故障排查:定位卡住业务流程的特定消息。

  • 手动重试:将失败消息重新分配给其他消费者。

(3)示例:监控 PEL 并发送告警

@Scheduled(cron = "0 0/5 * * * ?")  // 每5分钟检查一次
public void checkPendingMessages() {
    PendingMessages pending = redisTemplate.opsForStream().pending(
        "resource_stream", 
        "resource_group",
        Range.unbounded(), 
        1000
    );
    
    if (pending.size() > 100) {
        alertService.send("待处理消息堆积警告", "当前 PEL 数量: " + pending.size());
    }
}

裁剪旧消息

Streams所有消息常驻内存(包括处理成功),需定时裁剪消息(还有根据消息阈值裁剪的方式)

public void trimStream() {
    // 保留最新1000条消息
    redisTemplate.opsForStream().trim(STREAM_KEY, 1000);
}

7.5 注意事项

(1)消费者组的初始化

在 Spring Boot 中,通常通过 @PostConstruct 在应用启动时创建消费者组:

@PostConstruct
public void initConsumerGroup() {
    try {
        redisTemplate.opsForStream().createGroup(
            "my_stream", 
            ReadOffset.from("0-0"),  // 从第一条消息开始消费
            "my_group"
        );
    } catch (RedisSystemException e) {
        // 消费者组已存在,忽略异常
    }
}

(2)Redis 崩溃后的恢复机制

  • Redis 重启后:消费者组信息仍然存在(Redis 默认持久化到 RDB/AOF)。

  • Spring Boot 程序恢复

    • 自动重连:Spring Data Redis 会自动尝试重新连接 Redis。

    • 无需重新创建消费者组:消费者组是 Redis 内部的持久化数据,无需手动重建。

  • 容错处理建议

    @Scheduled(fixedDelay = 5000) // 定时检查消费者组是否存在
    public void checkConsumerGroup() {
        try {
            StreamInfo.XInfoStream info = redisTemplate.opsForStream().info("my_stream");
            if (!info.getGroups().contains("my_group")) {
                initConsumerGroup(); // 重新创建消费者组
            }
        } catch (Exception e) {
            // Redis 不可用,记录日志并重试
        }
    }

(3)消息传递模式

  • 生产者发送消息:消息直接写入 Redis Stream,不会主动推送给消费者。

  • 消费者拉取消息:消费者需主动调用 read() 方法从 Stream 中拉取消息。

(4)消费者组的负载均衡

  • 同一组内多个消费者:Redis会自动将消息分配给不同消费者,实现并行处理。

  • 消息分配策略:默认轮询分配,可通过XCLAIM手动接管未处理消息。

(5)消息重试与死信队列

  • 处理失败的消息:若消费者崩溃,未ACK的消息会被重新分配给其他消费者。

  • 死信队列:多次重试失败后,可将消息移至另一Stream进行人工干预。

8. Redis Pipeline 技术


8.1 什么是 Redis Pipeline?

  • 核心概念
    Redis Pipeline 是一种客户端技术,允许将多个 Redis 命令一次性批量发送到服务器,并一次性接收所有响应。

    • 传统模式:客户端发送一个命令 → 等待响应 → 再发送下一个命令(多次网络往返,RTT 延迟高)。

    • Pipeline 模式:客户端将多个命令打包成一个批次发送 → 服务器按顺序处理 → 将所有响应打包返回(单次网络往返)。

  • 性能对比

    操作方式网络往返次数(N 条命令)总耗时(假设网络延迟 10ms)
    普通模式N 次N × (命令处理时间 + 10ms)
    Pipeline 模式1 次命令总处理时间 + 10ms

8.2 为什么需要 Pipeline?

  • 高延迟场景优化
    当客户端与 Redis 服务器之间存在高网络延迟时(如跨机房访问),Pipeline 能显著减少总耗时。

  • 吞吐量提升
    批量处理命令可提高单位时间内处理的命令数量(QPS),适合批量写入、批量查询等场景。

  • 资源节省
    减少客户端和服务端的网络 I/O 压力,降低线程阻塞时间。


8.3. Pipeline 在 Java 中的使用(Spring Data Redis)

Redis Pipeline 的限制并不是不能执行条件判断,它的真正限制是无法根据前一个Redis命令的执行结果来作为条件动态调整后续的命令 。

SessionCallback<T> 是 Spring 提供的一个接口,用于将多个 Redis 操作封装在同一个 Redis 连接中执行,包括支持:

  • pipeline(管道模式)

  • 事务(transaction 模式)

使用步骤

步骤操作
1️⃣创建 SessionCallback 并在 execute() 方法中执行多个 Redis 命令
2️⃣使用 redisTemplate.executePipelined(callback) 来批量执行
3️⃣所有命令都会被打包为 pipeline 执行(一次请求发出去)
4️⃣返回的 List<Object> 与顺序一致,只包含有返回值的操作结果
5️⃣可以继续通过 stream 处理、封装到对象中

传统写法(匿名内部类)

List<Object> results = redisTemplate.executePipelined(new SessionCallback<>() {
    @Override
    public Object execute(@NonNull RedisOperations operations) throws DataAccessException{
        // 批量读取 key
        operations.opsForValue().get("key1");
        operations.opsForValue().get("key2");

        // 批量设置过期时间
        operations.expire("key1", Duration.ofMinutes(10));
        operations.expire("key2", Duration.ofMinutes(10));

        return null; // 固定写法,表示不关心 Session 返回值
    }
});

特点:

  • SessionCallback 是 Spring 提供的操作 Redis 的“会话上下文”;

  • 所有命令会被打包成一组,通过 pipeline 一次性发给 Redis

  • 使用 opsForValue(), opsForHash() 等 Spring 封装的方法;

  • 自动使用配置好的 RedisTemplate 序列化器

  • expire() 的结果不会返回,只有有返回值的命令会出现在结果中。

List<Object> resourceInfoRawList = redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(@NonNull RedisOperations<K, V> operations) throws DataAccessException {
                // 此时K和V已根据RedisTemplate配置的类型确定
                for (String key : resourceInfoKeys) {
                    operations.opsForHash().entries((K) key); // 转换key类型
                    operations.expire((K) key, 20, TimeUnit.MINUTES);
                }
                return null;
            }
        });

Pipeline 的注意事项(SessionCallback 版本)

注意事项说明
executePipelined(...) 方法用于启用 pipeline不要用 execute(...),否则不会批量
return null; 是固定写法表示返回值在 executePipelined() 里获取
只有有返回值的命令会出现在结果中如:get()entries(),但 set()expire() 的返回结果不会包含在 List 中
使用 opsForXxx() 方法时,会自动使用配置的序列化器无需手动序列化 byte[]
命令顺序与返回值一一对应results.get(0) 对应第一次 get() 的返回值,以此类推
能用于事务吗?事务用 redisTemplate.execute(SessionCallback),pipeline 用 executePipelined(SessionCallback),两者不冲突

8.5 multiGet() 

List<Object> values = redisTemplate.opsForValue().multiGet(listOfKeys);
  • 这是 RedisTemplate 提供的 Value 操作中的批量获取接口

  • 你传入一个 key 列表(比如 10 个),Redis 会批量返回这些 key 对应的值

  • 它等价于 Redis 的原生命令:MGET key1 key2 key3 ...

  • 本质上是一次 Redis 请求,在 Redis 服务端是原子执行的;

  • 适合获取一组 String 类型的值(opsForValue() 操作)。


⚠️ 局限

  • 只适用于 Redis 的 String 类型;

  • 不能用于 Hash, List, Set 等复杂结构;

  • Redis 本身限制了 MGET 的 key 类型,不能跨集群 slot 使用(仅限单节点 Redis);

  • 如果你使用的是 Redis Cluster,并且 key 分布在不同 slot,MGET 会失败(报 CROSSSLOT 错误)。

9. 性能优化与监控

9.1. 连接池配置

连接池可以帮助管理 Redis 和 MySQL 的连接资源,避免频繁建立和销毁连接,从而减少系统开销,提高效率。合理的连接池配置可以防止资源耗尽,确保系统稳定运行。

9.1.1 Redis 连接池配置

在 Spring Boot 中可以通过 lettucejedis 来配置 Redis 连接池,以下示例基于 lettuce 配置连接池参数。

# Redis 连接配置
spring.redis.host=localhost
spring.redis.port=6379

# Lettuce 连接池配置
spring.redis.lettuce.pool.max-active=10  # 最大连接数
spring.redis.lettuce.pool.max-idle=5     # 最大空闲连接数
spring.redis.lettuce.pool.min-idle=2     # 最小空闲连接数
spring.redis.lettuce.pool.max-wait=1000  # 最大等待时间(毫秒)

代码解释

  • max-active:最大连接数,设置 Redis 可以同时保持的最大连接数。
  • max-idle:最大空闲连接数,连接池中可以保持的最大空闲连接数,超过的连接将会被释放。
  • min-idle:最小空闲连接数,保持的最少空闲连接数,当连接数低于此值时会创建新连接。
  • max-wait:最大等待时间,连接池获取连接的等待时间(毫秒),如果超过时间未获取到连接,则抛出异常。

通过合理配置连接池参数,可以避免 Redis 的连接池资源耗尽,提高应用的并发处理能力。

9.1.2 MySQL 连接池配置

Spring Boot 默认使用 HikariCP 作为 MySQL 数据库的连接池,可以通过以下配置进行优化。

# MySQL 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=password

# HikariCP 连接池配置
spring.datasource.hikari.maximum-pool-size=10    # 最大连接数
spring.datasource.hikari.minimum-idle=2         # 最小空闲连接数
spring.datasource.hikari.idle-timeout=30000     # 空闲连接的最大存活时间
spring.datasource.hikari.connection-timeout=30000 # 获取连接的最大等待时间(毫秒)
spring.datasource.hikari.max-lifetime=1800000   # 连接的最大存活时间(毫秒)

代码解释

  • maximum-pool-size:最大连接数,即连接池中允许的最大连接数。
  • minimum-idle:最小空闲连接数,保持的最少空闲连接数,低于此值时会创建新连接。
  • idle-timeout:空闲连接的最大存活时间,超过此时间的空闲连接将被释放。
  • connection-timeout:连接池中获取连接的最大等待时间,如果超过此时间未获取到连接,则抛出异常。
  • max-lifetime:连接的最大存活时间,避免长时间使用的连接失效。

通过配置 HikariCP 参数,可以更好地管理数据库连接池,减少系统在高并发场景下的连接资源问题。


9.2. 性能监控

性能监控可以帮助实时查看系统运行状态,识别性能瓶颈。可以使用 Spring Boot Actuator、Redis CLI 和 MySQL 监控工具来跟踪关键指标,如缓存命中率、连接池状态、数据库响应时间等。

9.2.1 使用 Spring Boot Actuator

Spring Boot Actuator 提供了丰富的监控端点,可以帮助开发者快速了解应用的运行状态。

<!-- 引入 Spring Boot Actuator 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置 Actuator 端点

application.properties 中启用 Actuator 所需的端点,以便查看 Redis 和 MySQL 的状态。

management.endpoints.web.exposure.include=health,metrics,beans
management.endpoint.health.show-details=always

使用端点

  • /actuator/health:查看应用的健康状态,包含 Redis 和 MySQL 连接状态。
  • /actuator/metrics:查看应用的各种性能指标,如内存使用、CPU、线程等信息。

9.2.2 Redis CLI 监控 Redis 状态

Redis 提供了 INFO 命令,可以查看缓存的命中率、内存使用等详细信息。

redis-cli INFO

输出中的重要字段:

  • keyspace_hitskeyspace_misses:表示缓存命中和未命中的次数,可以计算缓存命中率:命中率 = keyspace_hits / (keyspace_hits + keyspace_misses)
  • connected_clients:当前连接到 Redis 的客户端数量。
  • used_memory:Redis 使用的内存总量。
  • expired_keys:过期的键数量,有助于判断是否需要优化键的过期策略。

9.2.3 MySQL 监控工具

可以使用 MySQL 的 SHOW STATUS 命令来监控 MySQL 的性能指标,查看连接数、查询数等。

SHOW GLOBAL STATUS;

常用的状态字段:

  • Threads_connected:当前连接的客户端数量。
  • Connections:成功连接的总次数。
  • Queries:服务器处理的查询总数。
  • Slow_queries:执行时间超过 long_query_time 的慢查询总数。

这些信息有助于优化数据库性能,如增加连接池大小或优化查询语句。


9.3. Redis Keyspace Notifications

Redis Keyspace Notifications 是 Redis 提供的键变动通知功能,可以配置 Redis 将键的变动(如过期、删除、更新等)通知到客户端,以便实时监控缓存变化。

9.3.1.配置 Redis Keyspace Notifications

可以在 Redis 配置文件(redis.conf)中启用 Keyspace Notifications,或使用命令行动态配置。

# 监听所有键的过期事件和删除事件
config set notify-keyspace-events Ex
  • Ex:表示启用键的过期事件和删除事件通知。
  • K:表示所有键(keyspace)事件的通知。

9.3.2.Spring Boot 中使用 Redis Keyspace Notifications

在 Spring Boot 中使用 Redis Keyspace Notifications,可以创建一个监听器来接收 Redis 键变动的通知事件。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisConfig {

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);

        // 监听所有键的过期事件
        container.addMessageListener(listenerAdapter, new PatternTopic("__keyevent@0__:expired"));
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter() {
        return new MessageListenerAdapter(new RedisKeyExpirationListener());
    }

    public static class RedisKeyExpirationListener implements MessageListener {
        @Override
        public void onMessage(Message message, byte[] pattern) {
            String expiredKey = message.toString();
            System.out.println("Key expired: " + expiredKey);
            // 在这里处理键过期事件,例如日志记录或重新加载缓存
        }
    }
}

代码解释

  • RedisMessageListenerContainer:配置 Redis 消息监听器容器,允许监听 Redis 中的事件。
  • PatternTopic("__keyevent@0__:expired"):监听 Redis 中 expired 事件,即键过期事件。@0 表示 Redis 数据库索引。
  • RedisKeyExpirationListener:自定义监听器,监听 Redis 键的过期事件。收到过期事件时会调用 onMessage 方法,并打印或记录过期的键。

通过 Keyspace Notifications,可以实时监控 Redis 键的变化,如缓存失效、删除等事件,适合于对缓存内容有严格要求的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值