第 41 章

Redis 7 新特性:Function、Sharded Pub/Sub 与 listpack

第41章 Redis 7 新特性:Function、Sharded Pub/Sub 与 listpack

Redis 7.0 是继 Redis 6.0 之后最重大的版本升级,带来了三项架构级改进:以 Function 替代无状态 Lua 脚本、以 Sharded Pub/Sub 解决 Cluster 广播瓶颈、以 listpack 全面取代 ziplist。本章深入分析每项特性的设计动机、实现原理与生产使用注意事项。


41.1 Function:持久化的服务端脚本

41.1.1 Lua 脚本的历史局限

Redis 4.0 以前,服务端逻辑扩展的唯一手段是 EVAL 执行 Lua 脚本。EVAL 的设计是"临时性"的:

缺陷一:脚本不持久化 Redis 维护一张 lua_scripts 哈希表,key 是 SHA1,value 是脚本源码。SCRIPT LOAD 将脚本缓存到该表后,可以用 EVALSHA 调用。但这张表不写入 RDB / AOF,Redis 重启后缓存清空,客户端必须重新执行 SCRIPT LOAD。这在滚动重启、故障转移场景下极易引发 NOSCRIPT 错误。

缺陷二:无命名空间 所有脚本共用同一个 SHA1 命名空间。多团队、多应用共享一个 Redis 实例时,脚本管理混乱,无法按归属清理。

缺陷三:无版本管理 脚本内容变更后,SHA1 随之变化,客户端必须同步更新 SHA1,无法做到平滑的滚动更新。

缺陷四:只读副本限制 EVAL / EVALSHA 默认被认为是写操作,无法在只读副本(Replica)执行,即使脚本实际上只读。Redis 6.0 引入 EVAL_RO / EVALSHA_RO 作为补丁,但缺乏系统化声明机制。

41.1.2 Function 的设计

Redis 7.0 引入 Function,核心概念:

41.1.3 加载与调用

# 加载库(REPLACE 参数用于覆盖已有同名库)
FUNCTION LOAD "#!lua name=mylib\n
local function my_hello(keys, args)\n
  return 'Hello ' .. args[1]\n
end\n
local function my_set(keys, args)\n
  return redis.call('SET', keys[1], args[1])\n
end\n
redis.register_function('hello', my_hello)\n
redis.register_function('myset', my_set)"

# 调用函数(numkeys=0 表示不传 key 参数)
FCALL hello 0 World
# → "Hello World"

# 调用时传 key
FCALL myset 1 mykey myvalue
# → OK

# 只读版本:可在 Replica 节点执行
FCALL_RO hello 0 World

参数约定与 EVAL 相同:FCALL funcname numkeys [key [key ...]] [arg [arg ...]]

41.1.4 函数管理命令

# 列出所有库及其函数
FUNCTION LIST
# → 1) 1) "library_name"
#      2) "mylib"
#   2) 1) "engine"
#      2) "LUA"
#   3) 1) "functions"
#      2) 1) 1) "name" 2) "hello"  3) "description" 4) nil  5) "flags" 6) (empty array)
#         2) 1) "name" 2) "myset"  ...

# 带 WITHCODE 参数查看源码
FUNCTION LIST WITHCODE

# 过滤特定库
FUNCTION LIST LIBRARYNAME mylib

# 删除库(同时删除库内所有函数)
FUNCTION DELETE mylib

# 导出所有函数(二进制,类似 DUMP)
FUNCTION DUMP
# → "\x\xf6\x00\xc3..."

# 从导出数据恢复(FLUSH 参数先清空现有函数)
FUNCTION RESTORE <dump-data>
FUNCTION RESTORE <dump-data> FLUSH

# 清空所有函数
FUNCTION FLUSH
FUNCTION FLUSH ASYNC   # 异步清空
FUNCTION FLUSH SYNC    # 同步清空

41.1.5 持久化机制

Function 的持久化由 Redis 自动处理:

这彻底解决了 Lua 脚本的重启丢失问题。

41.1.6 函数标志(Flags)

注册函数时可声明标志,影响 Redis 对函数的行为:

-- no-writes:声明函数不写入,允许在只读副本执行,也允许在 maxmemory 策略下执行
redis.register_function{function_name='readonly_fn', callback=my_fn, flags={'no-writes'}}

-- allow-oom:即使超出 maxmemory 也允许执行(谨慎使用)
redis.register_function{function_name='critical_fn', callback=my_fn, flags={'allow-oom'}}

-- allow-stale:允许在 replica 数据滞后时执行(配合 replica-serve-stale-data no 场景)
redis.register_function{function_name='stale_ok', callback=my_fn, flags={'allow-stale'}}

41.1.7 Function vs EVAL 对比

维度 EVAL / EVALSHA FUNCTION / FCALL
持久化 不持久化,重启丢失 持久化到 RDB/AOF
命名空间 无(SHA1) 有(库名.函数名)
版本管理 无(SHA1 变化即失效) 支持 REPLACE 原子升级
只读副本 需 EVAL_RO(6.0+) 声明 no-writes flag
管理命令 SCRIPT LIST/FLUSH FUNCTION LIST/DELETE/DUMP
推荐场景 一次性临时逻辑 生产长期使用的服务端逻辑

41.2 Sharded Pub/Sub(Redis 7.0)

41.2.1 传统 Pub/Sub 在 Cluster 下的问题

Redis Cluster 中,传统 PUBLISH 命令会将消息广播到集群中所有节点(包括所有 Master 和 Replica),由每个节点转发给订阅了该频道的本地客户端。

广播开销分析

本质问题:Pub/Sub 消息与 HashSlot 无关,不能利用 Cluster 的槽路由,所有节点都需要处理所有消息。

41.2.2 Sharded Pub/Sub 设计

Redis 7.0 引入三个新命令:SSUBSCRIBESUNSUBSCRIBESPUBLISH,按照频道名的 HashSlot 路由消息。

路由规则

# 订阅分片频道(客户端会自动路由到正确节点)
SSUBSCRIBE {user:1000}:events
# → 1) "ssubscribe"
#   2) "{user:1000}:events"
#   3) (integer) 1

# 取消订阅
SUNSUBSCRIBE {user:1000}:events

# 发布消息到分片频道(只到负责该槽的节点)
SPUBLISH {user:1000}:events "login"
# → (integer) 1   # 收到消息的订阅者数量(只含该节点)

# 查看分片频道订阅信息
PUBSUB SHARDCHANNELS           # 列出所有分片频道
PUBSUB SHARDNUMSUB channel1    # 查看频道订阅数

41.2.3 使用限制与注意事项

  1. 不支持 Pattern 订阅SSUBSCRIBE 不支持通配符,只能精确订阅。
  2. 客户端路由:客户端库需要能感知 Cluster 拓扑,自动将 SSUBSCRIBE/SPUBLISH 路由到正确节点。支持情况:
    • Lettuce 6.2+:原生支持
    • Jedis 4.0+:需配合 JedisCluster 使用
    • redis-py 4.3+:支持
  3. 仅限 Cluster 模式:单机、哨兵模式无意义(所有槽在同一节点,等同于普通 Pub/Sub)。

41.2.4 传统 vs 分片 Pub/Sub 对比

维度 PUBLISH / SUBSCRIBE SPUBLISH / SSUBSCRIBE
集群消息传播 广播到所有节点 O(N) 仅目标节点 O(1)
支持 Pattern 是(PSUBSCRIBE)
使用场景 单机 / 小集群 / 需要 Pattern 大集群,按业务分片
Redis 版本 2.0+ 7.0+ Cluster
客户端复杂度 需要 Cluster-aware 客户端

41.3 listpack 全面替换 ziplist

41.3.1 ziplist 的设计缺陷:连锁更新

ziplist 是 Redis 用于紧凑存储小数据量容器的编码方式,每个 entry 包含:

| prevlen (1 or 5 bytes) | encoding (1-9 bytes) | data |

prevlen 存储前一个 entry 的总长度,用于反向遍历。当前一个 entry 长度 < 254 字节时,prevlen 占 1 字节;≥ 254 字节时,占 5 字节。

连锁更新(Cascade Update): 当向 ziplist 中间插入一个 entry,使某个 entry 的 prevlen 从 1 字节扩展为 5 字节,后一个 entry 的 prevlen 也需要同步更新(因为前一个 entry 变大了 4 字节),这可能触发连锁的 prevlen 更新,最坏情况 O(N²) 时间复杂度。

41.3.2 listpack 的改进

listpack(紧凑列表)于 Redis 5.0 引入(最初用于 Stream),7.0 全面推广。每个 entry 格式:

| encoding-type (1 byte) | data | backlen (1-5 bytes) |

关键改动:backlen 存储当前 entry 自身的长度(而非前一个 entry 的长度),反向遍历时通过当前 entry 的 backlen 跳到自身头部,再读前一个 entry。这样插入操作不需要修改任何后续 entry,完全消除连锁更新。

内存节省:

41.3.3 Redis 7.0 中的配置参数迁移

# Hash
hash-max-listpack-entries 128    # 原 hash-max-ziplist-entries
hash-max-listpack-value 64       # 原 hash-max-ziplist-value

# ZSet
zset-max-listpack-entries 128    # 原 zset-max-ziplist-entries
zset-max-listpack-value 64       # 原 zset-max-ziplist-value

# List(7.0 起 List 小数据量使用 listpack,大数据量用 quicklist)
list-max-listpack-size -2        # 原 list-max-ziplist-size(已在 7.0 重命名)

旧参数名仍向后兼容,但建议迁移到新名称。

验证编码:

HSET myhash f1 v1
OBJECT ENCODING myhash
# → "listpack"   (7.0 以前返回 "ziplist")

# 当超过阈值时自动升级
# 添加 129 个 field 后
OBJECT ENCODING myhash
# → "hashtable"

41.4 Multi-Part AOF(Redis 7.0)

41.4.1 传统 AOF rewrite 的问题

传统 AOF rewrite(BGREWRITEAOF)流程:

  1. Fork 子进程,将当前内存状态写为新的 AOF 文件(appendonly.aof.tmp
  2. 主进程将 rewrite 期间的新写命令累积在 AOF rewrite buffer(内存中)
  3. 子进程完成后,主进程将 rewrite buffer 追加到新 AOF 文件
  4. 原子 rename:appendonly.aof.tmpappendonly.aof

问题:步骤 3 中追加 rewrite buffer 需要加锁,若 rewrite 期间写入量极大(buffer 达几百 MB),rename 前的追加操作会阻塞主线程数百毫秒。

41.4.2 Multi-Part AOF 架构

Redis 7.0 将 AOF 拆分为多个文件,由一个 manifest 文件管理:

/data/
  appendonlydir/
    appendonly.aof.1.base.rdb    # BASE 文件(全量,可以是 RDB 格式)
    appendonly.aof.1.incr.aof    # 增量 AOF(第1轮)
    appendonly.aof.2.incr.aof    # 增量 AOF(第2轮,rewrite 后新建)
    appendonly.aof.manifest      # 清单文件

manifest 文件格式

file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
file appendonly.aof.2.incr.aof seq 2 type i

rewrite 流程改进

  1. 主进程新建 appendonly.aof.2.incr.aof,后续新写命令写入此文件
  2. Fork 子进程,将当前内存状态写为新的 BASE 文件
  3. 子进程完成后,更新 manifest(新 BASE 替换旧 BASE,旧 INCR 文件可删除)
  4. 无需追加 buffer 到大文件,rewrite 期间主进程已实时写到新 INCR 文件

rewrite 完成时,主线程只需原子更新 manifest 文件(极小操作),完全消除了大文件追加导致的阻塞。

41.4.3 重启恢复流程

1. 读取 appendonly.aof.manifest
2. 加载 BASE 文件(RDB 格式:直接加载;AOF 格式:逐行重放)
3. 按 seq 顺序依次重放所有 INCR 文件
4. 恢复完成

配置参数(无需修改,7.0 默认启用):

appendonly yes
appenddirname "appendonlydir"    # 7.0 新增,指定 AOF 目录名
aof-use-rdb-preamble yes         # BASE 文件使用 RDB 格式(压缩更好)

41.5 其他重要新特性

41.5.1 LMPOP / ZMPOP(原子批量弹出)

# LMPOP:从多个 list 中弹出,返回第一个非空 list 的元素
# 语法:LMPOP numkeys key [key ...] LEFT|RIGHT [COUNT count]
LMPOP 2 list1 list2 LEFT COUNT 5
# → 1) "list1"          # 来源 key
#   2) 1) "elem1"
#      2) "elem2"
#      ...

# ZMPOP:从多个 ZSet 中弹出
# 语法:ZMPOP numkeys key [key ...] MIN|MAX [COUNT count]
ZMPOP 1 zset1 MIN COUNT 3
# → 1) "zset1"
#   2) 1) 1) "member1" 2) "1.0"
#      2) 1) "member2" 2) "2.0"
#      3) 1) "member3" 3) "3.0"

使用场景:多队列优先级消费、跨多个 ZSet 弹出任务。

41.5.2 SINTERCARD(集合交集基数)

# 只返回交集元素数量,不返回元素本身(避免大集合传输开销)
SINTERCARD 2 set1 set2
# → (integer) 42

# LIMIT 参数:最多返回 N 个(提前终止计算,性能优化)
SINTERCARD 2 set1 set2 LIMIT 10
# → (integer) 10  (实际交集可能更大,但最多计算到 10)

41.5.3 XAUTOCLAIM(Stream 自动声明)

# 自动声明超过指定毫秒未 ACK 的 PEL 消息,转移到新消费者
# 语法:XAUTOCLAIM key group consumer min-idle-time start [COUNT count] [JUSTID]
XAUTOCLAIM mystream mygroup consumer2 60000 0-0 COUNT 10
# → 1) "0-0"           # 下次调用的起始 ID(游标)
#   2) 1) (消息列表)    # 被转移的消息
#   3) 1) (已删除的消息 ID,PEL 中有但 Stream 中已不存在)

XCLAIM 相比,XAUTOCLAIM 可以一次性扫描并声明多条超时消息,适合构建消费者故障恢复逻辑。

41.5.4 版本升级注意事项

从 Redis 6.x 升级到 7.0 的兼容性检查清单:

# 1. 检查废弃命令使用情况
redis-cli COMMAND DOCS OBJECT   # 查看命令文档,注意 deprecated 标记

# 2. 检查配置参数(新旧参数名均支持,但建议迁移)
grep -i ziplist /etc/redis/redis.conf   # 找出旧参数名

# 3. 验证 AOF 目录(7.0 默认使用目录而非单文件)
# appendonly.aof → appendonlydir/appendonly.aof.*
# 升级前确保 appenddirname 路径可写

# 4. Cluster 模式:PUBLISH 行为不变,只有新增 SPUBLISH 才是分片
# 无需修改现有 Pub/Sub 代码,可按需迁移

本章小结

Redis 7.0 的三项核心改进各自解决了长期痛点:

这些特性共同构成了 Redis 7 作为生产级分布式缓存/存储的坚实基础。

本章评分
4.9  / 5  (3 评分)

💬 留言讨论