Redis高级数据结构

一、位图

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.0511287885.05112878经度 -180180;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 替代 GEORADIUSGEORADIUSBYMEMBER,但二者当前仍保持兼容。) 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)」的消息通信模型,采用「发布者 - 订阅者」模式,发布者向指定频道发送消息,所有订阅该频道的订阅者都会收到消息(一对多通信)。
  • 核心特性:
    1. 无消息持久化:若订阅者离线,期间发布的消息会丢失,无法回溯;
    2. 实时性:消息发布后立即推送给在线订阅者,实时性高;
    3. 广播机制:同一频道的所有订阅者都会收到相同消息;
    4. 支持模式订阅:可订阅符合指定模式的多个频道(如 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 消息丢失的问题。
  • 核心特性:
    1. 消息持久化:所有消息均持久化到磁盘,即使 Redis 重启,消息也不会丢失;
    2. 唯一消息 ID:每条消息自动生成(或手动指定)唯一 ID(格式:时间戳-序列号,如 1736200000-0);
    3. 消费者组:支持多个消费者组并行消费,每个消费者组维护自己的消费偏移量,互不干扰;
    4. 消息确认:消费者获取消息后需手动 ACK,未 ACK 的消息可重新消费,保证消息可靠投递;
    5. 消息回溯:支持按消息 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)。
  • 核心特性:
    1. 仅支持「添加」和「查询是否存在」,不支持「删除」元素(普通布隆过滤器);
    2. 假阳性率可通过「位图大小」「哈希函数个数」「元素总量」提前计算优化;
    3. 基于 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 可主动推送通知给订阅的客户端。过期删除机制:采用「惰性删除 + 定期删除」结合:

  1. 惰性删除:访问过期 Key 时才检查并删除,不浪费 CPU 资源,但可能占用内存;
  2. 定期删除: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 事件通知(两种方式)

  1. 临时开启(Redis 重启后失效):执行指令 CONFIG SET notify-keyspace-events ExK

  2. 永久开启(修改 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 实现,不具备消息可靠性保障,不适合作为强一致性或关键业务的唯一触发机制,常用于辅助通知或最终一致性场景。
相关推荐
optimistic_chen10 小时前
【Redis系列】事务特性
数据库·redis·笔记·缓存·事务
多米Domi01110 小时前
0x3f 第25天 黑马web (145-167)hot100链表
数据结构·python·算法·leetcode·链表
叫我:松哥10 小时前
基于机器学习的地震风险评估与可视化系统,采用Flask后端与Bootstrap前端,系统集成DBSCAN空间聚类算法与随机森林算法
前端·算法·机器学习·flask·bootstrap·echarts·聚类
一起养小猫10 小时前
LeetCode100天Day12-删除重复项与删除重复项II
java·数据结构·算法·leetcode
CodeCipher10 小时前
关于Redis单线程问题
数据库·redis·缓存
威风少侠10 小时前
Redis集群配置优化指南
数据库·redis·缓存
一起努力啊~11 小时前
算法刷题--螺旋矩阵II+区间和+开发商购买土地
数据结构·算法·leetcode
indexsunny14 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Redis缓存场景解析
java·spring boot·redis·缓存·微服务·消息队列·电商
ID_1800790547314 小时前
小红书笔记详情API接口基础解析:数据结构与调用方式
数据结构·数据库·笔记