云厂商托管与改造:ElastiCache、MemoryDB、阿里云 Tair
第三章:云厂商托管与改造:ElastiCache、MemoryDB、阿里云 Tair
3.1 云托管 Redis 的本质价值
自建 Redis 集群需要处理:主从切换(Sentinel)、集群扩缩容、版本升级、监控报警、备份恢复、安全补丁。云托管服务的价值不在于"便宜"(通常更贵),而在于将运维复杂性转移给云厂商。
但不同云厂商的托管服务,底层架构差异极大,选错了可能影响业务可靠性和成本。
3.2 AWS ElastiCache for Redis/Valkey
3.2.1 两种集群模式
Cluster Mode Disabled(CMD):
Primary Node ──── Replica 1
└── Replica 2
单分片,所有 key 在同一节点。读可横向扩展(从副本读),写不能扩展。适合数据集 < 1TB 且不需要写扩展的场景。
Cluster Mode Enabled(CME):
Shard 0: Primary 0 + Replica 0a + Replica 0b (slots 0-5460)
Shard 1: Primary 1 + Replica 1a + Replica 1b (slots 5461-10922)
Shard 2: Primary 2 + Replica 2a + Replica 2b (slots 10923-16383)
16384 个哈希槽分布在多个分片上,每个分片独立扩容。适合大数据集或需要高写入吞吐量的场景。
重要限制:CME 模式下,multi-key 操作(MGET、MSET、事务中的多 key、Lua 脚本访问多 key)要求所有 key 在同一 slot。解决方案:使用 hash tag 强制 key 路由到同一 slot:
# 错误:user:1001 和 order:1001 可能在不同 slot
MGET user:1001 order:1001
# 正确:{1001} 是 hash tag,只对花括号内的内容哈希
MGET {1001}:user {1001}:order
# 两个 key 的 slot = HASH_SLOT("1001") = 固定值
3.2.2 Global Datastore(跨区复制)
Global Datastore 允许在不同 AWS Region 之间建立主从关系:
us-east-1(Primary Cluster)
↓ 异步复制(跨 Region)
eu-west-1(Secondary Cluster)
↓ 异步复制
ap-southeast-1(Secondary Cluster)
特点:
- 跨 Region 复制延迟通常 < 1 秒(AWS 骨干网)
- Secondary 集群只读,灾备场景下可提升为 Primary(RPO < 1s,RTO < 1min)
- 双写到 Primary,跨 Region 读本地 Secondary(降低跨 Region 读延迟)
成本提示:跨 Region 数据传输费用约 $0.02-0.09/GB,对写量大的场景(如日志推送)要提前计算。
3.2.3 2024 年后的变化:拥抱 Valkey
2024 年 10 月起,AWS 新创建的 ElastiCache 集群默认引擎切换为 Valkey(而非 Redis)。对于存量集群,AWS 提供迁移路径,但不强制。
实际差异:ElastiCache for Valkey 7.2 与 ElastiCache for Redis 7.2 在功能和性能上基本等价。关键变化是许可证层面——AWS 不再需要向 Redis Inc. 支付授权费,成本节约可能体现在产品定价上。
3.2.4 迁移注意事项:自建 → ElastiCache
# 方案一:在线迁移(使用 SLAVEOF 将自建 Redis 作为 ElastiCache 的 replica)
# 注意:ElastiCache 不允许对外暴露 SLAVEOF,只能从外部推送 RDB
# 方案二:DUMP/RESTORE 命令(逐 key 迁移)
# 适合数据量不大(< 1GB)的场景
redis-cli -h source-host --pipe < dump.rdb
# 方案三:使用 redis-shake 工具(推荐)
# https://github.com/alibaba/RedisShake
# 支持全量 RDB 同步 + 增量 AOF 同步,停机时间 < 1 秒
./redis-shake -source=source-redis:6379 -target=elasticache-endpoint:6379
# 方案四:AWS DMS(Database Migration Service)
# 支持 Redis → ElastiCache 在线迁移,图形化界面,无需命令行
3.3 AWS MemoryDB for Redis/Valkey(重点)
3.3.1 与 ElastiCache 的本质差异
这是云托管 Redis 中最容易混淆的两个产品,必须清晰理解其差异:
| 维度 | ElastiCache | MemoryDB |
|---|---|---|
| 定位 | 缓存层(Cache) | 持久化内存数据库(Primary DB) |
| 持久化 | 可选(AOF/RDB,但重启可能丢数据) | 强制(多 AZ 事务日志) |
| 写入 ACK 时机 | 主节点本地写入后立即 ACK | 写入提交到所有 AZ 事务日志后才 ACK |
| 写延迟 | < 0.5ms | 2-5ms(持久化代价) |
| 数据丢失风险 | 极端情况下可能丢失数据 | 零数据丢失(类似金融级) |
| 是否可作主数据库 | 否(缓存丢失可重建) | 是(设计目标之一) |
| 价格 | 较低 | 较高(约 1.5-2x ElastiCache) |
3.3.2 MemoryDB 的持久化机制
MemoryDB 不依赖 Redis 自身的 AOF/RDB,而是在 Redis 内核之外构建了一个多 AZ 事务日志(类似 Raft 的分布式日志):
写入流程:
1. 客户端发送 SET key value 到 MemoryDB Primary
2. Primary 将此操作写入多 AZ 分布式事务日志(3 个 AZ 各一份)
3. 等待至少 2 个 AZ 确认写入(quorum)
4. Primary 将操作应用到内存中的 Redis 数据结构
5. 返回 ACK 给客户端
持久化保证:
- 任意单 AZ 故障(包括 Primary 所在 AZ):零数据丢失
- Primary 故障切换:新 Primary 从事务日志恢复,状态与旧 Primary 一致
事务日志使用 S3 作为长期存储,同时在本地 SSD 上保持热日志(最近 N 分钟)供快速故障恢复。
3.3.3 适用场景
MemoryDB 适合的场景是:需要 Redis 的速度,但不能接受数据丢失。
典型案例:
- 会话管理(Session Store):用户购物车数据,丢失意味着用户重新选品,体验极差
- 游戏排行榜:数据丢失导致排名回退,玩家投诉
- 金融级计数器:积分、余额,丢失数据有经济损失
- 轻量级主数据库替代:替代 DynamoDB 处理简单 KV 查询,Redis 语义更丰富且延迟更低
不适合的场景:
- 纯缓存(数据可从数据库重建) → 用 ElastiCache,成本低 50%
- 需要复杂 SQL 查询 → MemoryDB 仍是 KV 模型
- 写延迟敏感(< 1ms 要求)→ MemoryDB 的 2-5ms 写延迟可能不可接受
3.3.4 MemoryDB vs DynamoDB 简单对比
# DynamoDB 风格(JSON Schema,项目级操作)
dynamodb.put_item(
TableName='orders',
Item={
'order_id': {'S': '10086'},
'amount': {'N': '299.00'},
'items': {'L': [{'M': {'sku': {'S': 'A001'}, 'qty': {'N': '2'}}}]}
}
)
# MemoryDB / Redis 风格(多数据结构,更灵活)
r.hset('order:10086', mapping={
'amount': 299.00,
'status': 'pending',
'created_at': int(time.time())
})
r.rpush('order:10086:items', json.dumps({'sku': 'A001', 'qty': 2}))
r.expire('order:10086', 86400 * 30) # 30天后自动清理
MemoryDB 的优势:更低的读写延迟(0.5ms vs 5-10ms for DynamoDB),更丰富的数据结构(ZSet, List, Stream)。DynamoDB 的优势:自动扩缩容、serverless、更强的查询能力(GSI、LSI)。
3.4 阿里云 Tair(最重要的改造)
3.4.1 背景:从 ApsaraDB for Redis 到 Tair
2021 年,阿里云将 ApsaraDB for Redis 升级为 Tair,并开始大规模改造 Redis 内核,引入原生 Redis 不支持的新特性。Tair 是在 Redis 兼容协议基础上,针对阿里内部业务痛点开发的增强版。
以下所有 TairXxx 功能均通过自定义 Redis 模块实现(Redis Module API),在 Tair 实例上默认加载,用户无需额外配置。
3.4.2 TairString:带版本号的 CAS
问题:原生 Redis 实现原子 CAS 需要 Lua 脚本:
-- 繁琐的 Lua CAS 实现
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1
else
return 0
end
TairString 方案:
# EXSET:设置值并指定版本号
EXSET mykey "value-v1" VER 1
# EXCAS:Compare-And-Set,指定期望版本号
EXCAS mykey "value-v2" 1
# 返回:OK(如果当前版本=1,设置新值并版本+1)
# 返回:ERR(如果版本不匹配)
# EXGET:获取值和版本号
EXGET mykey
# 返回:1) "value-v2" 2) (integer) 2
# 实际使用场景:乐观锁
version = int(r.execute_command('EXGET', key)[1])
# ... 业务处理 ...
result = r.execute_command('EXCAS', key, new_value, version)
if result == 'OK':
print("更新成功")
else:
print("版本冲突,需要重试")
3.4.3 TairHash:Field 级别 TTL
问题:Redis Hash 只能对整个 key 设置 TTL,无法对单个 field 设置过期时间。
常见 workaround 是用独立的 String key 模拟每个 field,但这样会失去 Hash 的原子性优势:
# 原生 Redis 的笨拙方案
SET user:1001:name "Alice" EX 3600
SET user:1001:session "token-xyz" EX 300 # session 5分钟过期
# 无法原子地获取 user:1001 的所有字段
TairHash 方案:
# EXHSET:设置 field,支持单独 TTL
EXHSET user:1001 name "Alice"
EXHSET user:1001 session "token-xyz" EX 300 # session 5分钟后过期
# EXHGET:获取 field 值
EXHGET user:1001 session # 返回 "token-xyz"(未过期)
# 5分钟后:
EXHGET user:1001 session # 返回 nil(已过期)
# EXHGETALL:获取所有未过期的 field
EXHGETALL user:1001
# 返回:name → "Alice"(session 已过期不返回)
# EXHEXPIRE:修改某个 field 的过期时间
EXHEXPIRE user:1001 session 600 # 将 session TTL 延长到 10 分钟
# EXHTTL:查询 field 剩余 TTL
EXHTTL user:1001 session # 返回剩余秒数
实现原理:TairHash 在每个 field 旁存储一个过期时间戳。后台线程(类似 Redis 的 active expiry)定期扫描并删除过期 field。访问时检查 field 时间戳(类似 lazy expiry)。
应用场景:用户会话 Token(短 TTL)+ 基本信息(长 TTL)存在同一 Hash;商品属性中,某些字段有时效性(如促销标签)。
3.4.4 TairZset:多维度评分
问题:原生 ZSet 只支持单一 score,复合排序(如按分数降序、按时间升序)需要 Lua 脚本或客户端处理。
TairZset 方案:支持多维度 (score1, score2, ..., scoreN) 联合排序:
# 添加成员,指定多个维度的 score
# 语法:EXZADD key [NX|XX] score1 score2 ... member
EXZADD leaderboard 9850 1703000000 "player:1001" # score=9850, 时间戳=1703000000
EXZADD leaderboard 9850 1703000100 "player:2007" # 同分,晚一点
# 按第一维度降序排序,同分时按第二维度升序(先得高分、同分时先达到的排前面)
EXZREVRANGEBYSCORE leaderboard 10000 0 EXWITHSCORES
# 范围查询:score1 在 [8000, 10000] 之间
EXZRANGEBYSCORE leaderboard 8000 10000 EXWITHSCORES
# 条件:score1 >= 9000 AND score2 <= 1703000200
EXZRANGEBYSCORE leaderboard "(9000,0" "+inf" EXWITHSCORES
应用场景:
- 游戏排行榜:同分时按达到时间排序(先达到的排前面)
- 电商搜索结果:相关度 × 销量综合排序
- 任务调度:优先级 × 创建时间双维度排序
3.4.5 TairDoc:原生 JSON 路径查询
问题:Redis 原生不支持 JSON 存储和查询,通常用序列化字符串存储,查询时需要获取全量数据后在客户端解析。RedisJSON 模块提供了部分支持,但阿里云 Tair 通过 TairDoc 提供了更深的集成:
# 存储 JSON 文档
EXJSONSET config $ '{"database":{"host":"127.0.0.1","port":6379,"pool":{"min":5,"max":50}}}'
# JSONPath 查询
EXJSONGET config $.database.host
# 返回:["127.0.0.1"]
EXJSONGET config $.database.pool.max
# 返回:[50]
# 修改嵌套字段(无需读取整个文档)
EXJSONSET config $.database.pool.max 100
# 追加到数组
EXJSONSET config $.database.replicas '[]'
EXJSONARRAYAPPEND config $.database.replicas '"replica1:6380"'
EXJSONARRAYAPPEND config $.database.replicas '"replica2:6380"'
# 获取数组长度
EXJSONARRLEN config $.database.replicas # 返回 2
3.4.6 TairBloom:可扩容布隆过滤器
问题:Redis 原生 BF(RedisBloom)在创建时必须指定固定容量,满后无法扩容,需要重建整个过滤器。
TairBloom 方案:
# 创建可扩容布隆过滤器(不需要指定容量)
TFBFRESERVE mybloom 0.001 # 错误率 0.1%,初始容量自动管理
# 添加元素
TFBFADD mybloom "user:10086"
# 批量添加
TFBFMADD mybloom "user:1" "user:2" "user:3"
# 查询(可能存在的概率)
TFBFEXISTS mybloom "user:10086" # 返回 1(大概率存在)
TFBFEXISTS mybloom "user:99999" # 返回 0(一定不存在)
# 当元素数量超过当前容量时,自动创建新的 sub-filter
# 用户无感知,过滤器自动扩容
内部实现:TairBloom 实际上是多个固定大小 BF 的级联(类似 Scalable Bloom Filter 论文的实现)。查询时依次检查所有 sub-filter,任一返回 exists 则认为存在。
错误率分析:
- 单个 BF 的 FPR = 指定值(如 0.1%)
- 级联 N 个 BF 后的总 FPR ≤ N × 0.1%(可通过调整 growth ratio 控制)
- 实际生产中,TairBloom 默认 FPR 增长控制在 2x 以内
3.4.7 TairGIS:地理围栏与多边形查询
问题:Redis 原生 GEO 命令(GEORADIUS/GEOSEARCH)只支持圆形范围查询,无法做多边形围栏。
TairGIS 方案:
# 添加地理位置(支持 Point/Line/Polygon GeoJSON 格式)
GISADD stores '{"type":"Feature","geometry":{"type":"Point","coordinates":[116.397,39.907]},"properties":{"name":"store-001"}}'
# 添加多边形围栏
GISADD zones '{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[116.39,39.90],[116.41,39.90],[116.41,39.92],[116.39,39.92],[116.39,39.90]]]},"properties":{"name":"delivery-zone-1"}}'
# 查询某点是否在多边形内
GISCONTAINS zones '{"type":"Point","coordinates":[116.395,39.910]}'
# 返回:delivery-zone-1(所在围栏名称)
# 查询多边形内的所有 Point
GISWITHIN zones stores
# 返回落在指定围栏内的所有 store
# 两个多边形是否相交
GISINTERSECTS zones '{"type":"Polygon","coordinates":[[[116.400,39.905],[116.420,39.905],[116.420,39.925],[116.400,39.925],[116.400,39.905]]]}'
应用场景:外卖平台的配送范围判断、打车平台的服务区域判断、地理围栏营销(用户进入/离开商圈时触发推送)。
3.5 腾讯云 Redis
腾讯云 Redis 分为两个产品线:
标准版(Standard):类似 ElastiCache CMD,单分片主从架构,支持 Sentinel 自动切换,适合 < 100GB 数据集。
集群版(Cluster):类似 ElastiCache CME,多分片架构,最高支持 4TB 数据,通过内置 Proxy 层对客户端透明化分片逻辑。
Proxy 层设计:腾讯云集群版在客户端和 Redis 节点之间插入了 Proxy 层:
Client → Proxy(路由 + 聚合多 key 操作)→ Redis Node 0
→ Redis Node 1
→ Redis Node 2
优点:客户端无需感知集群拓扑,普通 Redis 客户端直接可用(无需 Cluster-aware 客户端)。 缺点:Proxy 层增加约 0.1-0.3ms 延迟,Proxy 本身成为潜在瓶颈。
3.6 其他云厂商
Azure Cache for Redis:
- 标准层(C tier):基础缓存
- 企业层(E tier):使用 Redis Enterprise 引擎(不是开源 Redis),支持 RediSearch(全文搜索)、RedisJSON、RedisTimeSeries 等企业模块
- 注意:E tier 价格显著高于标准层,按模块使用计费
Google Cloud Memorystore for Redis/Valkey:
- 2024 年增加 Valkey 引擎支持(与 Redis 7.2 兼容)
- 支持 VPC 内网访问,集成 Cloud IAM
- Memorystore for Redis Cluster:最高支持 5TB,300 万 QPS
Upstash(Serverless Redis):
- 按请求计费($0.2/100K 请求),无固定月费
- 适合低频访问场景(开发测试、边缘函数)
- 底层基于 Turso/自研存储,数据持久化到磁盘
- 注意:冷启动延迟较高(10-50ms),不适合延迟敏感场景
3.7 Redis 许可证风波完整时间线
2024-03-20 (周三) Redis Inc. 官网更新:宣布 Redis 7.4 起采用 RSALv2+SSPLv1
2024-03-20 (周三) GitHub 上 Redis 仓库 README 更新,社区开始讨论
2024-03-21 (周四) Linux Foundation 宣布创建 Valkey 项目
2024-03-21 (周四) AWS 宣布支持 Valkey,承诺将 ElastiCache/MemoryDB 切换引擎
2024-03-21 (周四) Google Cloud 宣布支持 Valkey
2024-03-21 (周四) Alibaba Cloud 加入 Valkey
2024-03-22 (周五) Ericsson、Snap 等企业宣布加入
2024-03-27 (周三) Oracle、Verizon 等加入
2024-04-02 Valkey GitHub 仓库正式开放 (github.com/valkey-io/valkey)
2024-04-16 Valkey 7.2.5 发布(首个正式版本,完整 BSD 许可证)
2024-06-11 Valkey 8.0.0-rc1 发布(多线程 I/O 改进)
2024-08 AWS 开始将新 ElastiCache 集群默认设为 Valkey
2024-10-01 AWS 官宣 ElastiCache 完全切换 Valkey 为默认引擎
2024-Q4 Alibaba Tair 支持 Valkey 协议兼容层
对现有 Redis 企业用户的影响:
- 使用 Redis 7.2 及以下版本的用户:可以继续使用(仍是 BSD 许可证)
- 升级到 Redis 7.4+:受 RSALv2 约束,企业内部使用仍可,但不能将其作为服务对外销售
- 云托管用户:各大云厂商已切换为 Valkey,用户无感知
3.8 选型对比表
| 维度 | ElastiCache | MemoryDB | 阿里云 Tair | 腾讯云 Redis |
|---|---|---|---|---|
| 最大数据量 | 340 GB/分片 | 427 GB/分片 | 4 TB(集群) | 4 TB(集群) |
| 写延迟(p99) | < 1ms | 3-8ms | < 1ms | < 1ms |
| 持久化保证 | 可选 | 强制多 AZ | 可选 | 可选 |
| 特殊数据结构 | 无 | 无 | TairHash/ZSet/GIS 等 | 无 |
| 价格(128GB) | ~$500/月 | ~$900/月 | ~$400/月 | ~$350/月 |
| SLA | 99.99% | 99.99% | 99.99% | 99.99% |
| 适用场景 | 通用缓存 | 持久化主存 | 特殊业务场景 | 通用缓存 |
| 跨区复制 | Global Datastore | Global Clusters | 跨地域同步 | 全球地域复制 |
3.9 小结
云托管 Redis 产品不是"Redis + 托管运维"那么简单,不同产品在数据持久化、数据结构扩展、集群架构上有根本性的差异:
- ElastiCache:缓存场景的最佳选择,成本最优
- MemoryDB:需要 Redis 语义但不能丢数据时的首选
- 阿里云 Tair:有特殊业务需求(field TTL、多维排序、地理围栏)时的差异化选项
下一章将进入 Redis 内核层面的分析,从 redisObject 结构体开始,解析 Redis 的内存对象系统。