用 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 管理员角色