在当今互联网应用中,音视频处理已成为基础能力之一。不同于传统Web应用,音视频场景对系统提出了三个核心挑战:高并发处理能力、低延迟传输要求、海量数据存储需求。以某短视频平台为例,高峰期每秒需要处理超过10万次的视频上传请求,这对后端架构设计提出了极高要求。
Spring Boot作为Java生态中最流行的微服务框架,其优势在于快速开发能力和丰富的组件生态。但在音视频场景下,单纯的Spring Boot应用会遇到性能瓶颈。这时就需要引入Redis作为高性能缓存层,形成"Spring Boot+Redis"的黄金组合。Redis的单线程事件循环模型在处理高并发IO时表现出色,实测在普通服务器上可达10万QPS,完全满足音视频场景的吞吐量需求。
音视频场景下首要解决的是海量文件的唯一标识问题。传统的自增ID在分布式环境下会产生冲突,我们采用Snowflake算法结合Redis实现分布式ID生成:
java复制public class VideoIdGenerator {
private static final String REDIS_KEY = "video:id:generator";
@Autowired
private RedisTemplate<String, Long> redisTemplate;
public long generateId() {
long timestamp = System.currentTimeMillis();
long sequence = redisTemplate.opsForValue().increment(REDIS_KEY);
return (timestamp << 22) | (sequence & 0x3FFFFF);
}
}
注意:Redis的INCR命令具有原子性,可以保证在分布式环境下的序列号唯一性。时间戳部分建议使用41位,可支持69年的唯一ID生成。
热门视频的访问量往往呈现幂律分布,前1%的视频可能承担90%的流量。我们采用多级缓存策略:
java复制@Cacheable(value = "videos", key = "#videoId")
public Video getVideo(String videoId) {
// 先查本地缓存
Video video = localCache.get(videoId);
if (video != null) return video;
// 查Redis缓存
String redisKey = "video:" + videoId;
video = redisTemplate.opsForValue().get(redisKey);
if (video != null) {
localCache.put(videoId, video);
return video;
}
// 查数据库
video = videoRepository.findById(videoId).orElseThrow();
redisTemplate.opsForValue().set(redisKey, video, 1, TimeUnit.HOURS);
return video;
}
弹幕是音视频场景的典型实时交互功能,对延迟要求极高(<100ms)。我们采用Redis的Pub/Sub功能实现:
java复制@RestController
@RequestMapping("/danmu")
public class DanmuController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PostMapping
public void sendDanmu(@RequestBody Danmu danmu) {
String channel = "video:" + danmu.getVideoId();
redisTemplate.convertAndSend(channel, danmu.getContent());
}
@GetMapping("/subscribe/{videoId}")
public SseEmitter subscribe(@PathVariable String videoId) {
SseEmitter emitter = new SseEmitter();
String channel = "video:" + videoId;
// 使用Redis消息监听器
redisTemplate.getConnectionFactory().getConnection()
.subscribe((message, pattern) -> {
try {
emitter.send(message.toString());
} catch (IOException e) {
// 处理异常
}
}, channel.getBytes());
return emitter;
}
}
在处理视频元数据批量查询时,使用Redis管道可以显著减少网络往返时间:
java复制public List<VideoMeta> batchGetVideoMeta(List<String> videoIds) {
return redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (String videoId : videoIds) {
connection.get(("video:meta:" + videoId).getBytes());
}
return null;
});
}
实测表明,对于100条视频元数据的批量查询,管道技术可以将耗时从200ms降低到50ms左右。
在视频点赞计数场景下,我们需要保证计数操作的原子性:
lua复制-- 点赞脚本
local current = redis.call('GET', KEYS[1])
if not current then
redis.call('SET', KEYS[1], 1)
else
redis.call('INCR', KEYS[1])
end
在Spring Boot中调用:
java复制public void likeVideo(String videoId) {
String script = "local current = redis.call('GET', KEYS[1])...";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
redisTemplate.execute(redisScript, Collections.singletonList("video:like:" + videoId));
}
音视频场景下Redis内存使用需特别注意:
java复制// 使用Hash存储视频元数据
public void saveVideoMeta(String videoId, VideoMeta meta) {
redisTemplate.opsForHash().putAll("video:meta:" + videoId,
BeanUtil.beanToMap(meta));
}
音视频场景下建议采用混合持久化策略:
配置示例:
code复制save 3600 1
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
java复制int expireTime = 3600 + new Random().nextInt(600); // 1小时±10分钟
使用Redis自带的bigkeys命令找出大Key:
code复制redis-cli --bigkeys
解决方案:
关键监控指标包括:
Spring Boot集成Prometheus监控示例:
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "video-service",
"region", System.getenv("REGION")
);
}
设置慢查询阈值(单位微秒):
code复制slowlog-log-slower-than 10000
分析慢查询日志:
code复制redis-cli slowlog get 10
常见慢查询优化:
对于日活千万级的音视频应用,建议:
配置示例:
code复制cluster-enabled yes
cluster-node-timeout 15000
cluster-require-full-coverage no
某短视频平台在发展初期使用纯MySQL架构,在用户量突破百万后遇到严重性能瓶颈。通过引入Redis实现三级缓存架构后:
关键优化点:
java复制// 使用Bitmap实现去重
public boolean isViewed(String userId, String videoId) {
String key = "user:view:" + userId;
return redisTemplate.opsForValue().getBit(key, hash(videoId));
}
public void markViewed(String userId, String videoId) {
String key = "user:view:" + userId;
redisTemplate.opsForValue().setBit(key, hash(videoId), true);
}
建议使用Docker快速搭建Redis开发环境:
bash复制docker run -p 6379:6379 --name redis -d redis:6.2-alpine
Spring Boot连接配置:
properties复制spring.redis.host=localhost
spring.redis.port=6379
spring.redis.timeout=3000
使用JMeter模拟高并发场景:
根据业务量估算Redis内存需求:
示例计算:
code复制预计日活用户100万 → 会话数据约500MB
日均视频播放量1000万 → 热点视频缓存约10GB
总内存需求 = (500MB + 10GB) * 1.3 ≈ 14GB
推荐部署方案:
哨兵配置示例:
code复制sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
备份脚本示例:
bash复制redis-cli save
scp /var/lib/redis/dump.rdb backup-server:/backups/redis-$(date +%Y%m%d).rdb
配置示例:
code复制io-threads 4
io-threads-do-reads yes
对于嵌套的视频元数据,可以使用RedisJSON模块:
java复制JSON.set video:123 . '{"title":"Spring Boot教程","duration":3600}'
JSON.get video:123 .title
K8s部署示例:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 3
template:
spec:
containers:
- name: redis
image: redis:6.2
ports:
- containerPort: 6379
resources:
requests:
memory: "4Gi"
cpu: "2"
在实际开发中,有几个容易忽视但非常重要的细节:
properties复制spring.redis.lettuce.pool.max-active=200
spring.redis.lettuce.pool.max-wait=1000
java复制@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
--hotkeys参数定期扫描:bash复制redis-cli --hotkeys
java复制public Object getWithAutoDegrade(String key) {
// 先尝试从Redis获取
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
// 更新最近访问时间
redisTemplate.opsForZSet().add("key:access:time", key, System.currentTimeMillis());
return value;
}
// Redis没有则查数据库
value = database.get(key);
if (value != null) {
// 根据访问频率决定是否缓存
Double lastAccess = redisTemplate.opsForZSet().score("key:access:time", key);
long accessCount = redisTemplate.opsForZSet().count("key:access:time",
System.currentTimeMillis() - 86400000, System.currentTimeMillis());
if (lastAccess != null && accessCount > 10) {
// 热点数据缓存1天
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.DAYS);
} else {
// 冷数据缓存10分钟
redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
}
}
return value;
}
这个方案在我们的生产环境中实现了Redis内存使用减少40%,同时保证了热点数据的访问性能。关键在于:
对于Java面试准备,建议重点掌握:
最后分享一个排查Redis内存泄漏的真实案例:某次上线后Redis内存持续增长,通过分析RDB文件发现是某个功能将未压缩的大视频ID列表存入Redis,改用分页查询和压缩存储后,内存使用立即下降了60%。这个经历让我深刻体会到:在音视频场景下,对Redis的每个写入操作都要慎重考虑数据规模和存储格式。