Skip to content

Redis 高级特性

  Redis 除了缓存和基本数据结构,还提供了事务、Lua 脚本、Pipeline、发布订阅、Stream 消息队列等高级特性。这些特性让 Redis 的能力远超缓存。

一、Redis 事务

1.1 事务命令

bash
MULTI         # 开启事务
SET key1 v1
SET key2 v2
INCR counter
EXEC          # 执行事务(所有命令顺序执行)
# 或
DISCARD       # 取消事务

1.2 事务特性

Redis 事务的特点:

  ✅ 原子性:事务中的命令要么全执行,要么全不执行(但不支持回滚)
  ✅ 隔离性:事务中的命令在执行前不会被执行
  ✅ 顺序性:严格按入队顺序执行

  ❌ 不支持回滚:EXEC 后即使某条命令失败,其他命令仍会执行
  ❌ 不支持并发控制:没有悲观锁或 MVCC

1.3 WATCH 乐观锁

bash
# 使用 WATCH 实现乐观锁
WATCH balance:user:1          # 监视 key
balance = GET balance:user:1  # 读取当前值

MULTI
SET balance:user:1 (balance - 100)  # 扣款
EXEC

# 如果 EXEC 之前,balance:user:1 被其他客户端修改了
# EXEC 返回 nil,事务不执行

1.4 事务 vs Lua

维度事务(MULTI/EXEC)Lua 脚本
原子性支持支持
条件判断不支持(中间不可做判断)支持
回滚不支持不支持
复用性每次都要发送脚本缓存,复用
网络开销多条命令多次往返一次发送,一次执行

二、Lua 脚本

2.1 为什么用 Lua?

Lua 脚本的优势:

  ✅ 原子性:整个脚本作为一个原子操作执行
  ✅ 减少网络开销:多条命令打包发送
  ✅ 复用性:脚本可以缓存到 Redis,重复使用
  ✅ 条件逻辑:可以在脚本中做判断和循环

2.2 基本用法

java
// 使用 Lua 脚本实现分布式锁释放
String script = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "    return redis.call('del', KEYS[1]) " +
    "else " +
    "    return 0 " +
    "end";

// 执行脚本
Long result = jedis.eval(script, 
    Collections.singletonList("lock:order:1001"),  // KEYS
    Collections.singletonList("uuid-12345"));       // ARGV

// 预加载脚本(缓存 SHA1,后续用 evalsha 调用)
String sha1 = jedis.scriptLoad(script);
jedis.evalsha(sha1, keys, args);

2.3 实战:限流脚本

lua
-- 滑动窗口限流
-- KEYS[1] = 限流 key
-- ARGV[1] = 窗口大小(毫秒)
-- ARGV[2] = 限流阈值

local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = redis.call('TIME')[1] * 1000 + math.floor(redis.call('TIME')[2] / 1000)

-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)

if count < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random())
    redis.call('PEXPIRE', key, window)
    return 1  -- 放行
else
    return 0  -- 限流
end

2.4 注意事项

⚠️ Lua 脚本注意事项:

  1. 脚本执行是原子的,会阻塞其他命令
  2. 脚本不能执行太长时间(默认超时 5 秒)
  3. 不要写随机逻辑(影响主从复制一致性)
  4. 脚本中所有 key 必须通过 KEYS 传入(集群模式需要 key 路由)

三、Pipeline(管道)

3.1 为什么需要 Pipeline?

没有 Pipeline:
  客户端 → SET key1 v1 → Redis(RTT 1ms)
  客户端 → SET key2 v2 → Redis(RTT 1ms)
  客户端 → SET key3 v3 → Redis(RTT 1ms)
  ...
  100 条命令 = 100ms RTT(网络延迟)

使用 Pipeline:
  客户端 → [SET key1 v1, SET key2 v2, ..., SET key100 v100] → Redis
  100 条命令 = 1ms RTT(一次网络往返)
  性能提升 100 倍!

3.2 使用方式

java
// Jedis Pipeline
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
    pipeline.set("key:" + i, "value:" + i);
}
pipeline.sync();  // 批量发送,一次性获取结果

// Spring Data Redis
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    for (int i = 0; i < 10000; i++) {
        connection.set(("key:" + i).getBytes(), ("value:" + i).getBytes());
    }
    return null;
});

3.3 Pipeline vs 事务 vs Lua

维度Pipeline事务(MULTI)Lua 脚本
原子性❌ 不保证✅ 保证✅ 保证
网络优化✅ 批量发送✅ 批量发送✅ 一次发送
命令顺序保证顺序保证顺序脚本内顺序执行
使用场景批量操作,不要求原子性需要原子性的批量操作需要条件判断的原子操作

四、发布订阅

4.1 基本命令

bash
# 订阅
SUBSCRIBE channel1 channel2     # 订阅多个频道
PSUBSCRIBE order.*              # 模式订阅

# 发布
PUBLISH channel1 "hello world"

# 取消订阅
UNSUBSCRIBE channel1

4.2 使用场景

java
// 发布者
jedis.publish("order:create", "order:1001");

// 订阅者
jedis.subscribe(new JedisPubSub() {
    @Override
    public void onMessage(String channel, String message) {
        System.out.println("收到消息:" + channel + " - " + message);
    }
}, "order:create");

4.3 局限性

Pub/Sub 的局限:

  ❌ 消息不持久化:订阅者离线期间的消息会丢失
  ❌ 没有 ACK 机制:无法确认消息是否成功处理
  ❌ 不支持消息堆积:生产者快、消费者慢时,消息会丢失

  如果需要可靠的消息队列,用 Stream

五、Stream(消息队列)

5.1 为什么需要 Stream?

Redis 5.0 引入 Stream,专为解决 Pub/Sub 的可靠性问题。支持消息持久化、消费者组、ACK 确认、消息回溯。

Redis Stream 特性:

  ✅ 消息持久化(存储在内存,可 AOF/RDB 持久化)
  ✅ 消费者组(多个消费者并行消费,不重复)
  ✅ ACK 机制(确认消息处理完成)
  ✅ 消息回溯(可重放历史消息)
  ✅ 阻塞读取(XREAD BLOCK)

5.2 基本操作

bash
# 添加消息
XADD mystream * key1 value1 key2 value2
# * 表示自动生成消息 ID
# 返回:1612345678000-0

# 读取消息
XREAD COUNT 2 STREAMS mystream 0      # 从头读 2 条
XREAD BLOCK 5000 STREAMS mystream $   # 阻塞等待新消息

# 创建消费者组
XGROUP CREATE mystream group1 0       # 从 0 开始消费

# 消费者组消费
XREADGROUP GROUP group1 consumer1 COUNT 2 STREAMS mystream >
# > 表示只读取新消息

# ACK 确认
XACK mystream group1 1612345678000-0

# 查看未确认消息
XPENDING mystream group1

5.3 Spring Boot 集成

java
// 生产者
@Service
public class OrderProducer {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void sendOrder(Order order) {
        Map<String, String> message = new HashMap<>();
        message.put("orderId", order.getId().toString());
        message.put("amount", order.getAmount().toString());
        
        redisTemplate.opsForStream()
            .add("order:stream", message);
    }
}

// 消费者
@Component
public class OrderConsumer {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostConstruct
    public void start() {
        new Thread(() -> {
            while (true) {
                List<MapRecord<String, Object, Object>> messages = 
                    redisTemplate.opsForStream()
                        .read(Consumer.from("order-group", "consumer-1"),
                              StreamReadOptions.empty().count(5).block(Duration.ofSeconds(5)),
                              StreamOffset.create("order:stream", ReadOffset.lastConsumed()));
                
                for (MapRecord<String, Object, Object> msg : messages) {
                    System.out.println("处理订单:" + msg.getValue());
                    redisTemplate.opsForStream()
                        .acknowledge("order:stream", "order-group", msg.getId());
                }
            }
        }).start();
    }
}

5.4 Stream vs Kafka vs RabbitMQ

维度Redis StreamKafkaRabbitMQ
吞吐量高(10W+/s)极高(百万/s)中(3W/s)
持久化依赖 RDB/AOF磁盘日志磁盘 + 内存
消息回溯支持支持不支持
使用场景轻量级消息队列大数据流处理企业级消息

六、面试要点

Q1:Redis 事务支持回滚吗?

  不支持。Redis 事务没有回滚机制,EXEC 后即使某条命令失败,其他命令仍会执行。Redis 作者认为这是设计简洁性和性能的权衡。

Q2:Pipeline 和事务的区别?

  Pipeline 只是批量发送命令减少 RTT,不保证原子性;事务(MULTI/EXEC)保证原子性,中间的命令不会被打断。Lua 脚本也保证原子性,且支持条件判断。

Q3:Redis 的 Stream 和 Pub/Sub 有什么区别?

  Pub/Sub 消息不持久化,离线丢失,无 ACK;Stream 支持消息持久化、消费者组、ACK 确认、消息回溯,是可靠的轻量级消息队列。

Q4:Lua 脚本为什么能保证原子性?

  Redis 是单线程执行命令,Lua 脚本在执行期间不会被其他命令打断。整个脚本像一个原子操作,要么全部执行完,要么不执行。