Redis 内存数据结构存储
20 道题- 分类
- 中间件
- 题目数
- 20 道
1 Redis Cluster 的分片机制与 16384 个 Slot 分配原理是什么?
答案:
Redis Cluster 采用 Hash Slot 机制实现数据分片,将键空间划分为 16384 个 Slot。
Slot 分配原理:
- 每个键通过
CRC16(key) % 16384计算所属 Slot - Slot 分布在多个主节点之间,每个主节点负责一段 Slot 范围
- 每个 Slot 可包含多个键,主节点负责其 Slot 范围内的所有读写操作
# 查看集群 Slot 分布
redis-cli cluster slots
# 手动分配 Slot
redis-cli --cluster add-node new-node:6379 existing-node:6379
redis-cli --cluster reshard <target-node>:6379
为什么是 16384?
| 维度 | 16384(16K) | 65536(64K) |
|---|---|---|
| 心跳消息大小 | 约 2KB(bitmap) | 约 8KB |
| 网络带宽占用 | 低(Gossip 协议 PING/PONG) | 高 |
| 节点数上限 | 建议 ≤ 1000 | 理论上更多 |
| Redis 设计权衡 | 足够用且消息体紧凑 | 带宽浪费 |
16384 是在心跳消息大小与节点扩展性之间的折中:16K 个 Slot 的 bitmap 仅需 2KB,在 Gossip 消息中传输效率高,且足以支撑 1000 个节点的集群规模。
2 Redis Cluster 的 Gossip 协议如何工作?
答案:
Redis Cluster 各节点通过 Gossip 协议 在 Cluster Bus(端口 16379,即 data_port + 10000)上交换集群元信息。
Gossip 通信机制:
- 每个节点每秒随机选取
cluster-node-timeout / 2个节点发送PING消息 - 收到
PING的节点回复PONG PING/PONG消息携带:- 发送者自身信息(node ID、IP、port、flags)
- 发送者已知的其他节点信息(Gossip 段)
- 当前纪元(currentEpoch)和配置纪元(configEpoch)
- 节点通过 Gossip 消息发现新节点,标记疑似故障节点(PFAIL -> FAIL)
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
cluster-node-timeout | 15000ms | 节点超时阈值,影响 PFAIL 判定 |
cluster-slave-validity-factor | 10 | 从节点故障转移有效性因子 |
cluster-migration-barrier | 1 | 从节点迁移屏障 |
K8s 场景注意事项:
- Cluster Bus 端口需在 Service 和 NetworkPolicy 中额外暴露
- Headless Service 需确保 Pod DNS 解析后 Cluster Bus 通信可达
cluster-announce-ip和cluster-announce-port需正确配置
3 Redis Cluster 节点扩缩容与 Slot 迁移如何执行?
答案:
扩容流程(添加新主节点):
# 1. 新节点加入集群
redis-cli --cluster add-node <new-node-ip>:6379 <existing-node-ip>:6379
# 2. 重新分配 Slot(在线迁移)
redis-cli --cluster reshard <target-node-ip>:6379 \
--cluster-from <source-node-id> \
--cluster-to <target-node-id> \
--cluster-slots <slot-count>
Slot 迁移内部流程:
- 目标节点执行
CLUSTER SETSLOT <slot> IMPORTING <source-node-id> - 源节点执行
CLUSTER SETSLOT <slot> MIGRATING <target-node-id> - 源节点执行
MIGRATE命令逐键迁移数据 - 迁移完成后通知集群所有节点更新 Slot 归属
- 源节点删除已迁移的键
# 添加从节点
redis-cli --cluster add-node <new-node-ip>:6379 <existing-node-ip>:6379 \
--cluster-slave --cluster-master-id <master-node-id>
# 删除节点
redis-cli --cluster del-node <node-ip>:6379 <node-id>
缩容流程:
- 将待下线节点的 Slot 迁移至其他节点(
--cluster reshard) - 确认 Slot 全部迁出后执行
--cluster del-node
K8s 上扩容:先 kubectl scale statefulset redis-cluster --replicas=N,再执行 redis-cli --cluster reshard 手动均衡 Slot。自动化方案依赖 Operator。
4 Redis 监控指标体系如何构建?
答案:
监控体系以 redis_exporter 为核心,通过 Prometheus 采集指标,Grafana 展示。
架构:
Redis Pod(sidecar: redis_exporter)
└── Prometheus(ServiceMonitor / PodMonitor)
└── Grafana Dashboard
└── AlertManager(告警规则)
核心指标分类:
| 类别 | 关键指标 | 阈值建议 |
|---|---|---|
| 连接 | redis_connected_clients、redis_rejected_connections | 连接数 > 80% maxclients |
| 内存 | redis_memory_used_bytes、redis_memory_max_bytes | used > 80% maxmemory |
| 命中率 | redis_keyspace_hits_total / (hits + misses) | 命中率 < 90% |
| 命令延迟 | redis_commands_duration_seconds | P99 > 10ms |
| 持久化 | redis_rdb_last_save_time_seconds | 最后保存 > 配置间隔 |
| 复制 | redis_master_repl_offset - redis_slave_repl_offset | 复制延迟 > 10MB |
| 集群 | redis_cluster_slots_ok / redis_cluster_slots_total | 不可用 Slot > 0 |
| 键过期 | redis_expired_keys_total、redis_evicted_keys_total | 驱逐率持续增长 |
Prometheus 告警规则示例:
groups:
- name: redis
rules:
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "Redis 内存使用率超过 85%"
- alert: RedisDown
expr: redis_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Redis 实例 {{ $labels.instance }} 不可达"
- alert: RedisReplicationLag
expr: (redis_master_repl_offset - redis_slave_repl_offset) > 10485760
for: 5m
labels:
severity: warning
annotations:
summary: "Redis 主从复制延迟超过 10MB"
5 Redis 慢查询日志与性能诊断如何进行?
答案:
慢查询日志记录执行时间超过阈值的命令。
# 配置慢查询
CONFIG SET slowlog-log-slower-than 10000 # 超过 10ms 记录
CONFIG SET slowlog-max-len 128 # 最多保留 128 条
# 查看慢查询
SLOWLOG GET 10 # 最近 10 条
SLOWLOG LEN # 总数
SLOWLOG RESET # 清空
性能诊断方法:
| 方法 | 用途 | 示例 |
|---|---|---|
SLOWLOG GET | 定位慢命令 | 识别 KEYS *、O(N) 命令 |
INFO commandstats | 命令调用统计 | 分析命令分布与耗时 |
redis-cli --latency | 网络延迟采样 | 判断网络瓶颈 |
redis-cli --latency-history | 延迟时间序列 | 检测延迟波动 |
redis-cli --bigkeys | 大 Key 扫描 | 发现内存热点 |
redis-cli --memkeys | 内存消耗排序 | 定位内存占用 |
MEMORY STATS | 内存使用详情 | 分析碎片率 |
redis-benchmark | 性能压测 | 基线测试 |
K8s 场景性能诊断清单:
- 检查 CPU Throttling(
container_cpu_cfs_throttled_seconds_total) - 检查内存是否触及 Limit 导致 OOM Kill
- 检查磁盘 IOPS 是否满足 AOF
appendfsync需求 - 检查网络延迟(
redis-cli --latency从应用 Pod 测试) - 检查 NUMA 亲和性与 CPU 绑定(
taskset)
6 Redis 内存管理策略有哪些?
答案:
maxmemory 与逐出策略:
maxmemory 4gb
maxmemory-policy allkeys-lru
| 逐出策略 | 行为 | 适用场景 |
|---|---|---|
noeviction | 不逐出,写操作返回错误 | 数据不允许丢失 |
volatile-lru | 从设置了 TTL 的键中 LRU 逐出 | 缓存 + 持久键混合 |
allkeys-lru | 所有键中 LRU 逐出 | 纯缓存场景 |
volatile-lfu | 设置了 TTL 的键中 LFU 逐出 | 热点数据保护 |
allkeys-lfu | 所有键中 LFU 逐出 | 访问频率敏感 |
volatile-random | 随机逐出设置了 TTL 的键 | 缓存淘汰均匀 |
allkeys-random | 随机逐出所有键 | 所有键同等重要 |
volatile-ttl | 优先逐出 TTL 短的键 | 按过期时间淘汰 |
内存碎片整理:
# 查看碎片率
INFO memory # mem_fragmentation_ratio
# 自动碎片整理(Redis 4.0+)
CONFIG SET activedefrag yes
CONFIG SET active-defrag-ignore-bytes 100mb
CONFIG SET active-defrag-threshold-lower 10 # 碎片率 > 1.1
mem_fragmentation_ratio = used_memory_rss / used_memory,大于 1.5 表示碎片严重。
K8s 内存管理注意:
resources.limits.memory需大于maxmemory,预留 fork RDB 和系统开销- 建议比例:
limits.memory >= maxmemory * 1.3 + 500MB - 开启
oom-score-adj避免 OOM Killer 误杀
7 Redis Pipeline 与 Batch 操作有什么不同?
答案:
| 维度 | Pipeline | Batch(MGET/MSET) | 事务(MULTI/EXEC) |
|---|---|---|---|
| 实现方式 | 客户端缓存多条命令一次性发送 | 单条命令操作多个 Key | 将多条命令打包原子执行 |
| 原子性 | 不保证 | 单个命令本身原子 | 保证(EXEC 前不被打断) |
| 网络往返 | 1 次 RTT(N 条命令) | 1 次 RTT(1 条命令) | 1 次 RTT(N 条命令) |
| 执行顺序 | 按发送顺序执行 | N/A | 按添加顺序执行 |
| 错误处理 | 某命令失败不影响其他 | 整体成功或失败 | 某命令语法错误时全部不执行 |
Pipeline 使用示例(Go):
pipe := rdb.Pipeline()
incr := pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)
cmds, err := pipe.Exec(ctx)
注意事项:
- Pipeline 内命令数量不宜过大,建议单次 ≤ 100 条
- Pipeline 不保证原子性,中间可能插入其他客户端的命令
- Redis Cluster 下 Pipeline 要求所有 Key 在同一个 Slot(可用
{}hash tag 控制)
8 Redis Pub/Sub 与 Stream 有什么区别?
答案:
| 维度 | Pub/Sub | Stream |
|---|---|---|
| 消息持久化 | 不持久,消费者不在线即丢失 | 持久化至内存/RDB/AOF |
| 消费者组 | 不支持 | 支持(XGROUP),可负载均衡 |
| 消息回溯 | 不支持 | 支持(按 ID 范围读取) |
| 消息确认 | 无 | 支持(XACK) |
| 适用场景 | 实时通知、聊天 | 事件溯源、消息队列、日志收集 |
| 内存管理 | 不堆积 | 需设置 MAXLEN 限制长度 |
Stream 基本操作:
# 写入
XADD mystream * field1 value1 field2 value2
# 读取(阻塞)
XREAD BLOCK 0 STREAMS mystream 0
# 消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
XREADGROUP GROUP mygroup consumer1 BLOCK 0 STREAMS mystream >
# 确认
XACK mystream mygroup <message-id>
# 限制长度
XADD mystream MAXLEN ~ 10000 * field value
K8s Stream 注意事项:
- Stream 数据存储在内存中,需注意
maxmemory限制 - 消费者组信息存储在 Redis 内存中,故障转移后需重建或持久化消费偏移
9 Redis 分布式锁方案有哪些?
答案:
| 方案 | 原理 | 安全性 | 适用场景 |
|---|---|---|---|
| SET NX + EX | SET key value NX EX <ttl> | 单实例安全 | 单机 Redis,低可靠性要求 |
| Redisson(RedLock) | 多数节点加锁成功视为获得锁 | 高(防脑裂) | 多节点 Redis,金融级场景 |
| SET + Lua 释放 | Lua 脚本原子校验 value 后删除 | 防误删 | 单实例需防锁被他人释放 |
SET NX 正确实现:
# 加锁
SET lock:order:123 unique-client-id NX EX 30
# 释放(Lua 脚本保证原子性)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
" 1 lock:order:123 unique-client-id
Redisson RedLock 原理:
- 客户端向 N 个独立 Redis 实例依次请求加锁
- 设置锁超时远小于 TTL(如 TTL=30s,超时=5ms)
- 当 ≥ N/2+1 个实例加锁成功,且总耗时 < TTL,锁获取成功
- 实际有效时间 = TTL - 获取耗时
- 释放时向所有实例发送释放命令
K8s 场景注意:RedLock 要求 Redis 实例彼此独立(不同节点、不同可用区),K8s 上需配合 podAntiAffinity 部署。
10 缓存穿透、击穿、雪崩如何防护?
答案:
| 问题 | 定义 | 防护方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,请求直达数据库 | 布隆过滤器、空值缓存(短 TTL)、参数校验 |
| 缓存击穿 | 热点 Key 过期瞬间高并发直达数据库 | 互斥锁(SET NX)、逻辑过期 + 异步刷新、永不过期 |
| 缓存雪崩 | 大量 Key 同时过期或 Redis 宕机 | TTL 随机化、多级缓存、限流降级、Redis 高可用 |
布隆过滤器(RedisBloom 模块):
# 添加元素
BF.ADD cache-filter "key:12345"
BF.EXISTS cache-filter "key:12345"
# 大规模导入
BF.RESERVE large-filter 0.01 1000000 # 错误率 1%,100 万容量
BF.MADD large-filter key1 key2 key3
热点 Key 互斥锁方案(伪代码):
func GetData(key string) (string, error) {
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
// 缓存未命中,使用互斥锁
lockKey := "lock:" + key
locked, _ := rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
if locked {
defer rdb.Del(ctx, lockKey)
val = queryDB(key)
rdb.Set(ctx, key, val, 30*time.Minute)
} else {
time.Sleep(100 * time.Millisecond)
return GetData(key) // 重试
}
}
return val, err
}
TTL 随机化:EXPIRE key <base_ttl + random(0, base_ttl * 0.3)>,避免集中过期。
11 大 Key 与热 Key 如何检测和处理?
答案:
检测手段:
| 工具 | 用途 | 命令 |
|---|---|---|
redis-cli --bigkeys | 扫描最大 Key | redis-cli -h <host> --bigkeys |
redis-cli --hotkeys | 扫描热 Key(需 maxmemory-policy 为 LFU) | redis-cli -h <host> --hotkeys |
MEMORY USAGE <key> | 精确计算 Key 内存占用 | MEMORY USAGE user:10001 |
OBJECT FREQ <key> | LFU 访问频率 | OBJECT FREQ user:10001 |
redis-rdb-tools | 离线分析 RDB 文件 | rdb -c memory dump.rdb |
SCAN + DEBUG OBJECT | 渐进式扫描 | SCAN 0 MATCH * COUNT 1000 |
大 Key 定义:
| 数据类型 | 阈值 |
|---|---|
| String | > 10MB |
| List / Set / ZSet / Hash | 元素数量 > 10000 |
| Stream | 消息数 > 100000 或总大小 > 50MB |
处理策略:
- 拆分:Hash 大 Key 按业务维度拆分为多个小 Hash
- 压缩:String 大 Key 先压缩再存储(snappy/gzip)
- 删除:分批渐进式删除,避免阻塞
# Hash 分批删除字段
HSCAN key 0 COUNT 100 → HDEL key field1 field2 ...
- 迁移:使用
MIGRATE将单个大 Key 迁移至独立实例 - 架构调整:热 Key 前置本地缓存(如 Caffeine、BigCache)
12 Redis 客户端连接池如何配置?
答案:
连接池关键参数(以 go-redis 为例):
| 参数 | 说明 | 建议值 |
|---|---|---|
PoolSize | 最大连接数 | CPU 核数 × 4 ~ 10 |
MinIdleConns | 最小空闲连接 | PoolSize × 0.2 |
MaxIdleConns | 最大空闲连接 | PoolSize |
PoolTimeout | 等待连接超时 | 5s ~ 10s |
IdleTimeout | 空闲连接回收时间 | 5min |
IdleCheckFrequency | 空闲检查频率 | 1min |
ConnMaxLifetime | 连接最大生命周期 | 30min ~ 1h |
go-redis 配置示例:
rdb := redis.NewClient(&redis.Options{
Addr: "redis-headless:6379",
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
PoolSize: 50,
MinIdleConns: 10,
MaxIdleConns: 50,
PoolTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Minute,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
})
Redis 服务端连接参数:
maxclients 10000
timeout 300 # 空闲连接超时秒数
tcp-keepalive 60 # TCP keepalive 间隔
tcp-backlog 511 # TCP 完成队列大小
K8s 场景注意:
- 连接池大小需配合 Pod 副本数计算总连接数,不超过
maxclients - Service Mesh(如 Istio)的 sidecar 可能增加连接,需留余量
IdleTimeout应小于 Service/负载均衡的空闲超时,避免半开连接
13 Redis SSL/TLS 加密与 ACL 权限如何控制?
答案:
TLS 配置:
# redis.conf
tls-port 6380
port 0 # 禁用非 TLS 端口
tls-cert-file /etc/tls/tls.crt
tls-key-file /etc/tls/tls.key
tls-ca-cert-file /etc/tls/ca.crt
tls-auth-clients yes # 要求客户端证书
tls-protocols "TLSv1.2 TLSv1.3"
tls-ciphers DEFAULT:!MEDIUM
K8s cert-manager 集成:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: redis-tls
spec:
secretName: redis-tls-secret
duration: 2160h
renewBefore: 360h
dnsNames:
- "redis-0.redis-headless.redis.svc.cluster.local"
- "*.redis-headless.redis.svc.cluster.local"
issuerRef:
name: ca-issuer
kind: ClusterIssuer
ACL 权限控制(Redis 6.0+):
# 创建用户
ACL SETUSER reader on >password ~* +@read -@write -@dangerous
ACL SETUSER writer on >password ~* +@all -@dangerous
ACL SETUSER appuser on >password ~prefix:* +@all
# 查看用户
ACL LIST
ACL GETUSER reader
# 配置文件持久化
aclfile /etc/redis/users.acl
ACL 权限规则:
| 规则 | 含义 |
|---|---|
on / off | 启用/禁用用户 |
>password | 设置密码 |
~pattern | 可访问的键模式(~* 表示所有键) |
+@category | 授权命令类别(+@read 只读) |
-command | 禁用特定命令(-FLUSHALL) |
+command | 授权特定命令 |
14 Redis Read-Only Replica 读写分离如何实现?
答案:
配置从节点可读:
# 从节点配置
replica-read-only yes # 从节点只读(默认)
# 或
replica-read-only no # 允许写入(数据会被主节点覆盖)
客户端读写分离策略:
| 策略 | 原理 | 一致性 |
|---|---|---|
| 应用层路由 | 客户端维护主从连接,写操作用主连接,读操作用从连接 | 最终一致(存在复制延迟) |
| Proxy 层 | Proxy 解析命令类型自动路由(如 Twemproxy、Codis) | 最终一致 |
| 一致性读 | 写后紧接的读强制走主节点 | 强一致 |
go-redis 读写分离示例:
// 主节点(写入)
master := redis.NewClient(&redis.Options{
Addr: "redis-master:6379",
})
// 从节点(读取)
slaves := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"slave0": "redis-slave-0.redis-headless:6379",
"slave1": "redis-slave-1.redis-headless:6379",
},
})
// 业务代码
master.Set(ctx, "key", "value", 0)
slaves.Get(ctx, "key")
一致性边界:Redis 主从复制是异步的,读从可能读到旧数据。需要强一致性的读操作必须走主节点。可通过 WAIT 命令将异步复制变为半同步:
SET key value
WAIT 1 1000 # 等待至少 1 个从节点确认,超时 1000ms
15 Redis 在线迁移方案有哪些?
答案:
| 方案 | 原理 | 停机时间 | 适用场景 |
|---|---|---|---|
| Redis-Shake | 解析 RDB + AOF,全量 + 增量同步 | 秒级(切换瞬间) | 跨集群迁移、云上云下迁移 |
| 主从复制切换 | 新实例 REPLICAOF 旧主,同步后切换 | 秒级 | 同版本升级、迁移 |
| RDB 导入 | BGSAVE → 拷贝 RDB → 新实例加载 | 分钟级 | 停机维护窗口 |
| MIGRATE | 单 Key 原子迁移 | 无(逐 Key) | 集群 Slot 迁移 |
| SCAN + DUMP/RESTORE | 分批序列化传输 | 无(逐批) | 选择性数据迁移 |
Redis-Shake 迁移流程:
# Redis-Shake K8s Job
apiVersion: batch/v1
kind: Job
metadata:
name: redis-shake-migration
spec:
template:
spec:
containers:
- name: redis-shake
image: apsaradb/redis-shake:latest
command:
- redis-shake.linux
args:
- -type=sync # sync / restore / scan
- -conf=/etc/shake.toml
volumeMounts:
- name: config
mountPath: /etc
volumes:
- name: config
configMap:
name: redis-shake-config
restartPolicy: Never
shak.toml 配置:
[sync_reader]
address = "source-redis:6379"
password = "source-pwd"
[redis_writer]
address = "target-redis:6379"
password = "target-pwd"
迁移步骤:
- 部署 Redis-Shake 同步任务
- 等待全量 + 增量追平(延迟 < 1ms)
- 停止源端写入
- 确认数据一致后切换客户端连接至目标
- 停止 Redis-Shake,清理源实例
16 Redis 拓扑感知调度如何通过 Pod Anti-Affinity 实现?
答案:
Pod Anti-Affinity 确保 Redis 实例分散在不同节点 / 可用区,避免单点故障。
三层调度策略:
| 策略级别 | topologyKey | 效果 |
|---|---|---|
| 节点级反亲和 | kubernetes.io/hostname | 同节点不部署多个 Redis Pod |
| 可用区级反亲和 | topology.kubernetes.io/zone | 同可用区最多一个副本 |
| 软反亲和 | preferredDuringScheduling | 优先分散,资源不足时可堆叠 |
完整配置:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["redis"]
topologyKey: kubernetes.io/hostname
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/redis
operator: Exists
TopologySpreadConstraints(替代 Anti-Affinity):
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: redis
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: redis
Node Selector / Taint-Toleration:Redis Pod 专属节点池,通过 taint 隔离其他工作负载:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "redis"
effect: "NoSchedule"
nodeSelector:
node-group: redis
17 Redis Lua 脚本与事务有什么区别?
答案:
| 维度 | Lua 脚本 | MULTI/EXEC 事务 | WATCH 乐观锁 |
|---|---|---|---|
| 原子性 | 完全原子,执行期间不可中断 | 命令打包,但不回滚 | 不保证原子性 |
| 条件逻辑 | 支持(if/else/循环) | 不支持(命令固定) | 支持 CAS |
| 网络往返 | 1 次 | 1 次 | 多次 |
| 错误处理 | 脚本执行失败不回滚已执行部分 | 语法错误全部不执行,运行时错误不回滚 | 竞争失败需重试 |
Lua 脚本示例:
-- 分布式限流:令牌桶
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local tokens = tonumber(redis.call('HGET', key, 'tokens')) or capacity
local last_time = tonumber(redis.call('HGET', key, 'last_time')) or now
local delta = math.max(0, now - last_time)
local new_tokens = math.min(capacity, tokens + delta * rate)
if new_tokens >= 1 then
redis.call('HSET', key, 'tokens', new_tokens - 1)
redis.call('HSET', key, 'last_time', now)
return 1
else
redis.call('HSET', key, 'tokens', new_tokens)
redis.call('HSET', key, 'last_time', now)
return 0
end
EVAL "<script>" 1 "rate_limit:user:123" 10 0.5 <current_timestamp>
WATCH 乐观锁:
WATCH balance:user:100
val = GET balance:user:100
MULTI
SET balance:user:100 <val - amount>
EXEC # 如果 key 被修改,EXEC 返回 nil
18 RedisJSON、RedisSearch、RedisTimeSeries 模块有何用途?
答案:
| 模块 | 用途 | 核心命令 | K8s 部署方式 |
|---|---|---|---|
| RedisJSON | JSON 文档存储与查询 | JSON.SET、JSON.GET、JSON.ARRAPPEND | redislabs/rejson 镜像 |
| RedisSearch | 全文搜索、向量搜索 | FT.CREATE、FT.SEARCH、FT.AGGREGATE | redislabs/redisearch 镜像 |
| RedisTimeSeries | 时序数据存储与聚合 | TS.CREATE、TS.ADD、TS.RANGE | redislabs/redistimeseries 镜像 |
| RedisBloom | 布隆过滤器、计数、布谷鸟 | BF.ADD、CF.ADD、CMS.INCRBY | redislabs/rebloom 镜像 |
| RedisGraph | 图数据库 | GRAPH.QUERY | redislabs/redisgraph 镜像 |
模块加载方式:
# redis.conf
loadmodule /usr/lib/redis/modules/redisearch.so
loadmodule /usr/lib/redis/modules/rejson.so
loadmodule /usr/lib/redis/modules/redistimeseries.so
Redis Stack 镜像(一站式):
containers:
- name: redis
image: redis/redis-stack-server:7.2.0-v10
# 已内置 RedisJSON、RedisSearch、RedisTimeSeries、RedisBloom、RedisGraph
RedisSearch 全文搜索:
FT.CREATE idx:articles ON JSON PREFIX 1 article: SCHEMA \
$.title AS title TEXT SORTABLE \
$.content AS content TEXT \
$.tags.* AS tags TAG
FT.SEARCH idx:articles "@title:kubernetes @tags:{redis}" RETURN 2 title tags
RedisTimeSeries 时序聚合:
TS.CREATE ts:sensor:temp RETENTION 86400000 LABELS sensor_id 1
TS.ADD ts:sensor:temp * 23.5
TS.RANGE ts:sensor:temp <start> <end> AGGREGATION avg 60000 # 1 分钟均值
19 PodDisruptionBudget 如何保障 Redis 在节点维护时的高可用?
答案:
PodDisruptionBudget(PDB) 限制同时不可用的 Pod 数量,防止节点维护或集群自动缩放时批量驱逐 Redis Pod。
PDB 配置:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: redis-pdb
spec:
maxUnavailable: 1 # Sentinel:最多 1 个 Pod 不可用
selector:
matchLabels:
app: redis
# Cluster 模式:每个 StatefulSet 至少保留 N-1 个可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: redis-cluster-pdb
spec:
minAvailable: 5 # 6 节点集群至少保持 5 个可用
selector:
matchLabels:
app: redis-cluster
PDB 与故障转移联动:
kubectl drain <node>触发 Pod 驱逐- PDB 限制同时驱逐数量
- 驱逐的 Pod 如果是主节点,Sentinel 触发故障转移
- 从节点提升为新主后,流量切换到新主
- 被驱逐的 Pod 在新节点重新调度
多重保障策略:
| 保障手段 | 作用 |
|---|---|
| PDB | 限制驱逐数量,防止集群大规模不可用 |
| Pod Anti-Affinity | 分散在不同节点,降低同时被驱逐风险 |
| Pod Priority | Redis Pod 优先级高于普通 Pod,避免被优先驱逐 |
terminationGracePeriodSeconds | 足够时间(≥ 60s)完成 BGSAVE 持久化 |
20 Redis vs KeyDB vs Dragonfly 对比
答案:
| 维度 | Redis 7.2 | KeyDB | Dragonfly |
|---|---|---|---|
| 架构 | 单线程事件循环(I/O 多线程) | 多线程(真正的并行处理) | 多线程(无共享架构) |
| 线程模型 | 1 个主线程 + I/O 线程(6.0+) | N 个工作线程并行执行命令 | 分片 + 事务引擎 |
| 性能 | ~100K QPS(单核) | ~500K QPS(多核) | ~1M+ QPS(多核) |
| 兼容性 | Redis 原生协议 | Redis 协议完全兼容 | Redis 协议兼容(部分命令不支持) |
| 内存效率 | 基准 | 与 Redis 相当 | 比 Redis 节省 30%+ 内存(DashTable) |
| 持久化 | RDB + AOF | RDB + AOF | 自定义快照格式 |
| 集群 | Redis Cluster(原生) | Active Replication(多主) | 1.0+ 支持集群 |
| Lua 脚本 | 完全支持 | 完全支持 | 有限支持(无 EVAL) |
| 模块 | RedisJSON/Search/TS/Bloom/Graph | 部分支持 | 不支持原生模块 |
| K8s 部署 | 成熟(多个 Operator) | 社区支持 | 官方 Helm Chart |
| 许可证 | RSALv2 / SSPLv1 | BSD 3-Clause | BSL(Business Source License) |
| 适用场景 | 通用缓存/队列/会话 | 多核高吞吐缓存 | 大内存成本敏感场景 |
选型建议:
- Redis:生态最成熟,模块丰富,企业级支持,K8s Operator 完善
- KeyDB:需要多线程并行处理的纯缓存场景,Redis 协议无缝迁移
- Dragonfly:内存成本敏感,不需要 Redis 模块,追求极致吞吐
参考资料:Redis 官方文档(redis.io/documentation)、Redis Cluster Spec