Redis 高级特性与 Spring Boot 实战指南

6 阅读6分钟

一、 核心配置:序列化与连接池

在开始特殊用法前,必须确保 RedisTemplate 配置正确,避免出现乱码或性能瓶颈。

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // 使用 JSON 序列化保存 Value,方便观察数据
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(jsonSerializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jsonSerializer);
        return template;
    }
}

二、 事务 (Transactions)

Redis 事务通过 MULTI, EXEC, DISCARD 命令实现。它保证了一组命令的原子性(要么都发给服务端,要么都不发),但注意 Redis 事务不支持回滚

实战代码:

public void executeTransaction() {
    redisTemplate.execute(new SessionCallback<List<Object>>() {
        @Override
        public List<Object> execute(RedisOperations operations) throws DataAccessException {
            operations.watch("balance"); // 乐观锁:监视 key 是否被改动
            operations.multi();
            operations.opsForValue().increment("balance", -100);
            operations.opsForValue().increment("score", 100);
            return operations.exec(); // 提交事务
        }
    });
}

三、 流水线 (Pipelining)

为什么要用? 当你需要连续执行 1000 条命令时,往返的网络耗时(RTT)远大于 Redis 处理的时间。Pipeline 可以将命令一次性打包发送,性能提升巨大。

实战代码:

public void usePipelined() {
    List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        for (int i = 0; i < 1000; i++) {
            connection.set(("key_" + i).getBytes(), "value".getBytes());
        }
        return null; // 必须返回 null
    });
}

四、 发布/订阅 (Pub/Sub)

Redis 的发布订阅是一种轻量级的消息队列,适用于实时性要求高但不需要持久化的场景。

1. 订阅者(配置):

@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory, 
                                        MessageListenerAdapter adapter) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(factory);
    container.addMessageListener(adapter, new PatternTopic("chat_room"));
    return container;
}

2. 发布者代码:

public void publishMessage(String msg) {
    redisTemplate.convertAndSend("chat_room", msg);
}

五、 Lua 脚本 (Scripting)

Lua 脚本是 Redis 处理复杂逻辑的终极武器,它能保证 多条命令的绝对原子性(脚本执行期间其他命令会被阻塞)。

典型场景:分布式限流

public boolean tryLock(String key, String value, int expireTime) {
    String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
                    "return redis.call('expire', KEYS[1], ARGV[2]) " +
                    "else return 0 end";
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
    Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, expireTime);
    return result != null && result.equals(1L);
}

六、 布隆过滤器 (Bloom Filter)

布隆过滤器用于判断“某个值是否一定不存在”。它内存占用极小,常用于解决缓存穿透

注意:原生 Redis 不带布隆过滤器,需安装 RedisBloom 模块。Spring Data Redis 通过 execute 调用原生命令。

实战代码:

public void bloomOps() {
    // 1. 添加元素
    redisTemplate.execute((RedisCallback<Object>) conn -> 
        conn.execute("BF.ADD", "user_exists".getBytes(), "user_101".getBytes()));

    // 2. 检查是否存在
    Boolean exists = (Boolean) redisTemplate.execute((RedisCallback<Object>) conn -> 
        conn.execute("BF.EXISTS", "user_exists".getBytes(), "user_101".getBytes()));
}

七、 Stream:持久化消息队列 (Redis 5.0+)

为什么要用? 发布/订阅(Pub/Sub)不能持久化消息。Stream 借鉴了 Kafka,支持消息持久化、消费组(Consumer Group)和消息确认(ACK)

实战代码:发送与消费

public void streamOps() {
    // 1. 发送消息
    ObjectRecord<String, String> record = StreamRecords.newRecord()
            .in("order_stream")
            .ofObject("OrderData:1001");
    RecordId recordId = redisTemplate.opsForStream().add(record);

    // 2. 读取消息 (监听模式通常配合 StreamMessageListenerContainer 使用)
    List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream()
            .read(StreamOffset.create("order_stream", ReadOffset.lastConsumed()));
}

八、 Bitmaps:位图 (极简内存占用)

场景: 1 亿用户,每天谁签到了?谁在线?

原理: 用 1 个 bit(0 或 1)表示一个状态。1 亿个 bit 仅占 12.5 MB。

实战代码:签到系统

public void bitmapOps(Long userId) {
    // 设置第 userId 位为 1(签到)
    redisTemplate.opsForValue().setBit("sign:202601", userId, true);

    // 统计当月总签到人数
    Long count = (Long)redisTemplate.execute((RedisCallback<Long>) conn -> 
        conn.bitCount("sign:202601".getBytes()));
        
    // 查询用户今天是否签到
    Boolean isSigned = redisTemplate.opsForValue().getBit("sign:202601", userId);
}

九、 HyperLogLog:海量基数统计

场景: 统计网页 UV(独立访客数)。

痛点: 如果用 Set 存 1000 万个 IP,非常耗内存。HyperLogLog 仅用 12 KB 就能统计 2^64 个数据,误差约 0.81%。

实战代码:UV 统计

public void hllOps(String ip) {
    // 添加访问 IP
    redisTemplate.opsForHyperLogLog().add("page_uv:home", ip);

    // 获取预估的去重计数
    Long uvCount = redisTemplate.opsForHyperLogLog().size("page_uv:home");
}

十、 Geospatial:地理位置 (附近的人)

场景: 查找附近 5 公里的外卖商家、车辆定位。

实战代码:位置计算

public void geoOps() {
    // 1. 添加坐标 (经度, 纬度, 名称)
    redisTemplate.opsForGeo().add("stores", new Point(116.40, 39.90), "Store_A");
    redisTemplate.opsForGeo().add("stores", new Point(116.41, 39.91), "Store_B");

    // 2. 计算两地距离
    Distance dist = redisTemplate.opsForGeo().distance("stores", "Store_A", "Store_B", RedisGeoCommands.DistanceUnit.KILOMETERS);

    // 3. 搜索半径 5km 内的商家
    Circle circle = new Circle(new Point(116.40, 39.90), new Distance(5, RedisGeoCommands.DistanceUnit.KILOMETERS));
    GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisTemplate.opsForGeo().radius("stores", circle);
}

十一、 Redis 过期监听 (Key Expiration Listener)

场景: 订单 30 分钟未支付自动取消(虽然通常建议用延迟队列,但小规模场景可用此功能)。

1. 开启配置 (redis.conf)

确保配置了 notify-keyspace-events Ex

2. 编写监听器

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        if (expiredKey.startsWith("order:")) {
            // 执行取消订单逻辑
            System.out.println("订单已过期:" + expiredKey);
        }
    }
}

十二、 场景选择全汇总

需求场景推荐特性核心理由业务价值
分布式锁 / 抢红包 / 秒杀扣减Lua 脚本绝对原子性。多条命令在 Redis 中作为整体执行,中间不会被其他请求插队。防止超卖,解决“查询-修改-写入”的并发冲突。
大批量初始化数据导入Pipeline (流水线)减少 RTT 网络往返。将数千条命令打包发送,大幅度降低网络延迟带来的开销。性能提升 10 倍以上,缩短系统预热时间。
可靠的异步订单处理 / 任务队列Stream消息持久化 + 消费组 (ACK) 。支持偏移量读取和消息确认,消息执行失败可追溯。相比 Pub/Sub 更加安全,实现类似 Kafka 的可靠消费。
海量用户日活 (DAU) / 签到统计Bitmaps (位图)极致内存利用率。1 个 bit 代表 1 个用户,1 亿用户状态仅需 12.5MB 内存。极速完成上亿数据的位运算(并集/交集),统计活跃度。
百万级/千万级网页 UV 统计HyperLogLog概率算法统计基数。无论数据量多大,仅固定占用 12KB 内存,误差率极低。在不存储具体用户 ID 的情况下,极低成本实现去重计数。
附近的人 / 附近商家查询Geospatial (地理位置)内置坐标索引。支持经纬度存储,并提供半径查询、距离计算等专用指令。轻松实现 LBS(基于位置的服务),如 O2O 配送范围计算。
简单的悲观并发控制Transactions (事务)WATCH + MULTI/EXEC。提供简单的乐观锁机制,监控 Key 是否被外部修改。保证一组操作在执行前数据未被篡改。
即时聊天室 / 实时系统通知Pub/Sub (发布订阅)轻量级解耦。生产者只需发送,所有在线的订阅者均能实时收到,无需存储。实现低延迟的实时广播,系统逻辑高度解耦。
防止数据库缓存穿透Bloom Filter (布隆过滤器)快速排除非法请求。以极小的空间开销判断“值是否一定不存在”。拦截非法请求直接回源数据库,保护底层存储安全。
延迟任务执行 (如 30 分钟未支付)Key Expiration Listener基于过期事件通知。当 Key 到期消失时,Redis 主动推送通知给 Java 应用。实现简易的定时清理任务(注:高可靠场景仍建议用 MQ 延迟队列)。

⚠️ 注意事项与进阶建议

  1. BigKey 问题:Bitmaps 和 HLL 如果 key 过大,在分配内存或删除时会引起 Redis 阻塞。建议按时间维度(如天、月)分 key 存储。
  2. Stream 的堆积:Stream 消息会持久化,必须设置 MAXLEN 限制长度,防止耗尽内存。
  3. 过期监听风险:过期监听并不保证实时性。Redis 是在 key 被真正删除(惰性删除或定期扫描)时才发通知,高负载下会有延迟。