实践篇 · 设计与选型

存储选型与架构设计

破除"Redis 不可靠 / 太贵"的直觉误区,建立工程化的选型思维

Redis 学习笔记 · 实践 01 · 大纲 · 前置:第 01 篇~第 03 篇

学完数据结构后,最常见的困惑是:"这些场景真的适合单存 Redis 吗?内存不是很贵又容易丢吗?" 本篇将破除两个根深蒂固的直觉误区,并通过黑名单实战案例来演示选型过程。

1. 误区一:"内存数据容易丢"

直觉来源:内存断电即失,进程挂了数据没了。

现实:Redis 早已不是"纯内存数据库",而是"以内存为主存储 + 多级持久化保障"的系统。

持久化保障层级

保障层级机制丢失窗口
RDB 快照定时将全量数据 dump 到磁盘两次快照之间(分钟级)
AOF 日志每条写命令追加到日志文件取决于 fsync 策略
AOF everysec每秒 fsync 一次(默认推荐)最多丢 1 秒数据
AOF always每条命令都 fsync理论不丢(性能代价大)
主从复制数据实时同步到从节点主挂时从节点有完整副本
Sentinel / Cluster自动故障转移秒级切换,几乎无感
MySQL 的数据也在内存里操作(Buffer Pool),靠 redo log 保证持久化。Redis 的 AOF 本质上和 MySQL 的 redo log 是同一个思路——都是 WAL(Write-Ahead Log,预写日志)。你不会觉得"MySQL 数据在内存里所以容易丢",对 Redis 也不该有这个偏见。

实际宕机场景分析

单机 Redis + AOF everysec:
  进程崩溃 → 重启后从 AOF 恢复,最多丢 1 秒写入

主从 + Sentinel(2从1哨兵组):
  主节点宕机 → Sentinel 秒级自动切换到从节点
  数据 → 从节点几乎有全量副本(异步复制延迟通常 < 100ms)

Redis Cluster(3主3从):
  单节点宕机 → 对应从节点自动提升
  要同时丢主从 → 极低概率(不同机架/可用区部署)

"丢了能接受"的判断标准

不是"会不会丢",而是"丢了之后恢复成本多高":

数据类型能否单存 Redis理由
关注/粉丝关系✅ 可以AOF+主从保障充分;极端丢失可从用户行为日志重建
排行榜✅ 可以可从原始数据(点赞表/分数表)重算
Session✅ 可以丢了让用户重新登录,体验可接受
验证码/限流计数✅ 可以本身就是临时数据
购物车⚠️ 看体量小平台可单存;大厂双存避免客诉
订单/支付流水❌ 必须数据库资金相关零容忍

2. 误区二:"大数据量会爆内存"

直觉来源:内存比磁盘贵几十倍,存不起海量数据。

现实:需要算一笔账,而不是凭感觉说"存不起"。

算账示例:1亿用户关注关系

假设条件:
  - 1 亿用户
  - 平均每人关注 200 人
  - 用户 ID 为 64 位整数(8 字节)

纯数据量:
  1亿 × 200 × 8 字节 = 160 GB

加上 Set 结构开销(intset 编码约 1.2x):
  ≈ 192 GB

部署方案:
  Redis Cluster 拆 100 个分片 → 每个节点 ≈ 2 GB
  或 3 台 64GB 内存机器即可承载

成本 vs 收益

方案月成本(预估)获得的能力
3 台 64GB Redis 云主机 ≈ ¥9,000/月 所有关注查询 < 1ms
共同关注实时计算
无需缓存同步逻辑
MySQL + 缓存方案 机器 ¥4,000 + 开发维护人力 关注查询 50~200ms
缓存一致性代码
复杂度高、bug 多

核心公式

如果 Redis 内存成本 < 用数据库实现同等性能所需的(机器 + 人力 + 复杂度)成本,那就该用 Redis。很多时候几千块月租换来的开发效率和查询性能是划算的。

装不下时的工程手段

策略做法适用条件
分片(Cluster)数据按 key hash 分散到多个节点单机装不下但总量可控
冷热分离活跃用户放 Redis,僵尸用户回落 DB数据量大但热数据比例低
编码优化使用 intset/ziplist 等紧凑编码数据类型满足编码条件
降级存储超大集合只存摘要/计数,全量走 DB大 V 粉丝列表等极端场景

3. 选型决策框架

用三个问题替代直觉判断:

问题 1:数据丢失后果严重吗?
├── 不严重 / 可重建 → Redis 单存(+ AOF + 主从)
├── 较严重但需要高性能 → 双存(Redis + DB)
└── 零容忍(资金/合规) → 以 DB 为主,Redis 仅做缓存

问题 2:数据量能装下吗?
├── 先算账:数据量(GB) × 单价 = 月成本
├── 成本可接受 → 全量 Redis
├── 太贵 → 冷热分离(热数据 Redis + 冷数据 DB)
└── 单机装不下 ≠ 方案不可行(分片解决)

问题 3:不用 Redis 的替代方案复杂度如何?
├── DB 方案需要多少额外缓存/索引/代码?
├── 缓存一致性维护成本?
└── 多数情况下 Redis 单存反而是最简方案
选型就像选交通工具:不是"飞机贵所以一律坐火车",而是"这趟行程的时间价值是否值得机票钱"。Redis 的内存开销是"机票",换来的是毫秒级响应和极简的代码架构。

4. 实例:大 V 粉丝列表设计

千万粉丝的大 V 是"全量 Redis"策略的极限挑战。实际工程中采用分层存储

操作存储位置原因
"我是否关注了 TA"Redis Set(SISMEMBER)高频读,O(1) 响应
"共同关注"Redis Set(SINTER)实时性要求高
粉丝总数Redis String(INCR 维护)首页高频展示
粉丝列表第 50 页数据库分页查询低频访问,不值得全放内存
最新 N 个粉丝Redis ZSet(score=关注时间)展示"最近关注",热数据
# 关注操作:同时维护正向 + 反向索引
# user:1001 关注 user:bigV

# 1. 正向:我的关注列表(全量 Redis)
SADD following:1001 "bigV"

# 2. 反向-热数据:大V最近粉丝(ZSet,只保留最近1万)
ZADD followers:bigV:recent 1717200000 "1001"
ZREMRANGEBYRANK followers:bigV:recent 0 -10001

# 3. 反向-计数
INCR followers:bigV:count

# 4. 反向-全量:写入 DB(用于分页浏览)
INSERT INTO follow_relation (follower_id, following_id, created_at) ...

设计原则

热路径走 Redis,冷路径走 DB。判断热冷的标准不是数据本身,而是访问频率——"是否关注"每次进主页都要查(热),"粉丝列表第 50 页"可能一年没人翻(冷)。

5. 实践:高频读低频写的黑名单设计

需求描述:系统需要维护一份黑名单(用户 ID / IP / 设备号等),每次请求都要判断"当前请求者是否在黑名单中"。特征:

5.1 方案一:单存 Redis

数据结构选择

选项命令内存适用规模
Set(推荐)SADD / SISMEMBER中等百万级以下
BitmapSETBIT / GETBIT极小ID 为连续数字时
Bloom FilterBF.ADD / BF.EXISTS极小允许误判时(千万级)

架构设计

┌─────────┐     查询:SISMEMBER      ┌─────────────┐
│  网关层  │ ──────────────────────→ │    Redis    │
│  每个请求│ ←────────────────────── │ blacklist:ip│
└─────────┘     0(不在) / 1(在)      └─────────────┘
                                           │
                                     ┌─────┴─────┐
                                     │ AOF持久化  │
                                     │ 主从复制   │
                                     └───────────┘

┌─────────────┐   写入:SADD / SREM
│ 运营后台    │ ─────────────────────→ Redis
│ 风控系统    │
└─────────────┘

核心代码

# === 初始化 ===
# 批量导入黑名单
SADD blacklist:ip "10.0.0.1" "192.168.1.100" "172.16.0.50"
SADD blacklist:uid "user_evil_1" "user_evil_2"

# === 查询(每个请求) ===
# 判断 IP 是否在黑名单中 → O(1)
SISMEMBER blacklist:ip "10.0.0.1"    → (integer) 1  ← 在黑名单中,拒绝
SISMEMBER blacklist:ip "10.0.0.2"    → (integer) 0  ← 正常放行

# === 写入(低频运营操作) ===
# 拉黑
SADD blacklist:ip "203.0.113.5"

# 解禁
SREM blacklist:ip "10.0.0.1"

# 查看黑名单大小
SCARD blacklist:ip    → (integer) 3

# 批量检查(Redis 6.2+)
SMISMEMBER blacklist:ip "10.0.0.1" "10.0.0.2" "203.0.113.5"
1) (integer) 1
2) (integer) 0
3) (integer) 1

持久化与高可用配置

# redis.conf 关键配置
appendonly yes                    # 开启 AOF
appendfsync everysec             # 每秒刷盘(最多丢 1 秒写入)
auto-aof-rewrite-percentage 100  # AOF 翻倍时自动重写压缩

# 主从复制(至少 1 个从节点)
replicaof master_host 6379

# Sentinel 自动故障转移(生产必备)
sentinel monitor blacklist-master master_host 6379 2

风险与兜底

风险场景后果兜底方案
Redis 宕机 + AOF 损坏黑名单全部丢失,恶意用户放行定期 RDB 备份到 OSS;黑名单源数据留一份在运营后台(文件/表格)可快速重导
主从切换瞬间短暂不可用(1~3 秒)网关层降级策略:Redis 不可用时暂时放行 or 暂时全部拦截(根据业务安全等级选择)
误操作 FLUSHDB黑名单清空开启 rename-command FLUSHDB "" 禁用危险命令

单存方案总结

适用条件:黑名单条目可从外部来源重建(运营记录、风控日志),对极端丢失有容忍度(几秒内恢复即可)。
优势:架构极简、响应极快(<0.1ms)、无一致性问题。
内存估算:100 万个 IP(平均 15 字节)≈ 50~80MB,完全可接受。

5.2 方案二:双存 Redis + DB

当黑名单属于核心安全资产(如金融风控、反欺诈),不能接受任何丢失风险时,采用双存方案。

架构设计

                        读(高频)
┌─────────┐    SISMEMBER    ┌─────────────┐
│  网关层  │ ─────────────→ │    Redis    │
│         │ ←───────────── │  blacklist  │
└─────────┘                 └──────┬──────┘
                                   │ 启动时/定时 全量同步
┌─────────────┐   写入        ┌────┴───────┐
│ 运营后台    │ ───────────→ │     DB     │
│ 风控系统    │               │ blacklist  │
└─────────────┘               └────────────┘
       │                           │
       └── 写DB后通知Redis更新 ────┘

数据库表设计

CREATE TABLE blacklist (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    type        TINYINT NOT NULL COMMENT '1=IP 2=UID 3=设备号',
    value       VARCHAR(128) NOT NULL COMMENT '黑名单值',
    reason      VARCHAR(256) DEFAULT '' COMMENT '拉黑原因',
    operator    VARCHAR(64) DEFAULT '' COMMENT '操作人',
    status      TINYINT DEFAULT 1 COMMENT '1=生效 0=已解禁',
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_type_value (type, value),
    KEY idx_status (status)
) ENGINE=InnoDB COMMENT='黑名单表';

写入流程(低频)

拉黑操作:
  1. 写入 DB(INSERT ... ON DUPLICATE KEY UPDATE status=1)
  2. DB 写入成功后,同步更新 Redis(SADD)
  3. 两步都成功 → 返回成功

解禁操作:
  1. 更新 DB(UPDATE SET status=0)
  2. DB 更新成功后,同步更新 Redis(SREM)
  3. 两步都成功 → 返回成功
# 写入伪代码
def add_to_blacklist(type, value, reason, operator):
    # Step 1: 先写 DB(权威数据源)
    db.execute("""
        INSERT INTO blacklist (type, value, reason, operator, status)
        VALUES (?, ?, ?, ?, 1)
        ON DUPLICATE KEY UPDATE status=1, reason=?, operator=?
    """, type, value, reason, operator, reason, operator)

    # Step 2: 再写 Redis
    redis.sadd(f"blacklist:{type}", value)

def remove_from_blacklist(type, value):
    db.execute("UPDATE blacklist SET status=0 WHERE type=? AND value=?", type, value)
    redis.srem(f"blacklist:{type}", value)

读取流程(高频)

# 直接查 Redis,不回源 DB
# 因为 Redis 中的数据通过写入流程 + 全量同步保证完整性
SISMEMBER blacklist:1 "10.0.0.1"    → 1 则拒绝

为什么读不走 DB?

黑名单查询在每个请求的关键路径上(10万+ QPS),如果走 DB 需要为 SELECT 建连接池、加索引、承受 1~5ms 延迟。Redis 的 SISMEMBER 是 O(1) 内存操作(<0.1ms),差 10~50 倍。DB 是权威存储,Redis 是查询加速层。

一致性保障

机制作用实现
写入时同步 正常情况下 DB 和 Redis 一致 写 DB 成功 → 立即 SADD/SREM Redis
全量同步(兜底) 修复漂移、处理异常场景 定时任务每 N 分钟从 DB 全量加载到 Redis
启动时预热 Redis 重启后数据恢复 服务启动时从 DB 全量 LOAD 到 Redis
# 全量同步任务(定时执行,如每 5 分钟)
def full_sync_blacklist():
    for type in [1, 2, 3]:  # IP, UID, 设备号
        # 从 DB 拉取所有生效的黑名单
        rows = db.query("SELECT value FROM blacklist WHERE type=? AND status=1", type)
        values = [row.value for row in rows]

        key = f"blacklist:{type}"

        # 原子替换:写入临时 key → RENAME 覆盖
        tmp_key = f"{key}:tmp:{timestamp}"
        if values:
            redis.sadd(tmp_key, *values)
            redis.rename(tmp_key, key)    # 原子替换
        else:
            redis.delete(key)             # 清空

为什么用 RENAME 而不是 DEL + SADD?

DEL 旧 key + SADD 新数据之间存在时间窗口——这段时间内黑名单为空,恶意用户可能趁虚而入。RENAME 是原子操作,切换瞬间无间隙。

异常场景处理

场景 A:写 DB 成功,写 Redis 失败
  → 下次全量同步时自动修复
  → 或立即重试 Redis 写入(最多重试 3 次)
  → 最坏情况:恶意用户在同步间隔内逃过检查(分钟级)

场景 B:Redis 宕机
  → Sentinel 自动故障转移到从节点(数据完整)
  → 如果从节点也不可用:
    方案 a) 降级到查 DB(临时,有性能损失)
    方案 b) 服务启动时触发全量同步恢复

场景 C:DB 和 Redis 数据不一致
  → 全量同步定时修复(以 DB 为权威)
  → 监控告警:对比 SCARD 与 DB COUNT,差异超阈值报警

双存方案总结

适用条件:黑名单是安全核心资产,不容许任何丢失;需要审计记录(谁在什么时候拉黑了谁)。
优势:数据绝对不丢(DB 为权威)、读性能极高(Redis 加速)、支持审计和回溯。
代价:架构复杂度增加、需维护同步逻辑、需处理一致性问题。

5.3 方案对比与选择

维度单存 Redis双存 Redis + DB
架构复杂度⭐ 极简⭐⭐⭐ 中等
读性能< 0.1ms< 0.1ms(同样走 Redis)
写性能< 0.1ms1~5ms(多了 DB 写入)
数据安全性⭐⭐ AOF+主从保障⭐⭐⭐⭐ DB 持久化兜底
可审计性❌ 无操作记录✅ DB 有完整操作历史
恢复能力依赖 AOF/RDB 备份DB 随时可重建 Redis
开发成本半天2~3 天
适用场景内部系统、非核心业务
黑名单可从外部重建
金融风控、核心安全
需要审计追溯
单存就像把钱放保险箱(Redis + AOF)——安全性不低,但万一被盗了就没了。双存就像保险箱 + 银行存款(Redis + DB)——随时可以从银行补回来,还有流水单可查。选哪个取决于你保管的是零花钱还是全部身家。

6. 总结心法

破除直觉的三条心法

  1. Redis 丢数据不是"会不会"的问题,而是"丢多少你能不能接受"的问题——AOF everysec + 主从之后,风险已经足够低,绝大多数非资金场景可以单存。
  2. 内存贵不贵不该凭感觉,该算账——把 Redis 内存成本和"不用 Redis 时的替代方案总成本(机器+人力+复杂度)"做对比,很多时候 Redis 反而是最经济的。
  3. 不是"全部 Redis"或"全部 DB"的二选一——分层存储(热路径 Redis + 冷路径 DB)才是工程现实。按访问频率而非数据重要性分层。

思考延伸

  1. 你当前负责的业务中,有哪些数据适合从"DB + 缓存"简化为"Redis 单存"?试着用三问框架评估一下。
  2. 双存方案中,如果全量同步期间有新的写入操作,可能出现什么一致性问题?如何解决?(提示:考虑 RENAME 的时序)
  3. 如果黑名单规模达到千万级(10M 条 IP),Set 仍然合适吗?此时 Bloom Filter 方案有什么优势和劣势?