如果你在视频点播系统里做过缓存这一层,你大概率会和我有同感:业务上线第一个月,缓存裸奔问题不大,等到热播剧上线、晚间黄金档并发冲上来,Redis 才开始真正决定系统能不能活下来。Redis-SDK 不是新概念,它就是一个把连接管理、序列化、命令封装、监控埋点集中起来的客户端库,但视频点播场景里,它要解决的问题比一般后台系统多得多——短时峰值高、读多写少、单 key 热点非常集中、转码异步链路长,稍不留神,缓存层就变成整个系统的瓶颈。

这篇文章我打算把 Redis-SDK 在视频点播业务里的真实用法从头到尾盘一遍,包括选型逻辑、封装设计、核心场景代码、高并发防护和线上事故复盘。不管你是刚接手点播系统的新人,还是已经在做缓存治理的后端开发,应该都能从中找到可以直接落地的思路。

1. 视频点播业务里Redis-SDK到底在解什么题

很多文章一上来就讲 JEDIS、Lettuce、Redisson 的对比,但我觉得先别急,先搞清楚视频点播系统里 Redis 到底承担了什么角色,否则选 SDK 时容易跑偏。

1.1 一次播放请求走了哪些“冤枉路”

用户在播放器里点“播放”,后端要做的事远不止把 MP4 地址丢给客户端。

  • 校验用户登录态:从 session 或 token 里还原用户信息。
  • 校验播放权限:这个用户有没有购买这部片?子账号有没有被主账号限制设备数?
  • 确定清晰度列表:根据用户等级返回 720P、1080P 还是 4K 地址。
  • 拼接鉴权 URL:CDN 防盗链参数有时效性,典型的过期时间是 30 到 120 秒。
  • 记录播放行为:点播次数、完播率、热门榜分数至少要异步更新。
  • 查询转码状态:用户上传的视频可能还在处理中,需要轮询进度。

这些步骤如果全部落到数据库或者第三方鉴权接口,不用多,几百路并发就能把上游拖垮。加了 Redis-SDK 之后,每次请求先查缓存,缓存命中就直接短路,只有 miss 才回源查库,最后把结果写回缓存。这里的“结果”不只是字符串,还有对象结构、过期时间、空值标记,都需要 SDK 层统一处理。

1.2 先弄清楚:你要的是云厂商SDK,还是业务自定义SDK

Redis-SDK 这个词在不同语境下有两层含义。第一层是云厂商提供的 Redis 实例访问 SDK,通常包含连接池、监控上报、透明故障切换。第二层是业务团队自己封装的客户端工具库,底层可能基于 Lettuce 或 JEDIS,但对外只暴露 getPlayUrl(userId, videoId) 、 incrPlayCount(videoId) 这类业务方法。

我的建议是:两层都要,但重点放在第二层。底层客户端决定了单连接性能、线程模型和故障恢复能力,上层封装则决定了业务代码有没有被缓存细节污染。视频点播系统里不同模块做的事情差异太大,鉴权缓存要求秒级过期,榜单服务要求原子自增,转码锁要求分布式锁语义,如果每个模块直接裸用底层客户端,代码里会到处散落 redisTemplate.opsForValue() ,以后想加监控埋点、想换序列化方案,工作量会让你怀疑人生。

1.3 SDK的边界:不能让它变成另一种“数据库客户端”

封装 Redis-SDK 最忌讳的是把缓存当作唯一数据源,把所有业务状态都塞进去。我看到过一种设计:播放进度、用户设置、支付订单全部放 Redis,美其名曰“快”,结果缓存一旦抖动,整个业务不可用,数据恢复还无从下手。

视频点播里 Redis 的定位是 加速层和协调层 ,不是持久化主存储。登录态可以放缓存,因为丢了用户可以重新登录;播放凭证必须允许从 DB 回源重建;转码状态需要和任务表保持一致;播放计数允许阶段性丢失,定期刷回数据库。所以封装 SDK 的时候,每个方法要回答三个问题:缓存 miss 了怎么办?Redis 异常了怎么办?数据写回底层数据库的路径在哪里?这些问题在设计阶段想清楚,后面运维会轻松很多。

2. 从零封装一个能扛住播放高峰的Redis客户端库

我现在的做法是维护一个单独的基础库,不叫“RedisUtil”,也不叫“CacheHelper”,而是按业务域拆成几个 client: AuthCacheClient 、 VodMetaCacheClient 、 TaskLockClient 。这样每个模块只依赖自己需要的部分,也方便做独立降级。

2.1 依赖选型和最小初始化配置

Java 技术栈里,主流底层客户端三个:JEDIS、Lettuce、Redisson。JEDIS 简单直接,但线程模型偏老,高并发下连接池容易成为瓶颈;Lettuce 基于 Netty,连接共享,性能更好,Spring Boot 默认集成;Redisson 是“带业务语义的客户端”,分布式锁、延迟队列、限流器都有现成实现。视频点播场景我建议底层用 Lettuce,分布式锁部分单独引 Redisson,或者自己基于 Lettuce 实现简单锁。这一点后面会展开。

Maven 依赖大概长这样:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

spring-boot-starter-data-redis 已经内置 Lettuce,不需要额外引 Redis 客户端。再手动引入连接池组件,是因为生产环境下不带连接池的 Lettuce 在突发流量下容易把连接数打爆。

最小 application.yml 配置我一般这样写:

spring:
  data:
    redis:
      host: 10.0.0.12
      port: 6379
      password: ${REDIS_PASSWORD}
      timeout: 2s
      lettuce:
        pool:
          max-active: 64
          max-idle: 16
          min-idle: 8
          max-wait: 1s
      connect-timeout: 2s

几个容易被忽略的点:

  • timeout 和 connect-timeout 不是一回事。前者是命令读写超时,后者是建连超时。视频点播高峰期如果某个 bigkey 导致命令阻塞,读写超时设置太大会让调用线程全部卡住,建议读超时 1 到 2 秒。
  • max-active 不要拍脑袋填 200。对于 Lettuce,连接是共享的,不需要太多;对于 JEDIS,这个值基本等于线程池上限,调太大反而会积累大量等待。
  • 密码不要写在配置文件里,用环境变量注入。

2.2 缓存模型设计:播放凭证、转码状态、用户会话各用什么结构

这是容易被低估的一步。缓存结构选错,后面数据维护成本特别高。

播放凭证 :短时效、高并发读、每位用户每个视频独立。我采用 SET key value EX 90 风格的短过期键。key 的格式是 vod:play:grant:{userId}:{videoId} ,value 是 JSON 串,里面带 CDN 鉴权地址、过期时间、清晰度列表。90 秒过期是为了对齐 CDN 鉴权 URL 的有效期,用户连续拖动进度条时,不需要每次重新拼接签名。

转码状态 :用户上传视频后,转码任务要经历 pending、running、success、failed。这类状态要求及时可见,但数据量不大。用 Hash 结构,field 是任务 ID,value 是状态码,针对每个用户有一个总进度 key。为什么不用 String?因为需要一次性获取某个用户所有任务的进度,Hash 一个 hgetall 就能搞定,String 就得循环 mget,key 数量一多网络往返也上来了。

用户会话 :登录 token 到用户信息的映射,频繁读,但可以容忍少量脏数据。用 String 存 JSON 就行,TTL 设 30 分钟,每次访问续期。注意续期不能是“每次请求都 expire ”,那个会对 Redis 造成大量写请求,特别是视频播放器每秒上报进度的时候,会把 session 续期变成巨大的写放大。正确做法是 TTL 到一半的时候才主动续期,或者在播放上报接口里单独用一个异步任务批量续期。

播放计数与榜单 :用 ZSet 。分数是播放量,member 是视频 ID。用户每播放一次就 zincrby ,排行榜直接 zrevrange 取前 N 名,天然带排序,还支持按时间窗口做日榜、周榜。这个结构在视频点播里几乎是万能解。

2.3 键命名规范和序列化器的选择

键命名看起来是小问题,实际是线上运维的大问题。视频点播业务涉及多端、多租户、多地域,如果键名随意,查问题的时候连“这个 key 是谁写的”都看不出来。

我的规范是 业务域:子域:对象类型:标识符[:额外维度] ,例如:

  • vod:meta:video:12345 —— 视频元信息
  • vod:play:grant:u123:v456 —— 用户 123 播放视频 456 的凭证
  • vod:transcode:task:789 —— 转码任务详情
  • vod:rank:hot:daily:20250618 —— 日榜

冒号分段不只是为了好看,Redis 官方虽然不建议键名太长,但短了又容易冲突。按业务域隔离还有个好处:后续做 key 前缀路由、分集群迁移、按模块淘汰缓存,规则会非常清晰。

序列化器是新手最容易踩的坑。Spring Data Redis 默认用 JDK 序列化,存入 Redis 的值是一堆 \xAC\xED\x00\x05t... ,在命令行里根本看不出来是什么,而且占空间大。我在代码里统一把 key 和 value 都指定为 StringRedisSerializer,对象值先转 JSON,再交给 SDK 存储。这样既方便排查,也方便其他语言的服务直接读同一个 key。

@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, String> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(new StringRedisSerializer());
    return template;
}

3. 核心场景代码走读:鉴权、榜单、转码锁

封装完成之后,真正的业务代码其实非常简单。我挑三个视频点播系统里最高频的场景,讲一下具体实现和背后的思考。

3.1 播放鉴权缓存:把DB和第三方鉴权接口请求量打下来

播放鉴权是整个系统里最怕慢的环节。用户点了播放,如果鉴权要 300 毫秒,播放器就开始转圈,用户早就退出页面了。

实现思路是两级缓存:第一级是本地 Caffeine 缓存,过期时间极短;第二级是 Redis。本地缓存扛住同一个用户反复请求的热点,Redis 扛住多实例共享缓存。两级都 miss 时才回源查数据库和第三方鉴权接口。

public PlayGrant getPlayGrant(String userId, String videoId) {
    String key = "vod:play:grant:" + userId + ":" + videoId;
    // 本地缓存优先
    PlayGrant grant = localCache.getIfPresent(key);
    if (grant != null) {
        return grant;
    }
    // Redis 缓存
    String json = redisTemplate.opsForValue().get(key);
    if (json != null) {
        grant = JSON.parseObject(json, PlayGrant.class);
        localCache.put(key, grant, 3, TimeUnit.SECONDS);
        return grant;
    }
    // 回源
    grant = playAuthService.buildPlayGrant(userId, videoId);
    if (grant == null) {
        // 空值也缓存,防止恶意刷不存在的内容
        redisTemplate.opsForValue().set(key, "", 30, TimeUnit.SECONDS);
        return null;
    }
    redisTemplate.opsForValue().set(key, JSON.toJSONString(grant), 90, TimeUnit.SECONDS);
    localCache.put(key, grant, 3, TimeUnit.SECONDS);
    return grant;
}

代码里的几个细节看着小,实战价值很高:

  • miss 回源后一定加锁。多个线程同时查数据库时,使用分布式锁或 JVM 锁保护回源逻辑,避免“缓存击穿”。最简单的办法是用 Redis 的 setnx 做一个短锁,只有抢到锁的线程回源,其他线程短暂 sleep 后重查缓存。
  • 空值缓存必须加短 TTL。有人觉得空值没必要缓存,但在视频点播里,用户会反复点击一个已经下架的视频,不加空值缓存,每次都要穿透到数据库。
  • 本地缓存 TTL 要远小于 Redis TTL,否则 Redis 那边手动清了缓存,本地还在用旧数据。

3.2 播放计数与热门榜单:一条ZSet链路解决读写冲突

视频点播的播放计数和榜单,对一致性的要求其实没那么高,允许几秒甚至几分钟的误差。但它有一个硬指标:写入必须极快,不能因为播放行为导致用户感知延迟。

用 ZSet 的 zincrby 是最顺手的方案。

public void recordPlay(long videoId, int count) {
    String todayKey = "vod:rank:hot:daily:" + LocalDate.now().format(BASIC_ISO_DATE);
    String totalKey = "vod:rank:hot:total";
    redisTemplate.opsForZSet().incrementScore(todayKey, String.valueOf(videoId), count);
    redisTemplate.opsForZSet().incrementScore(totalKey, String.valueOf(videoId), count);
}

这里有两个坑要提醒。

第一个坑是写入频率。播放器如果每播放 10 秒上报一次进度,那就不是“一次播放一次计数”,而是 N 次上报 N 次自增。必须在 SDK 层做聚合,用本地内存累计,每隔 5 秒批量上报一次。这既减少了 Redis 写请求,也让计数更平滑。

第二个坑是榜单 key 的过期。每日榜单如果只写了当天 key,不设过期时间,日积月累 Redis 里全是废弃 key。正确做法是每天凌晨任务创建第二天的 key,同时对 3 天前的 key 执行 expire 或者直接 del ,这个逻辑可以放在定时任务里,不要指望 Redis 自动清理。

// 每日凌晨任务
String yesterdayKey = "vod:rank:hot:daily:" + LocalDate.now().minusDays(1).format(BASIC_ISO_DATE);
redisTemplate.expire(yesterdayKey, 7, TimeUnit.DAYS);

保留 7 天的日榜是为了给运营做活动复盘,超过 7 天直接淘汰。

3.3 转码任务消费用分布式锁,怎么防止重复处理

转码是异步链路,上传服务把任务丢到消息队列,转码服务消费后开始处理。消费者一旦超时或者 GC 停顿,同一个任务可能被两个实例同时消费,转码服务重复执行不但浪费算力,还可能产出状态错乱的文件。

分布式锁在这个场景里是刚需。我不建议直接用 SETNX + expire 两步,因为存在设置锁之后、设置过期时间之前进程崩溃,锁就变成永不过时的死锁。推荐用 Redisson 的 RLock ,或者自己写一个 Lua 脚本原子完成。

public boolean tryLock(String taskId, int seconds) {
    String lockKey = "vod:transcode:lock:" + taskId;
    String requestId = UUID.randomUUID().toString();
    String script = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 else return 0 end";
    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        Collections.singletonList(lockKey),
        requestId, String.valueOf(seconds)
    );
    return result != null && result == 1L;
}

几乎所有现场分享我都会强调一点:拿到锁只是开始,释放锁才是最容易出问题的地方。释放锁必须校验 requestId,防止自己拿的锁被别人的线程释放。很多人写代码时直接 del lockKey ,在多实例场景下,如果第一个线程执行业务超过锁的过期时间,锁被 Redis 自动释放,第二个线程拿到锁开始执行,此时第一个线程执行完毕调用 del ,就把第二个线程的锁删了。一旦删除,第三个线程乘虚而入。这个问题的解法是释放锁时保证操作原子,且只能删除 value 与自己保存的 requestId 一致的锁。

public void releaseLock(String taskId, String requestId) {
    String lockKey = "vod:transcode:lock:" + taskId;
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        Collections.singletonList(lockKey),
        requestId
    );
}

顺带说一句,转码锁的过期时间不能设得太短。一个 4K 视频转码可能要 20 分钟,锁过期时间只有 60 秒的话,锁会频繁失效,反而引发重复消费。建议过期时间设为任务预期最大耗时的 3 到 5 倍,并配合看门狗续期机制。

4. 高并发下最见真章:穿透、击穿、雪崩与热Key

短视频和长视频平台每到晚上 8 点到 11 点,流量就是白天的十倍。这个时段最能检验缓存层设计。

4.1 一次线上事故:热Key如何打垮了整个缓存集群

有一次大促活动,运营把一部经典老片重新上架首页推荐,结果这个视频的播放凭证缓存 key 在短短几分钟内被疯狂请求。单 key 访问量直接飙到每秒十几万次,而 Redis 集群中这个 key 固定落在一个分片上,所有请求都打到同一台 Redis 节点,CPU 先被打满,然后客户端连接超时,紧接着熔断,最后整个视频列表接口都挂了。

复盘时我们发现问题不在 Redis 实例本身,而在于没有对热 key 做拆分。一个 key 再热,它也只能在集群的一个节点上承担压力,集群扩容解决不了这个问题。使用 Redis 集群时,你必须意识到: 热 key 是一台机器的问题,不是整个集群的问题。

解决思路是给 key 加随机后缀,把一个 key 变成 N 个 key 分担压力。

public String getPlayGrantWithSharding(String userId, String videoId) {
    int shardNum = 16;
    int shard = ThreadLocalRandom.current().nextInt(shardNum);
    String key = "vod:play:grant:" + userId + ":" + videoId + ":shard:" + shard;
    String json = redisTemplate.opsForValue().get(key);
    if (json != null) {
        return JSON.parseObject(json, VideoGrant.class);
    }
    // miss 后回源,并写入所有 shard,确保后续流量均匀
    VideoGrant grant = loadFromDB(userId, videoId);
    for (int i = 0; i < shardNum; i++) {
        String k = "vod:play:grant:" + userId + ":" + videoId + ":shard:" + i;
        redisTemplate.opsForValue().set(k, JSON.toJSONString(grant), 90, TimeUnit.SECONDS);
    }
    return grant;
}

热 key 拆分要掌握一个度:拆分的目的是把压力分散到多台节点,但如果拆出来的 key 数量太多,每次 miss 回源时写 N 个 key 的写放大又成了新问题。我用下来觉得 8 到 16 个分片是比较合理的区间。

4.2 三层防护:空值缓存、布隆过滤器、本地缓存

Redis 穿透是指请求的 key 在缓存中不存在,每次都要到数据库查询。视频点播里典型的穿透场景是攻击者不断请求不存在的视频 ID,这些请求全部放行到数据库,数据库连接池瞬间被打满。

我常用的三层防护:

  • 第一层:布隆过滤器拦截不存在的 ID。视频 ID 总数量级在百万到千万,布隆过滤器可以做到“一定不存在的直接拒绝”,虽然可能有误判,但误判只是多一次 DB 回源,影响可控。
  • 第二层:Redis 空值缓存。布隆过滤器只覆盖 ID 合法性,像“视频存在但用户无权限”这种情况,布隆过滤器挡不住,必须在 Redis 里缓存空结果。
  • 第三层:本地缓存。这层主要解决误判导致的重复穿透。

本地缓存我用 Caffeine,配置最大条目数和写入后过期时间,代价是每个实例存一份,内存占用可控。三层配合后,回源率能压到 1% 以下。

4.3 热Key应该“拆”而不是“扛”

遇到热 key,很多新手第一反应是加缓存时间,把 TTL 从 90 秒调到 10 分钟。这其实是饮鸩止渴。TTL 变长,缓存命中率确实上去了,但热点数据一旦变化(比如 CDN 签名过期),所有客户端会在同一时刻集中回源,把刚才的“热”变成一个更大的“刺”。

正确的热 key 治理逻辑是分层:

  • 本地缓存(毫秒级)
  • Redis 分片 key(秒级)
  • DB 兜底(保证最终一致)

本地缓存解决的是单个实例上的重复请求,Redis 分片解决的是跨实例共享和集群节点压力。而且本地缓存这块要设计成“主动失效”还是“短 TTL 被动失效”,我的经验是短 TTL 更简单,视频点播数据对一致性要求没那么高,本地 1 到 3 秒的过期时间完全够用。

5. 接入之后别急着上线:监控、压测、参数调优

SDK 写完、业务接入完,不意味着工作结束了。没有监控的缓存层就像一个没有仪表盘的飞机,你根本不知道它什么时候会出问题。

5.1 公共SDK必须上报的八个指标位

封装 Redis-SDK 的时候,我会强制每个方法经过一个统一的拦截器,自动记录指标。这里我列一下视频点播系统里必须有的指标:

指标 单位 说明
命令QPS 次/秒 区分读和写
平均耗时 毫秒 区分 P50、P95、P99
连接池活跃连接数 个 判断连接池是否打满
连接池等待次数 次 超时等待的请求量
缓存命中率 % 按业务模块分别统计
回源QPS 次/秒 落到 DB 的请求量
key过期淘汰量 个/秒 判断容量是否吃紧
bigkey数量 个 定时扫描结果

指标不要只写在日志里,要暴露给 Prometheus 这类监控系统。我见过太多团队,Redis 出问题时连“是不是缓存击穿”都判断不了,就是因为没有回源 QPS 这个指标。

5.2 用redis-benchmark和业务脚本做压测,场景怎么设计

通用压测工具 redis-benchmark 只能测 Redis 本身的极限性能,测不出业务场景下的真实瓶颈。我的做法是双轨压测。

第一轨:用 redis-benchmark 打基础数据。

redis-benchmark -h 10.0.0.12 -p 6379 -c 50 -n 100000 -t set,get -d 256

-c 50 表示 50 个并发连接, -n 100000 表示每个命令执行十万次, -d 256 表示 value 大小 256 字节。这组参数能看出 Redis 实例单机吞吐大概是什么水平,通常一台 8 核 16G 的节点,GET 可以跑到 10 万 QPS 以上,SET 略低。

第二轨:用业务脚本模拟真实播放请求。压测样例代码里,必须设置以下参数:

  • 热 key 占比:比如 50% 的流量集中在 10 个视频上。
  • key 数量:百万级别,考察 bigkey 对内存和网络的影响。
  • 过期时间分布:大量 key 集中在整点过期时,容易形成缓存雪崩,压测时让 TTL 增加随机扰动。

这个第二轨压测不图测出极限值,而是要找出业务代码中回源路径、锁竞争、序列化开销这几个容易被忽略的隐藏点。

5.3 连接池参数、超时时间与重试策略的调优顺序

调优不要凭感觉,更不要照搬网上的配置。我的经验是按这个顺序来:

  1. 先定超时时间。网络超时、读写超时分别设多少,取决于你的 Redis 集群延迟。同机房一般读写 1 秒,跨机房建议 2 到 3 秒。
  2. 再定重试策略。读写超时后要不要重试,这是很关键的选择。视频点播的播放鉴权接口不幂等,超时后重试可能会给下游造成重复请求;查询类的可以重试,但重试次数最多 2 次。
  3. 最后调连接池。观察监控里的活跃连接数和等待次数,如果 max-active 64 够用,不要为了“更快”调成 256。连接数太多,Redis 单线程处理命令的模型下,反而会增加上下文切换开销。

6. 这些坑,我都是拿线上事故换回来的

最后分享几个典型的坑,都是我在视频点播项目里真实踩过的,写出来希望你能绕开。

6.1 连接池不是越大越好

上线初期,我们把 Lettuce 的 max-active 调到 256,因为觉得并发高就得多开连接。结果某个运营活动刚上,Redis 节点 CPU 冲到 100%,但命令耗时反而上升。原因很简单,Redis 是单线程模型,连接太多意味着频繁的上下文切换和 fd 竞争。

后来把 max-active 调回 64,P99 耗时从 25ms 降到 8ms。这个教训我一直记着:线程池、连接池的参数,在单线程服务端模型下,真不是越大越好。

6.2 管道用不好,反而拖垮Redis

为了减少网络 RTT,很多人在 SDK 里大量使用 Pipeline,把一堆命令打包一次性发给 Redis。Pipeline 本身没问题,但有个前提: 一次 Pipeline 的命令数量不能无限大。

有一次我们批量回写播放计数,每次把 10 万个 INCR 塞进一个 Pipeline,结果 Redis 节点内存飙升、命令阻塞,直接影响了其他业务。原因就是 Pipeline 过大时,Redis 需要先缓冲全部命令,内存负载瞬间提高。

现在的做法是控制每个 Pipeline 批量大小,比如一次最多 500 个命令,分批次发送。如果需要操作不同类型的 key,优先使用 Lua 脚本,因为它能在服务端原子执行,减少多次网络往返。

6.3 序列化前后不一致,数据“越写越脏”

有段时间我们发现 Redis 里的用户会话经常解析失败,排查到最后,发现是一个历史模块还在用 JDK 序列化写入,新模块用 JSON 读取。两个序列化方案混用,写出来的是二进制乱码,读出来自然失败。

最终我们在 SDK 层统一强制 String 序列化,同时加了一个启动检查,扫描 Redis 里是否存在非 String 类型的 key,发现就告警。如果你正在维护老系统,建议先统计存量 key 的序列化格式,再统一切换,避免上线当天数据全部无法读取。

6.4 故障转移比想象中复杂,主从切换后的参数要验证

Redis 哨兵和集群都能做主从切换,但切换后你会面临一个困惑:新主节点上没有旧主节点的缓存数据,所有缓存瞬间 miss,大量请求回源数据库。

如果缓存 TTL 没有加随机值,集中过期加上主从切换,两件事叠加,就会形成雪崩。我们的解决思路是两件事同时做:一是给每个 key 的 TTL 加上随机偏移,避免同时过期;二是主从切换后启动一个“缓存预热任务”,提前把热点视频的播放凭证、榜单数据写回新主节点。

这套逻辑需要在 SDK 层面配合:封装缓存时把“查询-回源-写回”做成一个模板方法,方便预热任务直接复用,不至于每个模块各写一套预热逻辑。

视频点播系统的缓存治理,三分靠 Redis 本身,七分靠 SDK 设计和业务配合。把连接管理、序列化、热 key 拆分、空值缓存、锁竞争这些问题在 SDK 层统一收敛,业务团队只需要关心自己的数据模型和过期策略,遇到突发流量时才不会手忙脚乱。希望这篇文章里的实践细节能给你一些参考,少在缓存这个环节上踩几个坑。

Logo

邀请您加入社区

更多推荐