> 技术文档 > 用 Spring Boot + Redis 实现哔哩哔哩弹幕系统(上篇博客改进版)_redis实现弹幕

用 Spring Boot + Redis 实现哔哩哔哩弹幕系统(上篇博客改进版)_redis实现弹幕



用 Spring Boot + Redis 实现哔哩哔哩弹幕系统

支持:历史弹幕 + 实时弹幕 + 敏感词过滤 + 限频 + 持久化


🧩 项目功能总览

功能模块 技术实现 🎞 历史弹幕 Redis List 存储,按时间排序展示 📡 实时弹幕 WebSocket 双向通信 + 广播 🚫 敏感词过滤 Redis Set 管理敏感词,系统提醒用户 🚦 弹幕防刷限频 Redis 键限速,每人 2 秒 1 条 📦 持久化存储 Redis 弹幕每 30 秒批量写入 MySQL 🧑‍💼 管理接口 敏感词添加/删除/查看 REST 接口

🧱 技术栈

层级 技术 说明 后端 Spring Boot 主体开发框架 通信 WebSocket 实时弹幕传输 缓存 Redis 弹幕缓存、限频控制 数据库 MySQL 弹幕历史存储 前端 HTML + JS 视频播放 + 弹幕显示

🗃️ 弹幕数据模型(MySQL)

CREATE TABLE danmu ( id BIGINT AUTO_INCREMENT PRIMARY KEY, video_id BIGINT NOT NULL, user_id VARCHAR(50), text VARCHAR(255), time_in_video DOUBLE, send_time DATETIME);

☁️ Redis 数据结构设计

Key 类型 示例值 danmu:video:{videoId} List 弹幕 JSON,按时间顺序 filter:words Set 管理敏感词 limit:user:{userId} String 限制用户发送频率

☁️ Redis 存弹幕(实时 + 历史)

  • 弹幕按 timeInVideo 入 Redis List
  • 前端加载 Redis 弹幕,根据视频播放进度展示
  • 每隔 30 秒自动将 Redis 弹幕落库并清除缓存

🔐 敏感词过滤系统(服务 + 接口)

🔧 Redis Filter Service

@Servicepublic class DanmuFilterService { @Autowired RedisTemplate<String, String> redis; public boolean containsForbidden(String text) { Set<String> words = redis.opsForSet().members(\"filter:words\"); return words != null && words.stream().anyMatch(text::contains); }}

🔧 管理接口

@RestController@RequestMapping(\"/api/filters\")public class FilterController { @Autowired RedisTemplate<String, String> redis; @PostMapping(\"/add\") public String add(@RequestParam String word) { redis.opsForSet().add(\"filter:words\", word); return \"添加成功\"; } @PostMapping(\"/remove\") public String remove(@RequestParam String word) { redis.opsForSet().remove(\"filter:words\", word); return \"删除成功\"; } @GetMapping(\"/list\") public Set<String> list() { return redis.opsForSet().members(\"filter:words\"); }}

🚦 弹幕限频控制

👮 Redis 限流器

@Servicepublic class DanmuRateLimitService { @Autowired RedisTemplate<String, String> redis; public boolean isTooFast(String userId) { String key = \"limit:user:\" + userId; if (redis.hasKey(key)) return true; redis.opsForValue().set(key, \"1\", Duration.ofSeconds(2)); return false; }}

🔄 定时将弹幕持久化到 MySQL

@Componentpublic class DanmuBackupTask { @Autowired RedisTemplate<String, String> redis; @Autowired DanmuRepository danmuRepo; Gson gson = new Gson(); @Scheduled(fixedRate = 30000) // 每 30 秒 public void flushToDb() { Set<String> keys = redis.keys(\"danmu:video:*\"); if (keys == null) return; for (String key : keys) { List<String> list = redis.opsForList().range(key, 0, -1); if (list == null || list.isEmpty()) continue; List<Danmu> danmus = list.stream().map(j -> gson.fromJson(j, Danmu.class)).toList(); danmuRepo.saveAll(danmus); redis.delete(key); // 清空 Redis } }}

📡 WebSocket 处理器(敏感词 + 限频 + 广播)

@ServerEndpoint(\"/ws/danmu/{videoId}/{userId}\")@Componentpublic class DanmuWebSocket { private static final Map<String, Session> sessions = new ConcurrentHashMap<>(); private static DanmuFilterService filterService; private static DanmuRateLimitService rateLimitService; private static RedisTemplate<String, String> redis; @Autowired public void setDeps(DanmuFilterService f, DanmuRateLimitService r, RedisTemplate<String, String> rt) { filterService = f; rateLimitService = r; redis = rt; } @OnOpen public void onOpen(Session session) { sessions.put(session.getId(), session); } @OnMessage public void onMessage(String msgJson, Session session, @PathParam(\"videoId\") String videoId, @PathParam(\"userId\") String userId) { Danmu danmu = new Gson().fromJson(msgJson, Danmu.class); danmu.setUserId(userId); danmu.setSendTime(LocalDateTime.now()); // 限频 if (rateLimitService.isTooFast(userId)) { sendTo(session, \"[系统通知] 请勿频繁发送弹幕!\"); return; } // 敏感词 if (filterService.containsForbidden(danmu.getText())) { sendTo(session, \"[系统通知] 弹幕含违禁词,已屏蔽!\"); return; } // 存 Redis redis.opsForList().rightPush(\"danmu:video:\" + videoId, new Gson().toJson(danmu)); // 广播 sessions.values().forEach(s -> sendTo(s, new Gson().toJson(danmu))); } private void sendTo(Session session, String msg) { try { session.getBasicRemote().sendText(msg); } catch (Exception e) {} } @OnClose public void onClose(Session session) { sessions.remove(session.getId()); }}

💻 前端弹幕逻辑(伪代码)

// 加载历史弹幕fetch(\"/api/danmu/history?videoId=123\") .then(res => res.json()) .then(data => { danmus = data.sort((a, b) => a.time - b.time); });setInterval(() => { const currentTime = video.currentTime; while (danmus.length && danmus[0].time <= currentTime) { showDanmu(danmus.shift().text); }}, 200);// 连接 WebSocketconst ws = new WebSocket(\"ws://localhost:8080/ws/danmu/123/userA\");ws.onmessage = e => showDanmu(JSON.parse(e.data).text);// 发送弹幕function sendDanmu(text) { ws.send(JSON.stringify({ text, time: video.currentTime }));}

✅ 最终效果

功能 效果 实时弹幕 多用户同步,实时显示 历史弹幕 视频播放自动同步 敏感词拦截 系统通知+拦截广播 防刷控制 每 2 秒最多 1 条 持久化保障 弹幕定时入库

🧪 当前系统存在的缺点分析

分类 问题描述 影响 改进建议 🏗 架构 WebSocket 逻辑中 Redis 和 Spring Bean 注入依赖手动静态赋值 不规范,难维护,容易出错 使用 @Component + @ServerEndpointExporter 或 Spring WebSocket(STOMP)替代 💾 数据存储 Redis 弹幕写入后一次性 flush 到 MySQL,每次清空缓存 如果任务挂掉,数据可能丢失 采用 MQ(如 Kafka)异步写库,或采用 AOF 持久化增强安全性 🧍‍♂️ 用户控制 弹幕限频基于 Redis 键,粒度较粗(用户级 2 秒) 不能支持每用户每视频限频、动态限速 改为 Lua 脚本实现限流(滑动窗口或令牌桶)更精准 🔎 敏感词检测 整体为“包含”检测,容易误伤、无法处理变形词 用户体验下降 + 容易绕过 支持正则、Trie 树、拼音转写等模糊检测方案 📋 管理后台 敏感词接口无权限保护,任意人可添加/删除 高危漏洞 使用 Spring Security + 登录鉴权系统 📈 弹幕密度 当前只支持“每秒多条弹幕”的简单展示方式 弹幕重叠、遮挡,影响观看 加入轨道(轨迹)管理:每条弹幕分配不重复轨道并添加动画队列 📺 前端展示 弹幕展示样式较简单,没有封装动画、颜色、字体大小 不够炫酷,体验不如 B 站 使用 canvas 或独立 JS 弹幕引擎如 danmaku.js 📶 多节点支持 当前广播使用内存 Map 保存所有 Session 无法扩展多实例部署 引入消息中间件(如 Redis Pub/Sub、Kafka)实现弹幕广播中转 💬 消息格式 弹幕是纯文本,缺乏弹幕类型(滚动/顶端/底端)、颜色等字段 无法实现个性化弹幕样式 扩展弹幕数据结构支持样式字段:如 { text, type, color, fontSize }

✅ 总结建议

优化方向 推荐技术 高可用架构 Spring WebSocket + Redis Pub/Sub + Kafka 数据安全 Redis AOF + MQ 异步写库 用户限频 Redis Lua 限流脚本(滑动窗口算法) 敏感词检测 DFA + 正则匹配 + 后台管理审查 前端动画 使用弹幕引擎库,如 danmaku.js / canvas 实现 安全控制 Spring Security + RBAC 管理员角色