一、背景与整体架构
在电商、内容推荐等业务里,我们常把 热数据 拆成两层:
数据结构 | 存什么 | 为什么要用它 |
---|---|---|
Hash prod:<id> |
数值型 KV(价格、库存、点击数…) | HINCRBY 、HSET 超快;天然适合计数与并发写 |
JSON prodjs:<id> |
结构化/全文/向量字段 | RediSearch 可直接对 JSON 做全文、过滤、KNN |
核心挑战:一次业务写请求 → 两份数据都要最新可见,且
- 有时必须 强一致(写失败全回滚)
- 有时需要 高吞吐、可弹性(轻微延迟可接受)
下文给出两种落地方案 + 一套索引迁移套路 + 内存预估方法。
二、Lua 事务双写:强一致实时入库
1.1 典型场景
- 订单入库、库存扣减、价格改动等 不能容忍写丢失
- 应用侧不想维护 Redis 事务指令细节
- 单条写吞吐 < 数千 QPS
1.2 脚本 & 调用
-- save_product.lua —— 原子写 Hash + JSON
-- ARGV: id price stock jsonBody
local id, price, stock, body = ARGV[1], ARGV[2], ARGV[3], ARGV[4]
redis.call('HSET', 'prod:'..id,
'price', price,
'stock', stock,
'ts', redis.call('TIME')[1])
redis.call('JSON.SET', 'prodjs:'..id, '$', body)
redis.call('INCR', 'metrics:prod_write_cnt')
return 'OK'
sha=$(redis-cli SCRIPT LOAD "$(cat save_product.lua)")
redis-cli EVALSHA $sha 0 1001 199 8 \
'{"title":"Alpha","desc":"6.7\" AMOLED"}'
1.3 细节拆解
行 | 说明 |
---|---|
redis.call('HSET' …) |
写 KV,并记录毫秒时间戳 |
JSON.SET |
一次性覆盖全文 JSON;内部触发 RediSearch 索引更新 |
INCR metrics:… |
监控指标(可观测) |
Lua 脚本天然单线程 | Redis 内部串行执行 = 原子事务 |
1.4 优缺点
✅ 优点
- 真·原子,成功或失败毫无中间态
- 客户端代码极简:一次 RPC
⚠️ 局限
- 单节点 Lua 执行;高并发 CPU 上升
- 脚本更新需重新加载 SHA
- 脚本逻辑越重,阻塞时间越长 → 建议只做轻量写
三、RedisGears 异步双写:高吞吐流式同步
2.1 典型场景
- 写入峰值达 数万 QPS+
- 可接受 <1 ms 索引延迟
- 希望同步逻辑热插拔、动态升级
2.2 部署一步到位
MODULE LOAD /opt/redisgears/libredisgears.so \
Plugin /opt/redisgears/redisgears-python.so
2.3 JSON→Hash Pipeline
# json2hash.py
import json
from redisgears import executeCommand as cmd, GearsBuilder
def sync_hash(r):
k, ev = r['key'], r['event']
if ev != 'json.set' or not k.startswith('prodjs:'):
return
pid = k.split(':',1)[1]
body = json.loads(cmd('JSON.GET', k))
cmd('HSET', f'prod:{pid}',
'price', body.get('price', 0),
'stock', body.get('stock', 0),
'ts', body.get('updated_at', ''))
cmd('INCR', 'metrics:prod_sync_cnt')
GearsBuilder('KeysReader') \
.foreach(sync_hash) \
.register(prefix='prodjs:*', eventTypes=['json.set'])
RG.PYEXECUTE "$(cat json2hash.py)"
事件流原理
JSON.SET ---> KeySpace Event ---> RedisGears Stream ---> Python Function ---> HSET
2.4 优缺点
✅ 优点
- 写入路径只需操作 JSON,业务简单
- Gears 代码热更新,不影响主线程
- 延迟通常在 µs〜ms 级
⚠️ 局限
- 最终一致,脚本出错会导致滞后
- 依赖 RedisGears 模块(需运维评估)
- 集群模式需每分片都部署同脚本
四、索引热迁移:新 Schema 零停机上线
当你需要:
- 新增向量字段 / 改分词 / 改
SORTABLE
- 重建索引避免碎片(
FT.DUMP
→FT.LOAD
不支持时)
3.1 流程总览
- 创建新索引
idx_new
,带SKIPINITIALSCAN
- 回填历史:
FT.ADDHASH
orFT.ADD
- 对比文档数:
FT.INFO num_docs
- FT.ALIASUPDATE 一行切流
- 验证:业务监控 / A/B
- 可选删除:
FT.DROPINDEX idx_old DD
3.2 Bash 脚本速用
redis-cli FT.CREATE idx_new ON JSON PREFIX 1 prodjs: ... SKIPINITIALSCAN
redis-cli --scan --pattern 'prodjs:*' | while read k; do
redis-cli FT.ADDHASH idx_new "$k" 1.0 REPLACE
done
[ $(redis-cli FT.INFO idx_old |grep num_docs|awk '{print $2}') \
-eq $(redis-cli FT.INFO idx_new |grep num_docs|awk '{print $2}') ] \
|| { echo 'doc mismatch'; exit 1; }
redis-cli FT.ALIASUPDATE prod_read idx_new # 零停机切流
# 回滚: redis-cli FT.ALIASUPDATE prod_read idx_old
3.3 实战小贴士
- 并行回填:用 xargs -P 或多线程脚本
- 回填期间新写入会即时进入 idx_new(未 SKIP 时)
- 监控
indexing
指标确保写放大可接受 - Tag 升级:重建时顺便加
SEPARATOR ";"
等配置
五、RediSearch 容量预估:上线前必做的轰炸测试
4.1 公式来源
组件 | 估算因子 | 含义 |
---|---|---|
倒排表 | T × 6 MB |
百万级词条 ×6 |
Posting 位图 | D × F × 1.5 MB |
文档数 × TEXT 字段 |
SORTABLE | S × 8 MB |
排序字段 |
向量 | V × dim × dt MB |
HNSW/FLAT 数据 |
简化公式
Mem(MB) ≈ (T*6) + (D*F*1.5) + (S*8) + (V*dim*dt/1024)
4.2 样例计算
- 1M 商品 (D=1)
- 300 万词条 (T=3)
- 2 个 TEXT (F=2)
- 3 个 SORTABLE (S=3)
- 向量 1M384float32 (V=1, dim=384, dt=4)
Mem ≈ 18 + 3 + 24 + 1.5 ≈ 46.5 MB
≈ 46.5 ×1.2 ≈ 60 MB (预留 20% + AOF)
上线后 FT.INFO ➜ inverted_sz_mb
, vector_index_sz_mb
再 cross-check INFO memory
,调整参数:
- 减少
SORTABLE
- 打 Tag 的
SEPARATOR
优化 - 向量改 INT8 + 量化
六、结语与延伸阅读
选型结论 | 适用场景 |
---|---|
Lua 事务双写 | 写量低 & 强一致 |
RedisGears 异步 | 高并发写 & 最终一致 |
ALIAUPDATE 热切 | Schema 频繁演进业务 |
容量公式 + INFO | 上线前粗估 & 线上持续校准 |