音视频业务在互联网应用中属于典型的高并发、低延迟场景。以某短视频平台为例,高峰时段每秒需要处理数十万次视频上传请求和数百万次播放请求。这种场景下,传统的数据库读写模式会迅速成为性能瓶颈。
Spring Boot作为Java生态中最流行的微服务框架,其自动配置特性和starter机制能够快速构建可扩展的服务。但在音视频场景中,单纯依赖Spring Boot的默认配置往往难以满足需求。我们通常需要结合Redis实现以下核心功能:
Redis之所以成为首选,主要基于三个特性:1)内存操作带来的微秒级响应;2)丰富的数据结构支持;3)完善的持久化机制。比如用ZSET实现实时排行榜,单个操作时间复杂度仅为O(log(N)),比关系型数据库的ORDER BY效率高出数个数量级。
在视频详情页场景中,经典的三层缓存架构(本地缓存 → Redis → MySQL)可能遭遇缓存穿透问题。恶意请求不存在的videoId会导致大量请求直达数据库。我们在Spring Boot中通过两种方式防护:
java复制@PostConstruct
public void initBloomFilter() {
List<Long> allVideoIds = videoMapper.getAllIds();
allVideoIds.forEach(id -> redisTemplate.opsForValue()
.setBit("video_bloom", id % 100000, true));
}
java复制public VideoDetail getVideoDetail(Long videoId) {
// 先查布隆过滤器
if (!redisTemplate.opsForValue().getBit("video_bloom", videoId % 100000)) {
return null;
}
// 正常缓存查询流程
String cacheKey = "video:" + videoId;
VideoDetail detail = (VideoDetail)redisTemplate.opsForValue().get(cacheKey);
if (detail == null) {
detail = videoMapper.selectById(videoId);
// 即使为空也缓存5分钟
redisTemplate.opsForValue().set(cacheKey,
detail != null ? detail : new NullValue(),
5, TimeUnit.MINUTES);
}
return detail instanceof NullValue ? null : detail;
}
视频上传后的转码需要保证同一视频不会被多个worker重复处理。基于Redis的分布式锁实现要点:
java复制public boolean startTranscodeJob(Long videoId) {
String lockKey = "transcode_lock:" + videoId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 获取锁成功,执行转码逻辑
doTranscode(videoId);
return true;
}
} finally {
// 确保只释放自己的锁
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);
}
return false;
}
关键点:必须使用Lua脚本实现原子化的锁释放,避免因进程暂停导致锁被其他客户端误删
通过Redis的MONITOR命令或开源工具如Redis-Faina分析发现,视频播放量计数器(如video:123:views)这类热点Key在秒杀活动期间QPS可能突破10w+。解决方案:
java复制@Cacheable(value = "videoViews", key = "#videoId")
public Long getVideoViews(Long videoId) {
return redisTemplate.opsForValue().get("video:" + videoId + ":views");
}
@Scheduled(fixedRate = 5000)
public void batchUpdateViews() {
// 批量获取本地缓存中的计数器差值
Map<Long, Long> diffs = viewCounter.getAndResetDiffs();
// 使用pipeline批量更新Redis
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
diffs.forEach((videoId, diff) ->
connection.stringCommands().incrBy(
("video:" + videoId + ":views").getBytes(), diff));
return null;
});
}
video:123:views拆分为video:123:views:1~video:123:views:10,通过hash分散写入压力。音视频场景的Redis内存占用主要来自:
优化方案对比:
| 数据类型 | 原始内存 | 优化方案 | 节省效果 |
|---|---|---|---|
| String | 100MB | 使用Hash存储字段 | 减少40% |
| Set | 80MB | 当元素<1000时用int编码 | 减少60% |
| ZSET | 120MB | 调整zset-max-ziplist-entries | 减少35% |
配置示例:
properties复制# redis.conf
hash-max-ziplist-entries 512
zset-max-ziplist-entries 128
音视频场景下的持久化需要平衡性能和数据安全:
appendfsync everysec,在崩溃时最多丢失1秒数据save 300 100表示5分钟内100次修改则触发快照压测数据:纯AOF模式在视频转码任务队列场景下,写入性能比RDB模式低约15%
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 主从复制 | 配置简单 | 写性能无法扩展 | 读多写少 |
| Cluster | 自动分片 | 运维复杂 | 大数据量 |
| Codis | 支持水平扩展 | 代理层开销 | 快速迁移 |
对于全球部署的视频平台,推荐使用Redis Cluster+地域就近访问策略,延迟可控制在50ms内。
视频元数据更新时的双写一致性方案:
java复制@Transactional
public void updateVideoMeta(Long videoId, VideoMeta meta) {
// 1. 更新数据库
videoMapper.update(meta);
// 2. 删除缓存
redisTemplate.delete("video:" + videoId);
// 3. 发送binlog消息
kafkaTemplate.send("video_meta_update", videoId);
}
// 消费者处理binlog
@KafkaListener(topics = "video_meta_update")
public void handleUpdate(Long videoId) {
// 最终一致性保障:再次删除缓存
redisTemplate.delete("video:" + videoId);
}
错误示例:
java复制// 错误:在循环内执行pipeline
for (Long videoId : videoIds) {
redisTemplate.executePipelined(...);
}
正确做法:
java复制// 一次pipeline处理所有操作
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (Long videoId : videoIds) {
connection.stringCommands().incr(("video:" + videoId + ":views").getBytes());
}
return null;
});
性能对比:处理1000个计数器更新,错误用法耗时1200ms,正确用法仅需35ms。
某次线上事故:一个存储了50万粉丝列表的SET Key(占用800MB内存),在过期时导致Redis主线程阻塞13秒。解决方案:
删除大Key的推荐方式:
bash复制# 使用scan分批删除
redis-cli --scan --pattern large_key:* | xargs -L 1000 redis-cli del
典型配置问题:
properties复制# 错误配置(高并发时阻塞)
spring.redis.lettuce.pool.max-active=8
建议参数:
properties复制# 音视频场景推荐配置
spring.redis.lettuce.pool.max-active=500
spring.redis.lettuce.pool.max-idle=50
spring.redis.lettuce.pool.min-idle=10
spring.redis.lettuce.pool.max-wait=1000
监控指标:当active连接数持续超过max-active的70%时,需要考虑扩容或优化。