redis | 三种交互数据缓存设计方案 | 对比文档

5267 字
26 分钟
redis | 三种交互数据缓存设计方案 | 对比文档

redis | 三种交互数据缓存设计方案 | 对比文档#

本方案基于 Redis 缓存实现交互数据的高性能读写,提供三种方案:Set + MQ(通用)、Set + 定时扫描(无 MQ 依赖)、Bitmap + MQ(极致内存)。评审重点:三种方案的适用边界、Bitmap 在分布式场景下的扩展性、雪花 ID 带来的内存风险、兜底降级策略的完整性。


一、问题陈述#

当前系统面临交互数据热点问题:单篇内容 1 分钟内涌入 10 万点赞时,数据库连接池被打满、行锁冲突、主从延迟达 800ms 以上。现有纯数据库方案的点赞操作平均响应时间为 480ms,P99 达 2.1s,无法支撑日均千万级交互操作。

目标:将交互操作响应时间降至 10ms 以内,QPS 提升至万级,支持 1 亿用户规模。


二、数据结构选型#

数据结构存储方式内存占用(1亿用户)典型操作适用场景
String简单键值对高(每个 key 独立)SET/GET/INCR单个计数、缓存
Hash字段-值映射中(共享 key)HSET/HGET/HINCRBY对象属性、聚合计数
Set无序唯一集合~2GBSADD/SREM/SISMEMBER关系存储、去重
Bitmap位数组~12MBSETBIT/GETBIT/BITPOS状态标记(是/否)

选型结论

  • Set:通用方案,存储用户级状态,无 ID 类型限制
  • Bitmap:极致内存方案,存储用户级状态,需连续整数 ID
  • Hash:所有方案共用,存储内容级计数(一个 key 聚合多维度)

三、方案设计与交互流程#

本方案提供三种设计,按状态存储的数据结构区分,入库策略随之不同:

维度方案一:Set + MQ方案二:Set + 定时扫描方案三:Bitmap + MQ
状态存储Set(无 ID 限制)Set(无 ID 限制)Bitmap(需连续整数 ID)
计数存储HashHashHash
脏标记无(MQ 替代)Dirty Set无(MQ 替代)
入库方式MQ 消息异步写库定时任务扫描脏集合写库MQ 消息异步写库
内存占用(1亿用户)~2GB~2GB~12MB
数据延迟秒级分钟级秒级
适用场景通用方案,支持任意 ID 类型无 MQ 依赖,需批量合并自增 ID,追求极致内存

Set vs Bitmap 适用场景

用户量ID 类型ID 最大值Set 内存Bitmap 内存推荐方案
< 1000 万自增 ID< 1000 万~200MB~1.2MBBitmap
1000 万~1 亿自增 ID< 1 亿~2GB~12MBBitmap
> 1 亿自增 ID< 42 亿> 2GB~500MB均可,Bitmap 仍有优势
任意雪花 ID~2^40~2GB~125GBSet
任意UUID~2GB不可用Set
100 万自增 ID(稀疏)10 亿~20MB~120MBSet

判断标准ID 最大值 / 实际用户数 > 100 时,Bitmap 内存优势消失,应选 Set。

3.1 共享 Key 概念#

三种方案共享三个 Key 概念,不同方案按需组合:

Key 概念职责对应问题数据结构选择
状态 Key记录「谁做了」用户是否已操作?Set(方案一/二)或 Bitmap(方案三)
计数 Key记录「做了多少」当前有多少人操作?Hash(所有方案)
脏标记 Key记录「谁变了」哪些数据需要同步到 DB?Dirty Set(仅方案二)

Key 命名规范

状态 Key: {interactionType}:{storageType}:{targetType}:{targetId}
方案一/二: like:set:1:10086 (Set 存储)
方案三: like:bit:1:10086 (Bitmap 存储)
计数 Key: stat:{targetType}:{targetId}
示例: stat:1:10086
字段: like:1 = 128 (表示该笔记有 128 个点赞)
脏标记 Key: dirty:{interactionType}:{targetType}
示例: dirty:like:1
含义: 笔记类型下所有发生点赞变更的 targetId 集合

组合规则

  • MQ 入库方案(一、三):消息本身携带变更信息,无需脏标记 Key
  • 定时扫描入库方案(二):需要脏标记 Key 追踪变更,替代 KEYS * 全库扫描

3.2 方案一:Set + MQ 异步入库#

Key 设计#

Key 类型命名格式示例职责
Set{type}:set:{targetType}:{targetId}like:set:1:10086存储已操作的用户 ID 集合
Hashstat:{targetType}:{targetId}stat:1:10086聚合计数

MQ 消息本身携带变更信息,无需脏标记 Key。

交互流程#

┌─────────────────────────────────────────────────────────┐
│ 用户交互请求 │
└──────────────────────────┬──────────────────────────────┘
┌─────────────────────┐
│ 1. 参数校验 │
└──────────┬──────────┘
┌─────────────────────┐
│ 2. SISMEMBER │
│ like:set:1:10086 │
│ userId │
└──────────┬──────────┘
┌─────┴─────┐
│ 已存在? │
└─────┬─────┘
┌──────┴──────┐
▼ ▼
是 否
│ │
▼ ▼
┌──────────┐ ┌──────────────────────────┐
│ 返回 │ │ 3. SADD │
│ false │ │ like:set:1:10086 42 │
│ 已点赞 │ └────────────┬─────────────┘
└──────────┘ │
┌──────────────────────────┐
│ 4. HINCRBY │
│ stat:1:10086 like:1 1 │
└────────────┬─────────────┘
┌──────────────────────────┐
│ 5. MQ 发送消息 │
│ {userId, targetId, │
│ type, action} │
└────────────┬─────────────┘
┌─────────────┐
│ 返回 true │
│ 点赞成功 │
└─────────────┘

执行步骤

  1. 参数校验:校验 type/targetId/userId,非法参数直接返回 false
  2. SISMEMBER 判断是否已操作SISMEMBER like:set:1:10086 42
    • 返回 1:已点赞,直接返回 false
    • 返回 0:未点赞,继续下一步
  3. SADD 添加用户 IDSADD like:set:1:10086 42
  4. HINCRBY 计数+1HINCRBY stat:1:10086 like:1 1
  5. MQ 发送消息:与 Redis 写入并行执行

取消点赞

Terminal window
SREM like:set:1:10086 42
HINCRBY stat:1:10086 like:1 -1
# MQ 发送 action=unlike 消息

入库方式:MQ 异步消费#

┌─────────────────────────────────────────────────────────┐
│ 用户交互请求 │
└──────────────────────────┬──────────────────────────────┘
┌────────────┴────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Redis 写入 │ │ MQ 发送 │
│ SADD │ │ Topic: │
│ HINCRBY │ │ interaction │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 返回用户 │ │ MQ Consumer │
│ 操作结果 │ │ 消费消息 │
└───────────────┘ └───────┬───────┘
┌──────┴──────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 写入用户记录 │ │ 更新统计记录 │
│ INSERT/UPDATE │ │ user_id=0 │
└───────────────┘ └───────────────┘

MQ 消息格式

{
"userId": 42,
"targetType": 1,
"targetId": 10086,
"interactionType": 1,
"action": "like",
"timestamp": 1715673600000
}

为什么不需要脏标记 Key:MQ 消息本身就是变更的载体,每条消息包含完整的操作信息(谁、对什么、做了什么),消费者直接根据消息内容写库,无需反查 Redis 来确定”哪些数据变了”。

消费者幂等保障INSERT ... ON DUPLICATE KEY UPDATE(依赖唯一索引)。

3.3 方案二:Set + 定时扫描入库#

Key 设计#

Key 类型命名格式示例职责
Set{type}:set:{targetType}:{targetId}like:set:1:10086存储已操作的用户 ID 集合
Hashstat:{targetType}:{targetId}stat:1:10086聚合计数
Dirty Setdirty:{type}:{targetType}dirty:like:1标记哪些内容有待同步的变更

定时扫描方案需要知道”哪些数据发生了变更”,引入 Dirty Set 替代 KEYS * 全库扫描。

交互流程#

┌─────────────────────────────────────────────────────────┐
│ 用户交互请求 │
└──────────────────────────┬──────────────────────────────┘
┌─────────────────────┐
│ 1. 参数校验 │
└──────────┬──────────┘
┌─────────────────────┐
│ 2. SISMEMBER │
│ like:set:1:10086 │
│ userId │
└──────────┬──────────┘
┌─────┴─────┐
│ 已存在? │
└─────┬─────┘
┌──────┴──────┐
▼ ▼
是 否
│ │
▼ ▼
┌──────────┐ ┌──────────────────────────┐
│ 返回 │ │ 3. SADD │
│ false │ │ like:set:1:10086 42 │
│ 已点赞 │ └────────────┬─────────────┘
└──────────┘ │
┌──────────────────────────┐
│ 4. HINCRBY │
│ stat:1:10086 like:1 1 │
└────────────┬─────────────┘
┌──────────────────────────┐
│ 5. SADD │
│ dirty:like:1 10086 │
└────────────┬─────────────┘
┌─────────────┐
│ 返回 true │
│ 点赞成功 │
└─────────────┘

执行步骤

  1. 参数校验:校验 type/targetId/userId,非法参数直接返回 false
  2. SISMEMBER 判断是否已操作SISMEMBER like:set:1:10086 42
    • 返回 1:已点赞,直接返回 false
    • 返回 0:未点赞,继续下一步
  3. SADD 添加用户 IDSADD like:set:1:10086 42
  4. HINCRBY 计数+1HINCRBY stat:1:10086 like:1 1
  5. SADD 标记脏数据SADD dirty:like:1 10086

取消点赞

Terminal window
SREM like:set:1:10086 42
HINCRBY stat:1:10086 like:1 -1
SADD dirty:like:1 10086

入库方式:定时扫描#

┌──────────────────────────────────────────────────────┐
│ 定时任务触发(每 5 分钟) │
└──────────────────────────┬───────────────────────────┘
┌───────────────────────┐
│ Step 1: SMEMBERS │
│ dirty:like:1 │
│ → [10086, 20001, ...] │
└───────────┬───────────┘
┌───────────────────────┐
│ Step 2: 遍历每个 │
│ targetId │
└───────────┬───────────┘
┌──────────────┴──────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Step 3a │ │ Step 3b │
│ SMEMBERS │ │ HGETALL │
│ like:set:1:10086 │ │ stat:1:10086 │
│ 获取用户ID集合 │ │ 获取所有计数字段 │
│ 写入用户记录 │ │ 写入统计记录 │
│ INSERT/UPDATE │ │ user_id=0 │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ DEL Set Key │ │ 保留 Hash Key │
│ (已同步,可删除) │ │ (继续累加新操作) │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
└──────────────┬──────────────┘
┌───────────────────┐
│ Step 4: SREM │
│ dirty:like:1 │
│ 10086 │
│ (移除已同步的ID) │
└───────────────────┘

扫描步骤

  1. 获取脏数据列表SMEMBERS dirty:like:1 → 得到 [10086, 20001, ...]
  2. 遍历每个 targetId
    • Set 同步SMEMBERS like:set:1:10086 获取所有已操作用户 ID,写入/更新 DB 用户记录
    • Hash 同步HGETALL stat:1:10086 获取所有计数字段,写入 user_id=0 的统计记录
  3. 清理已同步数据
    • 删除已同步的 Set Key(DEL like:set:1:10086
    • 保留 Hash Key(继续累加新操作)
    • 从 Dirty Set 移除已同步的 targetId(SREM dirty:like:1 10086

Dirty Set 的核心价值:定时任务只需 SMEMBERS dirty:like:1 获取变更列表,而非 KEYS like:set:* 全库扫描。KEYS 命令时间复杂度 O(N) 且阻塞 Redis,生产环境禁止使用。

3.4 方案三:Bitmap + MQ 异步入库#

方案一/二使用 Set 存储用户状态,内存占用与用户数成正比(1 亿用户约 2GB)。当用户 ID 为连续整数且追求极致内存时,可用 Bitmap 替代 Set,内存降至约 12MB。

Key 设计#

Key 类型命名格式示例职责
Bitmap{type}:bit:{targetType}:{targetId}like:bit:1:10086用位偏移标记用户是否已操作
Hashstat:{targetType}:{targetId}stat:1:10086聚合计数

MQ 消息本身携带变更信息,无需脏标记 Key。

Bitmap 的前提条件:用户 ID 必须为连续整数(自增 ID),且最大 ID 不能过大。雪花 ID、UUID、稀疏 ID 场景不适用,详见第六章。

交互流程#

┌─────────────────────────────────────────────────────────┐
│ 用户交互请求 │
└──────────────────────────┬──────────────────────────────┘
┌─────────────────────┐
│ 1. 参数校验 │
└──────────┬──────────┘
┌─────────────────────┐
│ 2. SETBIT │
│ like:bit:1:10086 │
│ userId → 1 │
└──────────┬──────────┘
┌─────┴─────┐
│ 旧值 = ? │
└─────┬─────┘
┌──────┴──────┐
▼ ▼
旧值=1 旧值=0
│ │
▼ ▼
┌──────────┐ ┌──────────────────────────┐
│ 返回 │ │ 3. HINCRBY │
│ false │ │ stat:1:10086 like:1 1 │
│ 已点赞 │ └────────────┬─────────────┘
└──────────┘ │
┌──────────────────────────┐
│ 4. MQ 发送消息 │
│ {userId, targetId, │
│ type, action} │
└────────────┬─────────────┘
┌─────────────┐
│ 返回 true │
│ 点赞成功 │
└─────────────┘

执行步骤

  1. 参数校验:校验 type/targetId/userId,非法参数直接返回 false
  2. SETBIT 原子操作SETBIT like:bit:1:10086 42 1
    • 返回旧值=0:未点赞过,继续下一步
    • 返回旧值=1:已点赞,直接返回 false
  3. HINCRBY 计数+1HINCRBY stat:1:10086 like:1 1
  4. MQ 发送消息:与 Redis 写入并行执行

核心优势SETBIT 返回旧值的设计,让”判断是否已点赞”和”设置已点赞”合并为一次原子操作(方案一/二需要 SISMEMBER + SADD 两次操作),无需分布式锁,彻底避免并发重复计数。

取消点赞

Terminal window
SETBIT like:bit:1:10086 42 0
# 返回旧值=1: 已点赞,继续下一步
# 返回旧值=0: 未点赞,直接返回 false
HINCRBY stat:1:10086 like:1 -1
# MQ 发送 action=unlike 消息

入库方式:MQ 异步消费#

与方案一相同,MQ 消费者异步消费消息写入数据库。消息格式、幂等保障均一致。

3.5 四种交互场景的实现差异#

交互类型状态存储(方案一/二)状态存储(方案三)计数存储特殊机制
点赞Set (like:set)Bitmap (like:bit)Hash (like:{code})方案三:SETBIT 原子 toggle
收藏Set (collect:set)Bitmap (collect:bit)Hash (collect:{code})方案三:SETBIT 原子 toggle
关注Set (follow:set:{userId})Bitmap (follow:bit:{userId})Hash (follow:{code})Key 维度为用户
浏览无(SETNX 锁)无(SETNX 锁)Hash (view:{code})5 分钟防重复锁

3.6 三种方案对比#

维度方案一:Set + MQ方案二:Set + 定时扫描方案三:Bitmap + MQ
Redis Key 数量2(Set + Hash)3(Set + Hash + Dirty Set)2(Bitmap + Hash)
去重判断SISMEMBER + SADD(两次操作)SISMEMBER + SADD(两次操作)SETBIT(一次原子操作)
内存占用(1亿用户)~2GB~2GB~12MB
用户 ID 限制无限制无限制需连续整数 ID
数据延迟秒级分钟级秒级
DB 写入量每次操作一条记录批量合并每次操作一条记录
基础设施依赖依赖 MQ 集群仅 Redis + 定时任务依赖 MQ 集群
实现复杂度中(需 ID 映射或连续 ID)
一致性保障MQ 重试 + 死信队列扫描周期 + 数据校验MQ 重试 + 死信队列
适用规模中小规模中大规模(需批量合并)大规模(自增 ID + 极致内存)

选型建议

  • 默认选方案一:通用、简单、无 ID 限制,适合大多数场景
  • 无 MQ 基础设施选方案二:仅依赖 Redis,定时扫描批量合并写入
  • 自增 ID + 亿级用户选方案三:内存从 2GB 降至 12MB,SETBIT 原子去重减少一次 Redis 调用

四、数据同步与缓存预热#

4.1 数据库表设计#

CREATE TABLE document_user_interaction (
id BIGINT PRIMARY KEY,
user_id BIGINT COMMENT '用户ID(0表示统计记录)',
target_type INT COMMENT '目标类型: 1=笔记, 2=视频, 3=评论, 4=用户',
target_id BIGINT COMMENT '目标ID',
interaction_type INT COMMENT '交互类型: 1=点赞, 2=收藏, 3=关注, 4=浏览',
status INT COMMENT '状态: 0=取消, 1=有效, 统计记录存储计数',
create_time DATETIME,
update_time DATETIME,
UNIQUE KEY uk_user_target (user_id, target_type, target_id, interaction_type)
);

4.2 缓存预热#

服务重启后从数据库预热缓存:

public void warmCacheFromDb(Integer type, Long targetId) {
redisTemplate.delete(stateKey);
List<DocUserInteraction> interactions = mapper.selectByTarget(
targetType, targetId, INTERACTION_TYPE_LIKE);
for (DocUserInteraction interaction : interactions) {
if (useBitmap) {
redisTemplate.opsForValue().setBit(stateKey, interaction.getUserId(), true);
} else {
redisTemplate.opsForSet().add(stateKey, interaction.getUserId().toString());
}
}
Long dbCount = mapper.countByTarget(targetType, targetId, INTERACTION_TYPE_LIKE);
redisTemplate.opsForHash().put(statKey, countField, dbCount.toString());
}

预热时机:服务启动时批量预热热点内容;用户首次查询时懒加载预热。


五、兜底与降级方案#

5.1 Redis 不可用时的降级策略#

┌──────────────────────────────────────────────────────┐
│ Redis 健康检查 │
│ (每次操作前探测) │
└──────────────────────────┬───────────────────────────┘
┌──────┴──────┐
▼ ▼
Redis 可用 Redis 不可用
│ │
▼ ▼
┌───────────┐ ┌───────────────────┐
│ 正常流程 │ │ 降级流程 │
│ 缓存写入 │ │ 直接写入数据库 │
└───────────┘ │ INSERT/UPDATE │
│ 开启限流 │
└───────────────────┘

降级策略

  1. 直接写库:Redis 不可用时,交互操作降级为直接写入数据库
  2. 限流保护:降级状态下对交互接口限流(如 1000 QPS),防止 DB 被打满
  3. 降级状态管理:通过配置中心或本地缓存标记降级状态,避免每次请求都探测 Redis
  4. 自动恢复:Redis 恢复后自动切换回缓存模式,并从数据库预热热点数据

降级代码示例

public boolean like(Long userId, Integer targetType, Long targetId) {
if (redisHealthChecker.isAvailable()) {
return likeViaRedis(userId, targetType, targetId);
}
return likeViaDatabase(userId, targetType, targetId);
}
private boolean likeViaDatabase(Long userId, Integer targetType, Long targetId) {
rateLimiter.acquire();
DocUserInteraction existing = mapper.selectByUserAndTarget(
userId, targetType, targetId, INTERACTION_TYPE_LIKE);
if (existing != null && existing.getStatus() == 1) {
return false;
}
mapper.insertOrUpdate(userId, targetType, targetId,
INTERACTION_TYPE_LIKE, 1);
return true;
}

5.2 数据不一致的修复机制#

Redis 与数据库之间可能出现数据不一致,需要定期校验和修复。

不一致场景

场景原因影响
Redis 写入成功,DB 写入失败MQ 消费失败 / 定时扫描中断计数偏大
Redis 写入失败,DB 未写入Redis 超时但操作已执行计数偏小
状态 Key 与 Hash 计数不一致并发操作中间状态状态与计数矛盾

修复方案

  1. 定时对账任务:每小时抽样对比 Redis Hash 计数与 DB 统计记录,偏差超过阈值时触发全量校验
  2. 全量校验(方案三)BITCOUNT like:bit:1:10086 统计 Bitmap 中 1 的个数,与 Hash 中的计数对比,不一致时以 Bitmap 为准修复 Hash
  3. 全量校验(方案一/二)SCARD like:set:1:10086 获取 Set 成员数,与 Hash 中的计数对比,不一致时以 Set 为准修复 Hash
  4. DB 计数修复SELECT COUNT(*) FROM document_user_interaction WHERE ... 与 Hash 对比,不一致时以 DB 为准(DB 是最终数据源)

5.3 MQ 消息丢失的兜底#

MQ 方案(一、三)下,消息可能因网络故障、消费者宕机等原因丢失。

保障层级措施覆盖场景
生产者确认MQ 发送确认(Publisher Confirm)生产者发送失败
消息持久化MQ 开启消息持久化MQ Broker 宕机
消费者手动 ACK消费者处理成功后手动确认消费者处理异常
死信队列消费重试 3 次后进入死信队列消费反复失败
定时对账定时对账任务校验 Redis 与 DB 一致性以上所有兜底

关键设计:MQ 方案下无需 Dirty Set,消息丢失的兜底依赖定时对账任务(5.2 节)。对账任务发现 Redis 与 DB 不一致时,以 DB 为准修复 Redis 缓存。

5.4 缓存击穿防护#

热点内容缓存过期瞬间,大量请求同时穿透到数据库。

防护措施

  1. 互斥锁:缓存重建时使用 Redis SETNX 加锁,只允许一个请求查库并重建缓存
  2. 逻辑过期:Hash 中存储逻辑过期时间,过期后异步更新,用户始终读到缓存(可能是旧数据)
  3. 永不过期 + 定时刷新:缓存不设 TTL,由定时任务负责更新计数

六、Bitmap 局限性与风险#

6.1 SETBIT 首次写入的性能陷阱#

风险:SETBIT 对不存在的 Key 执行时,时间复杂度为 O(offset),需从字节 0 到目标 offset 分配并零初始化整段内存。 影响:offset 为 21.4 亿(哈希取模期望值)时,首次写入分配 256MB 内存,耗时 25 秒,期间 Redis 单线程阻塞,该节点无法响应任何请求。后续操作因 Key 已存在而正常(O(1)),问题易被忽视。

offset 大小首次 SETBIT 分配内存耗时量级
100 万~125 KB毫秒级
1 亿~12 MB百毫秒级
10 亿~120 MB秒级
21.4 亿(期望值)~256 MB数秒
42.9 亿(最坏)~512 MB超时风险

应对:方案三(Bitmap + MQ)仅在用户 ID 为自增小整数(最大值 < 1 亿)时使用,此时首次分配仅 ~12MB,百毫秒级完成。雪花 ID 场景下 offset 过大,应选方案一/二(Set)。

6.2 雪花 ID 与 Bitmap 的根本矛盾#

风险:雪花 ID 为 64 位(值域 ~2^63),Bitmap offset 上限为 2^32(约 42.9 亿),差 20 亿倍。哈希取模 hash64(userId) % (2^32 - 1) 是唯一的桥接方式,但存在两个致命问题。 影响:

  • 取模后 offset 仍然很大:期望值约 21.4 亿,首次写入仍需分配 ~256MB
  • 取模引入碰撞:两个不同 userId 映射到同一 offset,导致 A 收藏后查询 B 的状态返回 true(误判),B 取消收藏把 A 的状态也清掉
Offset 上限 M单内容 1000 人操作碰撞率单内容 1 万人操作碰撞率首次分配内存
2^32 (42.9 亿)≈ 0.01%≈ 1.2%512 MB
2^28 (2.68 亿)≈ 0.19%≈ 17%32 MB
2^24 (1677 万)≈ 3%≈ 95%2 MB

缩小 Offset 到不卡的范围,碰撞率对热门内容不可接受;保持大范围,首次写入卡顿不可接受。性能与正确性无法兼得。

应对:雪花 ID 体系下直接放弃 Bitmap,选方案一/二(Set 存原始 ID,零碰撞)。

6.3 分布式场景下的阻塞与倾斜#

风险:Redis 单线程模型下,一个慢 SETBIT 会阻塞整个节点;爆款内容的所有操作打到同一个 Key(同一个节点),造成数据倾斜。 影响:

  • 单线程阻塞:一个 SETBIT key 2147483648 1 分配 256MB 期间,该节点上所有业务(登录、查询、计数)排队等待,客户端批量超时
  • 数据倾斜:爆款文章首次被大量用户收藏,所有 SETBIT 打到同一节点,该节点承受内存分配 + CPU + 网络压力,其他节点空闲
  • 大 Key 运维风险:单 Key 数百 MB,DEL 释放内存触发碎片整理卡顿,RDB/AOF 生成时间变长,主从全量同步传输耗时
问题Bitmap 方案Set 方案
单线程阻塞SETBIT 分配数百 MB 阻塞整个节点SADD O(1),毫秒级
数据倾斜爆款内容所有操作打同一个节点用户维度 Key 天然分散
大 Key 风险单 Key 数百 MB单用户 Key 通常几 KB

应对:使用 Hash Tag 机制(like:bit:1:{10086}stat:1:{10086})确保相关 Key 落在同一 slot;定时扫描方案下 Dirty Set 也需纳入同一 slot(dirty:like:{1})。但 Hash Tag 无法解决单线程阻塞和数据倾斜,根本应对是:雪花 ID 场景选方案一/二(Set),自增 ID 场景选方案三时需评估单 Key 的 offset 上限。

6.4 同步任务的幂等性#

风险:定时任务可能重复执行,MQ 消费可能重复消费。 影响:统计记录重复累加,用户状态记录重复插入。 应对:用户记录使用 INSERT ... ON DUPLICATE KEY UPDATE(依赖 4.1 节唯一索引);统计记录使用 user_id=0 标识 + 幂等覆盖更新。

6.5 数据丢失风险#

风险:Redis 故障或重启导致缓存数据丢失。 影响:未同步的数据丢失,计数可能不准确。 应对:开启 Redis RDB + AOF 混合持久化;缩短同步周期至 5 分钟;服务重启后从数据库预热缓存;MQ 方案下消息本身即为数据备份。


七、总结#

本方案基于 Redis 缓存实现交互数据的高性能读写,提供三种方案:

  1. 方案一(Set + MQ):通用方案,无 ID 限制,实现简单,适合大多数场景
  2. 方案二(Set + 定时扫描):无 MQ 依赖,Dirty Set 追踪变更,定时批量合并写入
  3. 方案三(Bitmap + MQ):极致内存(12MB vs 2GB),SETBIT 原子去重,需连续整数 ID

关键权衡

  • 选择最终一致性换取高性能,兜底机制保障数据最终正确
  • Bitmap 的内存优势需与 ID 范围限制权衡,ID 稀疏度 > 100 时应改用 Set
  • MQ 方案延迟低,定时扫描方案批量合并能力强,按业务规模和基础设施选择
  • 分布式环境下需通过 Hash Tag 确保 Key 同 slot

八、参考资料#

redis | 三种交互数据缓存设计方案 | 对比文档
https://tblog.mmzhiku.xyz/posts/projects/projects-redis-interaction-cache-design/
作者
MmzMing
发布于
2026-05-13
许可协议
CC BY-NC-SA 4.0

评论区

看板娘
公告
友链 互换友链

正在招募技术类博客友链,要求原创、稳定更新。点击了解更多。

查看详情
维护 服务器升级

本周日凌晨 2:00-4:00 进行服务器维护,期间站点可能短暂无法访问。

欢迎 关于我的介绍

欢迎来到我的博客,我是深耕java、python和react技术开发。热爱技术、持续学习,欢迎同好交流探讨,也欢迎大佬互换友链。

查看详情
音乐
封面

音乐

暂未播放

0:00
0:00
暂无歌词
标签
# AI 6 # 认证 5 # 安全 4 # 登录 3 # Skill 2 # Redis 2 # Bitmap 2 # 部署 2 # Java 2 # 并发编程 2 # 性能优化 2 # 前端 1 # 博客 1 # Prompt 1 # 工作流 1 # RAG 1 # Cloudflare 1 # 缓存设计 1 # 高性能 1 # Bot 1 # Umami 1 # Vercel 1 # 线程池 1 # 虚拟线程 1 # 分布式 1 # JWT 1 # OAuth2 1 # MinIO 1 # 文件存储 1 # 扫码登录 1 # WebSocket 1 # Agent 1 # Oracle 1 # 数据库 1
目录

隐私政策

更新日期: 2026/5/19
生效日期: 2026/5/19

导言#

MmzMing的知识库 是一款由 MmzMing(以下简称“我们”)提供的产品。您在使用我们的服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明,在使用我们的服务时,我们如何收集、使用、储存和分享这些信息,以及我们为您提供的访问、更新、控制和保护这些信息的方式。

本《隐私政策》与您所使用的 MmzMing的知识库 服务息息相关,希望您仔细阅读,在需要时,按照本《隐私政策》的指引,作出您认为适当的选择。本《隐私政策》中涉及的相关技术词汇,我们尽量以简明扼要的表述,并提供进一步说明的链接,以便您的理解。

您使用或继续使用我们的服务,即意味着同意我们按照本《隐私政策》收集、使用、储存和分享您的相关信息。

如对本《隐私政策》或相关事宜有任何问题,请通过 784774835@qq.com 与我们联系。

1. 我们收集的信息#

我们或我们的第三方合作伙伴提供服务时,可能会收集、储存和使用下列与您有关的信息。如果您不提供相关信息,可能无法注册成为我们的用户或无法享受我们提供的某些服务,或者无法达到相关服务拟达到的效果。

  • 个人信息:您在注册账户或使用我们的服务时,向我们提供的相关个人信息,例如电话号码、电子邮件等。
  • 日志信息:指您使用我们的服务时,系统可能通过 cookies、标识符及相关技术收集的信息,包括您的 设备信息浏览信息点击信息,并将该等信息储存为日志信息,为您提供个性化的用户体验、保障服务安全。您可以通过浏览器设置拒绝或管理 cookie、标识符或相关技术的使用。
  • 位置信息:指您开启设备定位功能并使用我们基于位置提供的相关服务时,收集的有关您位置的信息,包括:
    • 您通过具有定位功能的移动设备使用我们的服务时,通过 GPS 或 WiFi 等方式收集的您的地理位置信息;
    • 您可以通过关闭定位功能,停止对您的地理位置信息的收集。

2. 信息的存储#

2.1 信息存储的方式和期限#

我们会通过安全的方式存储您的信息,包括本地存储(例如利用 APP 进行数据缓存)、数据库和服务器日志。

一般情况下,我们只会在为实现服务目的所必需的时间内或法律法规规定的条件下存储您的个人信息。

2.2 信息存储的地域#

我们会按照法律法规规定,将境内收集的用户个人信息存储于中国境内。

目前我们不会跨境传输或存储您的个人信息。将来如需跨境传输或存储的,我们会向您告知信息出境的目的、接收方、安全保证措施和安全风险,并征得您的同意。

2.3 产品或服务停止运营时的通知#

当我们的产品或服务发生停止运营的情况时,我们将以推送通知、公告等形式通知您,并在合理期限内删除您的个人信息或进行匿名化处理,法律法规另有规定的除外。

3. 信息安全#

我们使用各种安全技术和程序,以防信息的丢失、不当使用、未经授权阅览或披露。例如,在某些服务中,我们将利用加密技术(例如 SSL)来保护您提供的个人信息。但请您理解,由于技术的限制以及可能存在的各种恶意手段,在互联网行业,即便竭尽所能加强安全措施,也不可能始终保证信息百分之百的安全。您需要了解,您接入我们的服务所用的系统和通讯网络,有可能因我们可控范围外的因素而出现问题。

4. 我们如何使用信息#

我们可能将在向您提供服务的过程之中所收集的信息用作下列用途:

  • 向您提供服务;
  • 在我们提供服务时,用于身份验证、客户服务、安全防范、诈骗监测、存档和备份用途,确保我们向您提供的产品和服务的安全性;
  • 帮助我们设计新服务,改善我们现有服务;
  • 使我们更加了解您如何接入和使用我们的服务,从而针对性地回应您的个性化需求,例如语言设定、位置设定、个性化的帮助服务和指示,或对您和其他用户作出其他方面的回应;
  • 向您提供与您更加相关的广告以替代普遍投放的广告;
  • 评估我们服务中的广告和其他促销及推广活动的效果,并加以改善;
  • 软件认证或管理软件升级;
  • 让您参与有关我们产品和服务的调查。

5. 信息共享#

目前,我们不会主动共享或转让您的个人信息至第三方,如存在其他共享或转让您的个人信息或您需要我们将您的个人信息共享或转让至第三方情形时,我们会直接或确认第三方征得您对上述行为的明示同意。

为了投放广告,评估、优化广告投放效果等目的,我们需要向广告主及其代理商等第三方合作伙伴共享您的部分数据,要求其严格遵守我们关于数据隐私保护的措施与要求,包括但不限于根据数据保护协议、承诺书及相关数据处理政策进行处理,避免识别出个人身份,保障隐私安全。

我们不会向合作伙伴分享可用于识别您个人身份的信息(例如您的姓名或电子邮件地址),除非您明确授权。

我们不会对外公开披露所收集的个人信息,如必须公开披露时,我们会向您告知此次公开披露的目的、披露信息的类型及可能涉及的敏感信息,并征得您的明示同意。

随着我们业务的持续发展,我们有可能进行合并、收购、资产转让等交易,我们将告知您相关情形,按照法律法规及不低于本《隐私政策》所要求的标准继续保护或要求新的控制者继续保护您的个人信息。

另外,根据相关法律法规及国家标准,以下情形中,我们可能会共享、转让、公开披露个人信息无需事先征得您的授权同意:

  • 与国家安全、国防安全直接相关的;
  • 与公共安全、公共卫生、重大公共利益直接相关的;
  • 犯罪侦查、起诉、审判和判决执行等直接相关的;
  • 出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
  • 个人信息主体自行向社会公众公开个人信息的;
  • 从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道。

6. 您的权利#

在您使用我们的服务期间,我们可能会视产品具体情况为您提供相应的操作设置,以便您可以查询、删除、更正或撤回您的相关个人信息,您可参考相应的具体指引进行操作。此外,我们还设置了投诉举报渠道,您的意见将会得到及时的处理。如果您无法通过上述途径和方式行使您的个人信息主体权利,您可以通过本《隐私政策》中提供的联系方式提出您的请求,我们会按照法律法规的规定予以反馈。

当您决定不再使用我们的产品或服务时,可以申请注销账户。注销账户后,除法律法规另有规定外,我们将删除或匿名化处理您的个人信息。

7. 变更#

我们可能适时修订本《隐私政策》的条款。当变更发生时,我们会在版本更新时向您提示新的《隐私政策》,并向您说明生效日期。请您仔细阅读变更后的《隐私政策》内容,若您继续使用我们的服务,即表示您同意我们按照更新后的《隐私政策》处理您的个人信息。

8. 未成年人保护#

我们鼓励父母或监护人指导未满十八岁的未成年人使用我们的服务。我们建议未成年人鼓励他们的父母或监护人阅读本《隐私政策》,并建议未成年人在提交的个人信息之前寻求父母或监护人的同意和指导。