一、位图
1、结构特性
- 本质:基于 String 类型实现的位操作集合,String 底层是二进制字节数组,Bitmap 就是对这个字节数组的每一位(bit)进行操作(0 表示假 / 未发生,1 表示真 / 已发生)。
- 核心优势:极致的内存效率,1 个字节(Byte)= 8 个位(bit),存储 1 年的用户签到状态仅需 365 bit ≈ 46 字节,远超常规 String/Hash 的内存占用。
- 操作粒度:按「偏移量(offset)」操作,偏移量从 0 开始计数,支持的偏移量理论上最大为 2^32−1(约 42 亿 bit),实际上还受 Redis 单个 String 最大大小(512MB)限制,即最多可表示约 4.29 × 10^9 个 bit,足以满足绝大多数大规模状态标记场景,满足大规模数据场景。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 用户签到系统 |
存储用户每日签到状态、统计月签到天数、连续签到天数 |
内存占用极低,BITCOUNT 可快速统计签到次数,BITOP 可实现跨用户签到对比 |
| 活跃用户统计 |
按天标记用户是否登录(1 表示登录),通过 BITOP 求交集得到连续 N 天活跃用户 |
相比 Set 存储用户 ID,内存节省几个数量级,支持大规模用户群统计 |
| 布尔值状态存储 |
标记商品是否上架、订单是否已查看、消息是否已读等二元状态 |
无需额外存储键值对,单个 Bitmap 可存储海量二元状态 |
| 权限掩码控制 |
用每一位对应一个权限点(如 0 位 = 查看、1 位 = 编辑),通过位运算判断用户权限 |
位运算效率极高,权限判断仅需一次 BITAND 操作 |
3、指令
(1)核心位操作指令(增 / 改 / 查)
| 指令 |
作用细节 |
示例 |
SETBIT key offset value |
原子性设置指定偏移量的位值(value 只能是 0 或 1);偏移量超出当前 String 长度时,Redis 自动扩容并填充 0 |
SETBIT sign:user:1001 5 1(用户 1001 第 5 天签到) |
GETBIT key offset |
获取指定偏移量的位值;偏移量超出 String 长度或键不存在时,返回 0 |
GETBIT sign:user:1001 5(查询用户 1001 第 5 天是否签到) |
BITCOUNT key [start end] |
统计指定字节范围内位值为 1 的数量;start/end 为字节索引(可选,默认全量),支持负索引 |
BITCOUNT sign:user:1001 0 11(统计用户 1001 在前 12 个字节范围内,位值为 1 的数量;若业务中约定 1 bit 表示 1 天签到,则可对应统计前 96 天内的签到次数) |
(2)位运算指令(多 Bitmap 操作)
| 指令 |
作用细节 |
示例 |
BITOP AND destkey key1 key2 ... |
对多个 Bitmap 执行「按位与」运算,结果存入 destkey;用于统计同时满足多个条件的场景 |
BITOP AND active:3days sign:20240101 sign:20240102 sign:20240103(统计 3 天均活跃的用户) |
BITOP OR destkey key1 key2 ... |
对多个 Bitmap 执行「按位或」运算,结果存入 destkey;用于统计满足任一条件的场景 |
BITOP OR active:anyday sign:20240101 sign:20240102(统计 2 天内任一活跃的用户) |
BITOP XOR destkey key1 key2 ... |
对多个 Bitmap 执行「按位异或」运算,结果存入 destkey;用于统计状态变化的场景 |
BITOP XOR change:user sign:20240101 sign:20240102(统计两天签到状态变化的用户) |
BITOP NOT destkey key |
对单个 Bitmap 执行「按位非」运算,结果存入 destkey;用于取反状态(如未签到用户) |
BITOP NOT unsign:20240101 sign:20240101(统计 20240101 未签到的用户) |
(3)辅助指令
| 指令 |
作用细节 |
示例 |
BITPOS key value [start] [end] |
查找指定字节范围内第一个位值为 value(0/1)的偏移量;用于统计首次签到、首次未签到时间 |
BITPOS sign:user:1001 1(查找用户 1001 首次签到的天数) |
4、在Spring Boot 实现
java
复制代码
@Service
public class BitmapRedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 签到Key前缀(格式:sign:user:用户ID)
private static final String SIGN_KEY_PREFIX = "sign:user:";
// 活跃用户统计Key前缀(格式:active:yyyyMMdd)
private static final String ACTIVE_KEY_PREFIX = "active:";
// 1. 用户签到(设置指定偏移量为1,offset=天数(如当月第5天则传5))
public Boolean userSign(Long userId, long offset) {
String key = SIGN_KEY_PREFIX + userId;
// setBit:key、偏移量、位值(true=1,false=0)
Boolean result = stringRedisTemplate.opsForValue().setBit(key, offset, true);
// 设置Key过期时间(1年,避免长期无效数据占用内存)
stringRedisTemplate.expire(key, 365, TimeUnit.DAYS);
return result;
}
// 2. 查询用户指定日期是否签到
public Boolean isUserSigned(Long userId, long offset) {
String key = SIGN_KEY_PREFIX + userId;
return stringRedisTemplate.opsForValue().getBit(key, offset);
}
// 3. 统计用户累计签到天数
public Long countUserSignDays(Long userId) {
String key = SIGN_KEY_PREFIX + userId;
// Spring Data Redis 无直接封装BITCOUNT,需通过execute调用原生命令
return stringRedisTemplate.execute(
(RedisCallback<Long>) connection ->
connection.bitCount(key.getBytes()));
}
// 4. 统计连续3天活跃的用户(BITOP AND 运算)
public Long countContinuousActiveUser(String date1, String date2, String date3) {
String destKey = "active:continuous:3days";
String key1 = ACTIVE_KEY_PREFIX + date1;
String key2 = ACTIVE_KEY_PREFIX + date2;
String key3 = ACTIVE_KEY_PREFIX + date3;
// 执行BITOP AND运算,结果存入destKey
stringRedisTemplate.execute((connection) -> {
connection.bitOp(
org.springframework.data.redis.connection.RedisStringCommands.BitOperation.AND,
destKey.getBytes(),
key1.getBytes(),
key2.getBytes(),
key3.getBytes()
);
return null;
}, true);
// 统计结果集中位值为1的数量(即连续3天活跃的用户数)
return stringRedisTemplate.execute((connection) ->
connection.bitCount(destKey.getBytes()), true);
}
// 5. 标记用户当日活跃(offset=用户ID,避免用户ID冲突)
public Boolean markUserActive(Long userId, String date) {
String key = ACTIVE_KEY_PREFIX + date;
// 用用户ID作为偏移量,标记该用户当日活跃
return stringRedisTemplate.opsForValue().setBit(key, userId, true);
}
}
二、超日志(HyperLogLog,简称 HLL)
1、结构特性
- 本质:基于概率统计算法实现的去重计数结构,并非存储具体元素,而是存储元素的哈希值特征,用于快速估算大规模数据集的基数(不重复元素的数量)。
- 核心优势:极致的内存效率,无论数据集多大(千万级、亿级),单个 HyperLogLog 仅占用约 12 KB 内存;查询效率极高,时间复杂度 O (1)。
- 精度:存在轻微误差(标准误差约 0.81%),可满足大部分非精确去重统计场景,无法获取具体元素列表(仅能计数,不能查询成员)。
- 核心操作:添加元素、统计基数、合并多个 HLL。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 网站访问 UV 统计 |
统计每日 / 每月网站独立访客数(去重,无需记录具体访客 ID) |
相比 Set 存储访客 ID,内存占用从 GB 级降至 KB 级,支持亿级 UV 统计 |
| 接口调用独立用户统计 |
统计接口每日被多少独立用户调用,避免重复计数 |
无需存储用户 ID,高效去重,适合高并发接口统计 |
| 商品浏览独立用户统计 |
统计商品被多少独立用户浏览,辅助商品热度分析 |
内存占用低,支持多个商品 HLL 合并分析 |
| 大规模数据集去重计数 |
如日志中的独立 IP 数、独立设备数统计 |
不存储具体数据,仅计数,适合超大规模数据集 |
3、指令
(1)核心操作指令(增 / 查)
| 指令 |
作用细节 |
示例 |
PFADD key element1 element2 ... |
向 HyperLogLog 中添加一个或多个元素;元素重复不影响计数,PFADD 返回值为 1 表示 HyperLogLog 的内部寄存器状态发生变化,返回 0 表示添加的元素未引起寄存器变化(通常是重复或对估算结果无影响),返回值并不严格等同于基数是否发生变化。 |
PFADD uv:20240109 "user1" "user2" "user3"(统计 20240109 的 UV) |
PFCOUNT key1 key2 ... |
统计一个或多个 HyperLogLog 的基数(不重复元素数量);多个 key 时,返回所有 key 合并后的去重基数 |
PFCOUNT uv:20240109(统计 20240109 单日 UV);PFCOUNT uv:20240101 uv:20240102(统计 2 天累计 UV) |
(2)合并指令
| 指令 |
作用细节 |
示例 |
PFMERGE destkey key1 key2 ... |
将多个 HyperLogLog 合并为一个新的 HyperLogLog(destkey);合并后 destkey 的基数为所有源 key 基数的并集去重 |
PFMERGE uv:202401 uv:20240101 uv:20240102 ... uv:20240131(统计 2024 年 1 月总 UV) |
4、在Spring Boot 实现
java
复制代码
@Service
public class HyperLogLogRedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// UV统计Key前缀(格式:uv:yyyyMMdd)
private static final String UV_KEY_PREFIX = "uv:";
// 1. 记录访客访问(添加单个用户ID到HLL)
public Boolean recordVisitor(String date, String userId) {
String key = UV_KEY_PREFIX + date;
// PFADD 指令:添加元素到HLL
Long result = stringRedisTemplate.opsForHyperLogLog().add(key, userId);
// 设置Key过期时间(保留3个月,避免无效数据占用内存)
stringRedisTemplate.expire(key, 90, TimeUnit.DAYS);
return result != null && result > 0;
}
// 2. 批量记录访客访问(添加多个用户ID到HLL)
public Boolean batchRecordVisitor(String date, String... userIds) {
String key = UV_KEY_PREFIX + date;
Long result = stringRedisTemplate.opsForHyperLogLog().add(key, userIds);
stringRedisTemplate.expire(key, 90, TimeUnit.DAYS);
return result != null && result > 0;
}
// 3. 统计单日UV(单个HLL基数)
public Long countSingleDayUV(String date) {
String key = UV_KEY_PREFIX + date;
// PFCOUNT 指令:统计HLL基数
return stringRedisTemplate.opsForHyperLogLog().size(key);
}
// 4. 统计多个日期累计UV(多个HLL合并统计)
public Long countMultiDayUV(String... dates) {
String[] keys = Arrays.stream(dates)
.map(date -> UV_KEY_PREFIX + date)
.toArray(String[]::new);
// 批量统计多个HLL的合并基数(无需手动PFMERGE,直接统计)
return stringRedisTemplate.opsForHyperLogLog().size(keys);
}
// 5. 合并多个日期的HLL到月度总UV(永久保存月度统计)
public void mergeMonthUV(String month, String... dates) {
String destKey = "uv:" + month;
String[] sourceKeys = Arrays.stream(dates)
.map(date -> UV_KEY_PREFIX + date)
.toArray(String[]::new);
// PFMERGE 指令:合并多个HLL到目标Key
stringRedisTemplate.opsForHyperLogLog().union(destKey, sourceKeys);
// 月度统计保留1年
stringRedisTemplate.expire(destKey, 365, TimeUnit.DAYS);
}
}
三、地理位置(Geo)
1、结构特性
- 本质:基于有序集合(ZSet)实现的地理位置存储与查询结构,将经纬度转换为 52 位整数(GeoHash 算法)作为 ZSet 的分数,地理位置名称作为 ZSet 的元素,支持距离计算、范围查询等。
- 核心能力:计算两点距离、按距离 / 范围查询地理位置、按区域筛选地理位置。
- 精度限制:支持经纬度范围为
纬度 -85.05112878 到 85.05112878,经度 -180 到 180;GeoHash 算法存在一定精度误差,距离越近精度越高。
- 底层依赖:ZSet 的所有指令均可用于 Geo 键(如 ZREM 删除地理位置)。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 附近的人 / 商家查询 |
如外卖 APP 查询附近 3 公里的餐厅、社交 APP 查询附近 500 米的用户 |
支持按距离排序查询,高效返回附近地理位置 |
| 地理位置距离计算 |
如导航 APP 计算两个地点的直线距离、同城配送计算配送距离 |
内置距离计算指令,支持多种单位(米、千米、英里等) |
| 区域筛选 |
如查询某个城市 / 商圈内的所有门店、筛选指定经纬度范围内的用户 |
支持按圆形区域、矩形区域筛选,满足区域化业务需求 |
| 地理位置排序 |
如按距离从近到远排序展示周边景点、酒店 |
基于 ZSet 有序特性,无需额外排序,直接返回有序结果 |
3、指令
(1)地理位置添加指令
| 指令 |
作用细节 |
示例 |
GEOADD key longitude latitude member1 [lon2 lat2 member2 ...] |
向 Geo 集合中添加一个或多个地理位置;longitude= 经度,latitude= 纬度,member= 地理位置名称(唯一);添加成功返回新增元素数量 |
GEOADD shop:restaurant 116.403963 39.915112 "海底捞(王府井店)"(添加北京王府井海底捞) |
(2)地理位置查询指令
| 指令 |
作用细节 |
示例 |
GEOPOS key member1 member2 ... |
获取一个或多个地理位置的经纬度;返回格式为「[经度,纬度]」,成员不存在返回 nil |
GEOPOS shop:restaurant "海底捞(王府井店)"(查询海底捞经纬度) |
GEODIST key member1 member2 [unit] |
计算两个地理位置之间的距离;unit 可选(m = 米(默认)、km = 千米、mi = 英里、ft = 英尺);返回距离值(浮点型),成员不存在返回 nil |
GEODIST shop:restaurant "海底捞(王府井店)" "麦当劳(天安门店)" km(计算两家店的千米距离) |
GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [WITHRANK] [COUNT n] |
以指定经纬度为中心,查询半径 radius 内的地理位置;WITHCOORD 返回经纬度,WITHDIST 返回与中心的距离,WITHRANK 返回排名,COUNT n 限制返回数量 |
GEORADIUS shop:restaurant 116.40 39.91 3 km WITHDIST COUNT 10(查询天安门周边 3 公里内 10 家餐厅及距离) |
GEORADIUSBYMEMBER key member radius unit [WITHCOORD] [WITHDIST] [WITHRANK] [COUNT n] |
以指定成员为中心,查询半径 radius 内的地理位置;参数规则与 GEORADIUS 一致,更适合以已有地点为中心的查询。(说明:从 Redis 6.2 起,官方推荐使用 GEOSEARCH / GEOSEARCHSTORE 替代 GEORADIUS 与 GEORADIUSBYMEMBER,但二者当前仍保持兼容。) |
GEORADIUSBYMEMBER shop:restaurant "海底捞(王府井店)" 2 km COUNT 5(查询海底捞周边 2 公里内 5 家餐厅) |
GEOHASH key member1 member2 ... |
获取一个或多个地理位置的 GeoHash 编码;编码越长精度越高,相同前缀表示地理位置相近 |
GEOHASH shop:restaurant "海底捞(王府井店)"(查询海底捞的 GeoHash 编码) |
(3)地理位置删除指令
| 指令 |
作用细节 |
示例 |
ZREM key member1 member2 ... |
Geo 无专属删除指令,直接使用 ZSet 的 ZREM 指令删除指定地理位置成员 |
ZREM shop:restaurant "海底捞(王府井店)"(删除王府井海底捞记录) |
4、在Spring Boot 实现
java
复制代码
@Service
public class GeoRedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 餐厅Geo集合Key
private static final String SHOP_RESTAURANT_KEY = "shop:restaurant";
// 用户地理位置Geo集合Key
private static final String USER_LOCATION_KEY = "user:location";
// 1. 添加餐厅地理位置
public Long addRestaurant(String restaurantName, double longitude, double latitude) {
GeoOperations<String, String> geoOps = stringRedisTemplate.opsForGeo();
// 构建地理位置对象(经度、纬度、名称)
RedisGeoCommands.GeoLocation<String> location = new RedisGeoCommands.GeoLocation<>(
restaurantName,
new org.springframework.data.redis.connection.RedisGeoCommands.Point(longitude, latitude)
);
// GEOADD 指令:添加单个地理位置
return geoOps.add(SHOP_RESTAURANT_KEY, location);
}
// 2. 查询餐厅的经纬度
public List<RedisGeoCommands.GeoLocation<String>> getRestaurantPos(String restaurantName) {
GeoOperations<String, String> geoOps = stringRedisTemplate.opsForGeo();
// GEOPOS 指令:获取地理位置经纬度
return geoOps.position(SHOP_RESTAURANT_KEY, restaurantName);
}
// 3. 计算两家餐厅之间的距离(单位:千米)
public Double calculateRestaurantDistance(String restaurant1, String restaurant2) {
GeoOperations<String, String> geoOps = stringRedisTemplate.opsForGeo();
// GEODIST 指令:计算距离,指定单位为千米
RedisGeoCommands.Distance distance = geoOps.distance(
SHOP_RESTAURANT_KEY,
restaurant1,
restaurant2,
RedisGeoCommands.DistanceUnit.KILOMETERS
);
return distance != null ? distance.getValue() : null;
}
// 4. 查询指定经纬度周边3公里内的餐厅(带距离,最多返回10家)
public List<RedisGeoCommands.GeoLocation<String>> getNearbyRestaurant(double longitude, double latitude) {
GeoOperations<String, String> geoOps = stringRedisTemplate.opsForGeo();
// 构建查询参数:中心经纬度、半径、单位
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance() // 包含与中心的距离
.limit(10) // 最多返回10条
.sortAscending(); // 按距离从近到远排序
// GEORADIUS 指令:查询周边地理位置
RedisGeoCommands.Circle circle = new RedisGeoCommands.Circle(
new RedisGeoCommands.Point(longitude, latitude),
new RedisGeoCommands.Distance(3, RedisGeoCommands.DistanceUnit.KILOMETERS)
);
return geoOps.radius(SHOP_RESTAURANT_KEY, circle, args);
}
// 5. 删除指定餐厅地理位置
public Long deleteRestaurant(String restaurantName) {
// Geo无专属删除指令,使用ZSet的remove方法(底层为ZREM)
return stringRedisTemplate.opsForZSet().remove(SHOP_RESTAURANT_KEY, restaurantName);
}
}
四、发布订阅(Publish/Subscribe,简称 Pub/Sub)
1、结构特性
- 本质:基于「频道(Channel)」的消息通信模型,采用「发布者 - 订阅者」模式,发布者向指定频道发送消息,所有订阅该频道的订阅者都会收到消息(一对多通信)。
- 核心特性:
- 无消息持久化:若订阅者离线,期间发布的消息会丢失,无法回溯;
- 实时性:消息发布后立即推送给在线订阅者,实时性高;
- 广播机制:同一频道的所有订阅者都会收到相同消息;
- 支持模式订阅:可订阅符合指定模式的多个频道(如
news:* 订阅所有新闻相关频道)。
- 核心角色:发布者(Publisher)、频道(Channel)、订阅者(Subscriber)、模式匹配器(Pattern Matcher)。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 实时消息推送 |
如系统通知、订单状态变更推送、聊天消息实时同步 |
实时性高,消息发布后立即推送给订阅者,无需轮询 |
| 系统日志广播 |
如将应用日志、异常日志广播到多个日志收集服务,实现日志分布式处理 |
一对多广播,无需逐个发送日志,简化日志收集架构 |
| 事件通知 |
如微服务架构中,服务 A 完成数据更新后,通知服务 B/C 进行后续处理 |
解耦服务之间的依赖,无需同步调用,提高系统容错性 |
| 实时数据同步 |
如股票行情、实时监控数据的多终端同步展示 |
支持大规模订阅者,实时推送数据变更,保证数据一致性 |
3、指令
(1)订阅者指令(订阅 / 取消订阅)
| 指令 |
作用细节 |
示例 |
SUBSCRIBE channel1 channel2 ... |
订阅一个或多个指定频道;订阅后进入阻塞状态,持续接收该频道的消息 |
SUBSCRIBE order:change user:notify(订阅订单变更和用户通知频道) |
PSUBSCRIBE pattern1 pattern2 ... |
订阅符合指定模式的所有频道;模式支持 *(匹配任意字符)、?(匹配单个字符)、[](匹配指定范围) |
PSUBSCRIBE news:*(订阅所有以 news: 开头的频道) |
UNSUBSCRIBE [channel1 channel2 ...] |
取消订阅一个或多个指定频道;无参数时取消订阅所有频道 |
UNSUBSCRIBE order:change(取消订阅订单变更频道) |
PUNSUBSCRIBE [pattern1 pattern2 ...] |
取消订阅符合指定模式的所有频道;无参数时取消订阅所有模式频道 |
PUNSUBSCRIBE news:*(取消订阅所有新闻相关频道) |
(2)发布者指令(发布消息)
| 指令 |
作用细节 |
示例 |
PUBLISH channel message |
向指定频道发布一条消息;返回接收该消息的在线订阅者数量(离线订阅者无法接收) |
PUBLISH order:change "订单1001已支付"(向订单变更频道发布消息) |
(3)辅助指令(查询频道 / 订阅者信息)
| 指令 |
作用细节 |
示例 |
PUBSUB CHANNELS [pattern] |
查询当前存在的所有频道(或符合指定模式的频道) |
PUBSUB CHANNELS order:*(查询所有订单相关频道) |
PUBSUB NUMSUB [channel1 channel2 ...] |
查询一个或多个频道的在线订阅者数量 |
PUBSUB NUMSUB order:change(查询订单变更频道的在线订阅者数) |
PUBSUB NUMPAT |
查询当前模式订阅的总数 |
PUBSUB NUMPAT(查询所有模式订阅的数量) |
4、在Spring Boot 实现
步骤 1:自定义消息监听器(订阅者)
java
复制代码
/**
* 订单变更频道消息监听器
*/
@Component
public class OrderChangeMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
// 转换消息内容
String channel = new String(message.getChannel());
String messageContent = new String(message.getBody());
// 业务处理:如更新订单状态、推送通知等
System.out.println("收到频道 [" + channel + "] 的消息:" + messageContent);
}
}
步骤 2:配置消息监听容器
java
复制代码
/**
* Redis 发布订阅配置类
*/
@Configuration
public class RedisPubSubConfig {
@Resource
private RedisConnectionFactory redisConnectionFactory;
@Resource
private OrderChangeMessageListener orderChangeMessageListener;
/**
* 消息监听容器(核心配置,绑定频道与监听器)
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置Redis连接工厂
container.setConnectionFactory(redisConnectionFactory);
// 绑定「订单变更频道」与对应的监听器
container.addMessageListener(orderChangeMessageListener, new ChannelTopic("order:change"));
return container;
}
}
步骤 3:实现发布者(发送消息)
java
复制代码
/**
* Redis 发布者服务
*/
@Service
public class RedisPublisherService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 1. 向指定频道发布消息
public Long publishMessage(String channel, String message) {
// PUBLISH 指令:向频道发送消息,返回在线订阅者数量
return stringRedisTemplate.convertAndSend(channel, message);
}
// 2. 向订单变更频道发布订单状态消息
public Long publishOrderChangeMessage(String orderId, String orderStatus) {
String channel = "order:change";
String message = String.format("订单[%s]状态变更为:%s", orderId, orderStatus);
return publishMessage(channel, message);
}
}
五、Stream(流)
1、结构特性
- 本质:Redis 5.0+ 新增的可靠消息队列结构,基于「日志流」模型实现,支持消息持久化、分区消费、消息回溯、阻塞消费,解决了 Pub/Sub 消息丢失的问题。
- 核心特性:
- 消息持久化:所有消息均持久化到磁盘,即使 Redis 重启,消息也不会丢失;
- 唯一消息 ID:每条消息自动生成(或手动指定)唯一 ID(格式:
时间戳-序列号,如 1736200000-0);
- 消费者组:支持多个消费者组并行消费,每个消费者组维护自己的消费偏移量,互不干扰;
- 消息确认:消费者获取消息后需手动 ACK,未 ACK 的消息可重新消费,保证消息可靠投递;
- 消息回溯:支持按消息 ID、时间范围回溯历史消息。
- 核心组件:消息(Entry)、消费者(Consumer)、消费者组(Consumer Group)、偏移量(Offset)。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 可靠消息队列 |
如订单创建、秒杀消息、支付回调等需要确保消息不丢失的场景 |
消息持久化 + ACK 机制,保证消息至少投递一次 |
| 分布式任务分发 |
如微服务架构中,任务调度中心向多个执行节点分发任务,需确保任务不重复、不丢失 |
消费者组 + 偏移量管理,支持任务分片消费 |
| 日志存储与回溯 |
如应用操作日志、系统运行日志的存储,支持按时间范围查询历史日志 |
消息按时间戳排序,支持范围查询与回溯 |
| 高并发消息处理 |
如电商大促期间的订单消息、库存消息,需要多个消费者并行处理 |
支持多个消费者组、多个消费者并行消费,提高处理吞吐量 |
3、指令
(1)消息添加指令
| 指令 |
作用细节 |
示例 |
|
| XADD key [MAXLEN [~] count] * |
id field1 value1 field2 value2 ...` |
向 Stream 中添加消息;* 表示自动生成消息 ID,id 手动指定;MAXLEN 限制 Stream 最大长度(~ 表示近似截断,提高性能) |
XADD order:stream MAXLEN ~ 10000 * orderId 1001 status paid(添加订单消息,最多保留 1 万条) |
(2)消息查询指令
| 指令 |
作用细节 |
示例 |
XRANGE key start end [COUNT n] |
按消息 ID 范围查询消息;start= 最小值(- 表示最早),end= 最大值(+ 表示最晚),COUNT 限制返回数量 |
XRANGE order:stream - + COUNT 10(查询前 10 条消息) |
XREVRANGE key end start [COUNT n] |
按消息 ID 倒序查询消息;参数规则与 XRANGE 一致 |
XREVRANGE order:stream + - COUNT 5(查询最新 5 条消息) |
XLEN key |
获取 Stream 中的消息总数 |
XLEN order:stream(查询订单消息总数) |
(3)消费者组指令
| 指令 |
作用细节 |
示例 |
| XGROUP CREATE key groupName id $ [MKSTREAM] |
创建消费者组;$ 表示从最新消息开始消费,id 表示从指定消息 ID 开始,MKSTREAM 表示若 Stream 不存在则创建 |
XGROUP CREATE order:stream group1 $ MKSTREAM(创建消费者组 group1,从最新消息开始消费XGROUP CREATE order:stream group1 $ MKSTREAM(创建消费者组 group1,从最新消息开始消费 |
XREADGROUP GROUP groupName consumerName [COUNT n] [BLOCK ms] STREAMS key1 [key2 ...] id1 [id2 ...] |
消费者从消费者组中获取消息;BLOCK 表示阻塞消费,> 表示获取未被消费过的消息 |
XREADGROUP GROUP group1 consumer1 BLOCK 5000 STREAMS order:stream >(消费者 1 阻塞获取未消费消息) |
XACK key groupName id1 id2 ... |
手动确认消息消费完成;确认后消息会从消费者组的待处理列表(PEL,Pending Entries List)中移除,消息本身仍保留在 Stream 中,直到被 XDEL 或 XTRIM 清理 |
XACK order:stream group1 1736200000-0(确认消息 1736200000-0 消费完成) |
XGROUP DESTROY key groupName |
删除指定消费者组 |
XGROUP DESTROY order:stream group1(删除消费者组 group1) |
(4)消息删除与截断指令
| 指令 |
作用细节 |
示例 |
XDEL key id1 id2 ... |
删除 Stream 中的指定消息(标记为删除,实际在截断时清理) |
XDEL order:stream 1736200000-0(删除消息 1736200000-0) |
XTRIM key MAXLEN [~] count |
截断 Stream,保留指定数量的最新消息 |
XTRIM order:stream MAXLEN ~ 5000(保留最新 5000 条消息) |
5.4 Spring Boot 实现
java
复制代码
@Service
public class StreamRedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// Stream 键
private static final String ORDER_STREAM_KEY = "order:stream";
// 消费者组名称
private static final String CONSUMER_GROUP_NAME = "order_group";
// 消费者名称前缀
private static final String CONSUMER_PREFIX = "consumer_";
// 1. 向 Stream 中添加订单消息
public String addOrderMessage(String orderId, String status) {
// 构建消息内容
Map<String, String> messageMap = new HashMap<>();
messageMap.put("orderId", orderId);
messageMap.put("status", status);
// 转换为 Stream 消息条目
Record<String, String> record = StreamRecords.newRecord()
.in(ORDER_STREAM_KEY)
.ofStrings(messageMap)
.withMaxlenApproximate(10000); // 最多保留1万条消息
// XADD 指令:添加消息,返回消息 ID
RecordId recordId = stringRedisTemplate.opsForStream().add(record);
return recordId != null ? recordId.getValue() : null;
}
// 2. 创建消费者组(若不存在)
public void createConsumerGroup() {
try {
// XGROUP CREATE 指令:创建消费者组,从最新消息开始消费
stringRedisTemplate.opsForStream().createGroup(
ORDER_STREAM_KEY,
ReadOffset.latest(),
CONSUMER_GROUP_NAME
);
} catch (Exception e) {
// 消费者组已存在时忽略异常
System.out.println("消费者组已存在:" + e.getMessage());
}
}
// 3. 消费者获取消息(阻塞消费,超时5秒)
public List<MapRecord<String, String, String>> consumeMessage(int consumerId) {
String consumerName = CONSUMER_PREFIX + consumerId;
// 构建消费参数
StreamReadOptions options = StreamReadOptions.empty()
.block(5, TimeUnit.SECONDS) // 阻塞5秒
.count(10); // 每次最多获取10条消息
// XREADGROUP 指令:从消费者组中获取未消费消息
return stringRedisTemplate.opsForStream().read(
Consumer.from(CONSUMER_GROUP_NAME, consumerName),
options,
StreamOffset.create(ORDER_STREAM_KEY, ReadOffset.lastConsumed())
);
}
// 4. 确认消息消费完成
public Long ackMessage(String... messageIds) {
// XACK 指令:确认消息消费
return stringRedisTemplate.opsForStream().acknowledge(
ORDER_STREAM_KEY,
CONSUMER_GROUP_NAME,
messageIds
);
}
// 5. 查询 Stream 中的最新5条消息
public List<MapRecord<String, String, String>> getLatestMessages() {
// XREVRANGE 指令:倒序查询最新5条消息
return stringRedisTemplate.opsForStream().reverseRange(
ORDER_STREAM_KEY,
Range.unbounded(),
Limit.limit().count(5)
);
}
}
六、布隆过滤器(Bloom Filter)
1、结构特性
- 本质:基于位图和多个哈希函数实现的高效去重结构,用于快速判断「一个元素是否存在于一个大规模数据集中」,不存在假阴性(判断不存在则一定不存在),存在一定假阳性(判断存在可能不存在,可通过参数优化)。
- 核心优势:极致的内存效率和查询效率,在合理参数配置下,存储 1 亿级元素、假阳性率约 1% 的布隆过滤器,理论内存消耗约在十余 MB 量级,具体取决于位图大小和哈希函数配置,查询和添加操作时间复杂度均为 O (k)(k 为哈希函数个数,通常为 5-10)。
- 核心特性:
- 仅支持「添加」和「查询是否存在」,不支持「删除」元素(普通布隆过滤器);
- 假阳性率可通过「位图大小」「哈希函数个数」「元素总量」提前计算优化;
- 基于 Bitmap 实现,Redis 中可通过「RedisBloom」模块(第三方扩展)或手动基于 Bitmap 实现。
- 注意:Redis 原生不支持布隆过滤器,需安装 RedisBloom 扩展,或通过 Spring Boot 整合 Redisson 实现(更便捷)。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 缓存穿透防护 |
如查询不存在的用户 ID、商品 ID 时,先通过布隆过滤器判断,避免穿透到数据库 |
快速判断元素不存在,拦截无效查询,保护数据库 |
| 大规模数据去重 |
如邮件黑名单、垃圾短信号码库、爬虫 URL 去重,判断元素是否已存在 |
内存效率极高,支持亿级数据去重,查询速度快 |
| 防止重复操作 |
如防止用户重复下单、重复点赞、重复提交表单,判断操作是否已执行 |
快速去重,避免并发重复操作,提高系统性能 |
| 大规模数据集存在性判断 |
如字典词库、用户手机号库,判断某个值是否存在于数据集中 |
无需存储具体数据,仅需存储位图,节省大量内存 |
3、指令(RedisBloom 扩展)
| 指令 |
作用细节 |
示例 |
BF.ADD key element |
向布隆过滤器中添加一个元素;添加成功返回 1,元素已存在返回 0 |
BF.ADD blacklist:phone 13800138000(添加黑名单手机号) |
BF.MADD key element1 element2 ... |
批量向布隆过滤器中添加元素;返回每个元素的添加结果(1 = 成功,0 = 已存在) |
BF.MADD blacklist:phone 13800138000 13900139000(批量添加黑名单手机号) |
BF.EXISTS key element |
判断元素是否存在于布隆过滤器中;返回 1 = 可能存在,0 = 一定不存在 |
BF.EXISTS blacklist:phone 13800138000(判断手机号是否在黑名单中) |
BF.MEXISTS key element1 element2 ... |
批量判断元素是否存在;返回每个元素的判断结果(1 = 可能存在,0 = 一定不存在) |
BF.MEXISTS blacklist:phone 13800138000 13900139000(批量判断手机号) |
BF.RESERVE key error_rate capacity [EXPANSION] |
预创建布隆过滤器;error_rate= 假阳性率(默认 0.01),capacity= 预计元素总量 |
BF.RESERVE blacklist:phone 0.001 1000000(预创建假阳性率 0.1%、容量 100 万的布隆过滤器) |
4、Spring Boot 实现(整合 Redisson)
步骤 1:引入 Redisson 依赖
XML
复制代码
<!-- pom.xml -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
步骤 2:配置 Redisson(application.yml)
XML
复制代码
spring:
redis:
host: localhost
port: 6379
password:
database: 0
# Redisson 配置
redisson:
singleServerConfig:
address: redis://localhost:6379
password:
database: 0
步骤 3:实现布隆过滤器操作
java
复制代码
@Service
public class BloomFilterRedisService {
@Resource
private RedissonClient redissonClient;
// 手机号黑名单布隆过滤器名称
private static final String PHONE_BLACKLIST_BLOOM = "blacklist:phone";
// 预计元素总量
private static final long CAPACITY = 1000000L;
// 假阳性率(0.1%)
private static final double ERROR_RATE = 0.001;
// 1. 获取/初始化布隆过滤器
private RBloomFilter<String> getPhoneBlacklistBloomFilter() {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(PHONE_BLACKLIST_BLOOM);
// 初始化布隆过滤器(仅首次调用时生效)
bloomFilter.tryInit(CAPACITY, ERROR_RATE);
return bloomFilter;
}
// 2. 向黑名单中添加单个手机号
public Boolean addPhoneToBlacklist(String phone) {
RBloomFilter<String> bloomFilter = getPhoneBlacklistBloomFilter();
return bloomFilter.add(phone);
}
// 3. 批量向黑名单中添加手机号
public Boolean[] batchAddPhoneToBlacklist(String... phones) {
RBloomFilter<String> bloomFilter = getPhoneBlacklistBloomFilter();
Boolean[] results = new Boolean[phones.length];
for (int i = 0; i < phones.length; i++) {
results[i] = bloomFilter.add(phones[i]);
}
return results;
}
// 4. 判断手机号是否在黑名单中
public Boolean isPhoneInBlacklist(String phone) {
RBloomFilter<String> bloomFilter = getPhoneBlacklistBloomFilter();
// 返回 true=可能存在(假阳性),false=一定不存在
return bloomFilter.contains(phone);
}
// 5. 缓存穿透防护示例:判断商品ID是否存在
public Boolean isGoodsExist(String goodsId) {
String bloomFilterName = "goods:exist";
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(bloomFilterName);
bloomFilter.tryInit(5000000L, 0.005); // 500万商品,假阳性率 0.5%
return bloomFilter.contains(goodsId);
}
}
七、Lua脚本
1、结构特性
本质:Lua 是一种轻量级脚本语言,Redis 内置 Lua 解释器,支持通过 Lua 脚本组合多个 Redis 指令,实现复杂业务逻辑。
核心优势:原子性执行 (脚本内所有指令要么全部执行,要么全部不执行,无中间状态)、减少网络开销 (多指令合并为一次网络请求)、自定义复杂逻辑 (突破 Redis 原生指令限制,实现自定义业务规则)。Lua 脚本的原子性由 Redis 本身保证,与 Spring 事务机制无关;execute(..., true) 仅表示在回调中直接使用底层 Redis 连接。
执行机制:Redis 在执行 Lua 脚本时,会在单线程上下文中原子性地执行脚本内的所有命令,脚本执行期间不会被其他客户端命令打断。因此 Lua 脚本应保持简洁高效,避免长时间运行(官方建议执行时间不超过 10ms),否则会影响后续命令的响应延迟。
支持特性:可调用 Redis 原生指令、访问 Redis 数据、使用 Lua 语法(条件判断、循环、函数等),脚本执行结果可返回给客户端。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 原子性复合操作 |
如 "先查询再更新""判断条件后执行操作"(如库存扣减:查询库存 > 0 则扣减,否则返回失败) |
脚本原子性避免并发竞争,无需额外加锁 |
| 批量数据操作 |
如批量删除符合指定前缀的 Key、批量更新多个 Key 的值、批量统计数据 |
合并多指令,减少网络往返次数,提高效率 |
| 自定义业务逻辑 |
如积分计算(消费金额→积分转换 + 积分上限判断 + 积分累加)、权限校验(多条件组合判断) |
突破 Redis 原生指令限制,灵活实现复杂业务规则 |
| 分布式锁增强 |
如带过期时间的分布式锁(加锁 + 过期时间设置 + 唯一标识判断)、锁续约逻辑 |
原子性保证锁操作的一致性,避免锁泄露 |
3、核心指令
| 指令 |
作用细节 |
示例 |
| EVAL script numkeys key1 key2 ... arg1 arg2 ... |
执行 Lua 脚本;script 为脚本内容,numkeys 为 Key 参数个数,key1/key2 为 Redis Key(脚本中通过 KEYS [1] 访问),arg1/arg2 为普通参数(脚本中通过 ARGV [1] 访问) |
EVAL "return KEYS [1]..ARGV [1]" 1 user:1001 "hello"(拼接 Key 和参数) |
| EVALSHA sha1 numkeys key1 ... arg1 ... |
执行已缓存的 Lua 脚本(通过脚本 SHA1 校验值);避免重复传输脚本内容,提高执行效率 |
先通过 SCRIPT LOAD 缓存脚本,再用 EVALSHA 执行 |
| SCRIPT LOAD script |
缓存 Lua 脚本到 Redis,返回脚本的 SHA1 校验值 |
SCRIPT LOAD "return redis.call ('GET', KEYS [1])"(缓存获取 Key 值的脚本) |
| SCRIPT EXISTS sha1 sha2 ... |
判断多个脚本 SHA1 值是否已缓存 |
SCRIPT EXISTS "abc123..."(判断脚本是否缓存) |
| SCRIPT FLUSH |
清空所有缓存的 Lua 脚本 |
SCRIPT FLUSH(重启 Redis 也会清空) |
| SCRIPT KILL |
用于终止当前正在执行的 Lua 脚本;仅当脚本尚未执行任何写操作时才允许终止,若脚本已修改数据,Redis 会拒绝终止请求。 |
脚本执行超时后,通过 SCRIPT KILL 终止 |
4、在Spring Boot 实现
(1)基础脚本执行(直接执行脚本字符串)
java
复制代码
@Service
public class LuaScriptService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 1. 原子性库存扣减脚本(库存>0 则扣减,返回剩余库存;否则返回-1)
private static final String STOCK_DEDUCT_SCRIPT = """
local stockKey = KEYS[1]
local deductNum = tonumber(ARGV[1])
-- 查询当前库存
local currentStock = redis.call('GET', stockKey)
if not currentStock then
return -1 -- 库存 Key 不存在
end
currentStock = tonumber(currentStock)
if currentStock < deductNum then
return -1 -- 库存不足
end
-- 扣减库存
redis.call('DECRBY', stockKey, deductNum)
-- 返回剩余库存
return currentStock - deductNum
""";
// 执行库存扣减
public Long deductStock(String stockKey, int deductNum) {
// 调用 Lua 脚本:KEYS=[stockKey], ARGV=[deductNum]
return stringRedisTemplate.execute(
(connection) -> {
Object result = connection.eval(
STOCK_DEDUCT_SCRIPT.getBytes(),
org.springframework.data.redis.connection.RedisStringCommands.RedisScriptReturnType.INTEGER,
1, // numkeys=1
stockKey.getBytes(),
String.valueOf(deductNum).getBytes()
);
return result != null ? Long.parseLong(result.toString()) : -1L;
},
true // 启用事务支持(脚本原子性)
);
}
// 2. 批量删除指定前缀的 Key(如 "order:1001" "order:1002",前缀为 "order:")
private static final String BATCH_DELETE_PREFIX_SCRIPT = """
local prefix = KEYS[1]
local cursor = "0"
repeat
-- 扫描符合前缀的 Key,每次返回 100 个
local res = redis.call('SCAN', cursor, 'MATCH', prefix..'*', 'COUNT', 100)
cursor = res[1]
local keys = res[2]
if #keys > 0 then
redis.call('DEL', unpack(keys)) -- 批量删除
end
until cursor == "0"
return "success"
""";
// 执行批量删除前缀 Key
public String batchDeleteByPrefix(String prefix) {
return stringRedisTemplate.execute(
(connection) -> {
Object result = connection.eval(
BATCH_DELETE_PREFIX_SCRIPT.getBytes(),
org.springframework.data.redis.connection.RedisStringCommands.RedisScriptReturnType.VALUE,
1,
prefix.getBytes()
);
return result != null ? result.toString() : "fail";
},
true
);
}
}
(2)缓存脚本执行(通过 SHA1 执行,优化性能)
java
复制代码
@Service
public class CachedLuaScriptService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 缓存脚本 SHA1 值(避免重复计算)
private final Map<String, String> scriptShaMap = new HashMap<>();
// 脚本内容:积分累加(判断是否超过上限)
private static final String POINT_ADD_SCRIPT = """
local pointKey = KEYS[1]
local addPoint = tonumber(ARGV[1])
local maxPoint = tonumber(ARGV[2])
-- 查询当前积分
local currentPoint = redis.call('GET', pointKey)
if not currentPoint then
currentPoint = 0
else
currentPoint = tonumber(currentPoint)
end
-- 计算新积分(不超过上限)
local newPoint = math.min(currentPoint + addPoint, maxPoint)
-- 更新积分
redis.call('SET', pointKey, newPoint)
return newPoint
""";
// 初始化:缓存脚本到 Redis,获取 SHA1 值
public void initScript() {
String sha = stringRedisTemplate.execute(
(connection) -> new String(connection.scriptLoad(POINT_ADD_SCRIPT.getBytes())),
true
);
scriptShaMap.put("pointAdd", sha);
}
// 通过 SHA1 执行缓存的脚本
public Long addPoint(String pointKey, int addPoint, int maxPoint) {
String sha = scriptShaMap.get("pointAdd");
if (sha == null) {
initScript();
sha = scriptShaMap.get("pointAdd");
}
return stringRedisTemplate.execute(
(connection) -> {
Object result = connection.evalSha(
sha.getBytes(),
org.springframework.data.redis.connection.RedisStringCommands.RedisScriptReturnType.INTEGER,
1, // numkeys=1
pointKey.getBytes(),
String.valueOf(addPoint).getBytes(),
String.valueOf(maxPoint).getBytes()
);
return result != null ? Long.parseLong(result.toString()) : 0L;
},
true
);
}
}
5、注意事项
- 脚本原子性:脚本执行期间 Redis 会阻塞其他请求,因此脚本需简洁,避免循环次数过多或复杂计算。
- 数据类型:Lua 脚本中需注意 Redis 数据与 Lua 数据的类型转换(如 Redis 字符串转 Lua 数字用
tonumber())。
- 脚本缓存:频繁执行的脚本建议通过
SCRIPT LOAD 缓存,用 EVALSHA 执行,减少脚本传输开销。
- 错误处理:脚本中可通过
redis.call() 调用指令(执行失败抛出异常),或 redis.pcall()(执行失败返回错误信息),需做好异常捕获。
八、Redis Key 过期与事件通知
1、结构特性
本质:Redis 支持为 Key 设置过期时间(TTL),过期后 Key 会被自动删除;同时提供事件通知机制,当 Key 发生过期、删除、修改等事件时,Redis 可主动推送通知给订阅的客户端。过期删除机制:采用「惰性删除 + 定期删除」结合:
- 惰性删除:访问过期 Key 时才检查并删除,不浪费 CPU 资源,但可能占用内存;
- 定期删除:Redis 会周期性地执行过期扫描(默认每秒约 10 次,由
hz 参数控制),随机抽取部分带过期时间的 Key 进行检查和删除,以平衡内存回收效率与 CPU 开销。事件通知机制:基于 Pub/Sub 模型,Redis 作为发布者,客户端订阅指定事件频道(如 __keyevent@0__:expired 表示 0 号数据库的 Key 过期事件),当事件触发时接收通知。
2、应用场景
| 场景类型 |
具体说明 |
适配原因 |
| 过期数据清理回调 |
如订单超时取消(订单 Key 过期后,触发通知执行取消订单、恢复库存操作)、临时 Token 失效通知 |
无需定时轮询,Key 过期后主动触发业务逻辑,实时性高 |
| 数据变更监控 |
如监控热点商品库存修改、用户积分变动、配置项更新,实时感知数据变化并同步到其他系统 |
无需主动查询,事件触发后被动接收通知,减少资源占用 |
| 临时数据生命周期管理 |
如缓存数据过期后自动刷新(接收过期通知后,重新查询数据库并缓存)、临时会话失效处理 |
自动化管理数据生命周期,无需手动干预 |
| 审计与日志记录 |
如记录 Key 的删除、修改、过期事件,用于数据审计、操作追溯、系统监控 |
自动捕获关键事件,简化日志记录逻辑 |
3、核心指令
(1)Key 过期相关指令
| 指令 |
作用细节 |
示例 |
| EXPIRE key seconds |
为 Key 设置过期时间(秒),返回 1 成功,0 失败(Key 不存在) |
EXPIRE order:1001 3600(订单 1 小时后过期) |
| PEXPIRE key milliseconds |
为 Key 设置过期时间(毫秒) |
PEXPIRE token:user1 180000(Token 3 分钟后过期) |
| EXPIREAT key timestamp |
为 Key 设置过期时间戳(秒级),timestamp 为 Unix 时间戳 |
EXPIREAT goods:stock 1735689600(指定时间点过期) |
| PEXPIREAT key timestamp |
为 Key 设置过期时间戳(毫秒级) |
PEXPIREAT cache:data 1735689600000 |
| TTL key |
获取 Key 剩余过期时间(秒),-1 无过期,-2 Key 不存在 |
TTL order:1001 |
| PTTL key |
获取 Key 剩余过期时间(毫秒) |
PTTL token:user1 |
| PERSIST key |
移除 Key 的过期时间,使其永久有效,返回 1 成功,0 失败 |
PERSIST order:1001(取消订单过期) |
(2)事件通知相关指令
| 指令 |
作用细节 |
示例 |
| CONFIG SET notify-keyspace-events flags |
开启事件通知,flags 为事件类型标识(如 Ex 表示过期事件,K 表示 Key 空间事件) |
CONFIG SET notify-keyspace-events ExK(开启过期事件和 Key 空间事件)实际生产中,若仅关心过期事件,通常只需开启 Ex 即可 |
| SUBSCRIBE keyevent@db:event |
订阅指定数据库的指定事件;db 为数据库编号(默认 0),event 为事件类型(expired = 过期、del = 删除、set = 修改等) |
SUBSCRIBE keyevent@0:expired(订阅 0 号库 Key 过期事件) |
| PSUBSCRIBE keyevent@*:expired |
模式订阅所有数据库的过期事件 |
PSUBSCRIBE keyevent@*:expired |
4、在Spring Boot 实现
(1)第一步:开启 Redis 事件通知(两种方式)
-
临时开启(Redis 重启后失效):执行指令 CONFIG SET notify-keyspace-events ExK;
-
永久开启(修改 redis.conf 配置文件):
notify-keyspace-events ExK # 保存后重启 Redis
(2)第二步:Spring Boot 订阅事件通知
java
复制代码
/**
* Key 过期事件监听器
*/
@Component
public class KeyExpireListener implements MessageListener {
@Resource
private RedisMessageListenerContainer redisMessageListenerContainer;
// 订阅的频道:0 号数据库的 Key 过期事件
private static final String EXPIRE_CHANNEL = "__keyevent@0__:expired";
// 项目启动后订阅事件
@PostConstruct
public void subscribe() {
redisMessageListenerContainer.addMessageListener(this, new ChannelTopic(EXPIRE_CHANNEL));
}
// 事件触发时执行的逻辑
@Override
public void onMessage(Message message, byte[] pattern) {
// message.getBody():过期的 Key 名称
String expiredKey = new String(message.getBody());
System.out.println("Key 过期:" + expiredKey);
// 业务逻辑:根据 Key 前缀执行不同操作
if (expiredKey.startsWith("order:")) {
// 订单过期:取消订单、恢复库存
cancelOrder(expiredKey);
} else if (expiredKey.startsWith("token:")) {
// Token 过期:清理用户会话
clearUserSession(expiredKey);
}
}
// 取消订单业务逻辑
private void cancelOrder(String orderKey) {
String orderId = orderKey.split(":")[1];
// 执行取消订单操作(如更新订单状态、恢复库存等)
System.out.println("取消超时订单:" + orderId);
}
// 清理用户会话
private void clearUserSession(String tokenKey) {
String userId = tokenKey.split(":")[1];
// 执行清理会话操作(如删除用户缓存的会话信息)
System.out.println("清理用户过期会话:" + userId);
}
}
(3)第三步:Key 过期与事件通知综合使用(示例:订单超时取消)
java
复制代码
@Service
public class OrderService {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 创建订单:设置订单 Key 并指定过期时间(30 分钟)
public void createOrder(String orderId) {
String orderKey = "order:" + orderId;
// 存储订单信息(实际场景可存储订单JSON字符串)
stringRedisTemplate.opsForValue().set(orderKey, "order_info:" + orderId);
// 设置 30 分钟后过期(触发过期事件)
stringRedisTemplate.expire(orderKey, 30, TimeUnit.MINUTES);
}
}
5、注意事项
- 事件通知可靠性:Redis 事件通知是「fire and forget」模式,客户端离线时会丢失通知,需确保监听客户端高可用(如集群部署)。
- 性能影响:开启过多事件通知可能增加 Redis 开销,建议仅开启必要的事件类型(如仅开启过期事件 Ex)。
- 数据库编号:默认监听 0 号数据库,若 Key 存储在其他数据库,需修改订阅频道的数据库编号(如
__keyevent@1__:expired)。
- 过期延迟:由于 Redis 过期删除机制,Key 过期后可能不会立即触发通知,存在毫秒级延迟,不适用于对实时性要求极高的场景。
- Redis Key 事件通知基于 Pub/Sub 实现,不具备消息可靠性保障,不适合作为强一致性或关键业务的唯一触发机制,常用于辅助通知或最终一致性场景。