用 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
@Service
public 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 限流器
@Service
public 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
@Component
public class DanmuBackupTask { @Autowired RedisTemplate < String , String > redis; @Autowired DanmuRepository danmuRepo; Gson gson = new Gson ( ) ; @Scheduled ( fixedRate = 30000 ) 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) ; } }
}
📡 WebSocket 处理器(敏感词 + 限频 + 广播)
@ServerEndpoint ( "/ws/danmu/{videoId}/{userId}" )
@Component
public 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. 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 ) ;
const 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 管理员角色