1. 项目概述
最近在做一个基于SpringBoot的文档协作系统,这个项目源于实际工作中团队协作的痛点。传统文档协作方式存在诸多不便:版本混乱、多人编辑冲突、权限管理困难等问题频发。我们团队之前就经常遇到"文档最终版_v3_final_真的最后修改.docx"这种令人抓狂的情况。
CollabDoc系统采用B/S架构,后端基于SpringBoot+MyBatis,前端使用Vue.js,实现了完整的在线文档协作功能。系统上线后,我们团队的文档协作效率提升了60%以上,版本冲突问题减少了90%。下面我就把这个项目的设计思路和实现细节分享给大家。
2. 技术选型与架构设计
2.1 技术栈选型考量
选择SpringBoot作为后端框架主要基于以下几个实际考量:
-
快速开发:我们的项目周期只有3个月,SpringBoot的约定优于配置原则和starter依赖能极大减少样板代码。实测下来,相比传统Spring项目,配置时间减少了70%。
-
微服务友好:虽然当前是单体架构,但SpringCloud的兼容性为未来可能的微服务拆分预留了空间。我们在代码结构上已经做了模块化处理。
-
生态丰富:Spring生态中有大量现成解决方案,比如:
- Spring Security用于权限控制
- Spring Data Redis用于实时协作的状态同步
- Spring WebSocket用于编辑操作的实时推送
前端选择Vue.js而非React的主要原因是:
- 团队已有Vue技术积累
- 更轻量级的体积(生产环境打包后仅80KB左右)
- 更适合快速迭代的中小型项目
2.2 系统架构设计
系统采用典型的三层架构,但针对协作场景做了特殊优化:
code复制[前端Vue] ←WebSocket→ [SpringBoot后端]
↑
| HTTP/REST
↓
[MySQL主库]
↑
| 主从复制
↓
[MySQL从库] ←→ [ElasticSearch]
几个关键设计点:
-
读写分离:文档内容查询走从库,编辑操作走主库。我们配置了1主2从,实测QPS能达到3000+。
-
缓存策略:
- 使用Redis做两级缓存:
- 本地Caffeine缓存(50ms级响应)
- 分布式Redis缓存(100ms级响应)
- 文档内容采用LRU策略,热点文档缓存命中率达85%
- 使用Redis做两级缓存:
-
实时协作方案:
- 操作转换(OT)算法解决编辑冲突
- WebSocket保持长连接(心跳间隔30s)
- 差分更新减少网络传输(平均每次操作仅传输2KB数据)
3. 核心功能实现
3.1 文档实时协作
这是系统最核心的功能模块,技术实现也最为复杂。我们参考了Google Docs的实现思路,但做了适合中小团队的简化。
关键技术点:
- 操作转换(OT)算法:
java复制// 简化版的OT核心逻辑
public TextOperation transform(TextOperation op1, TextOperation op2) {
if (op1.isNoop() || op2.isNoop()) return op2;
// 插入位置的冲突处理
if (op1.isInsert() && op2.isInsert()) {
if (op1.getPosition() < op2.getPosition()) {
return new TextOperation(op2.getType(),
op2.getPosition() + op1.getText().length(),
op2.getText());
} else {
return op2;
}
}
// 更多转换规则...
}
- 版本控制:
- 采用基线+增量的存储方式
- 每5分钟或20次操作生成一个完整版本
- 版本差异使用diff-match-patch算法计算
- 实时同步:
java复制@Controller
public class DocWebSocketHandler {
@Autowired
private SimpMessagingTemplate template;
@MessageMapping("/doc/edit/{docId}")
public void handleEdit(@DestinationVariable String docId,
EditOperation operation) {
// 1. 应用OT转换
operation = transformOperation(docId, operation);
// 2. 持久化到数据库
saveOperation(docId, operation);
// 3. 广播给其他协作者
template.convertAndSend("/topic/doc/" + docId, operation);
}
}
性能优化:
- 使用Redisson的RAtomicLong实现分布式版本号
- 操作日志分片存储(每1000条操作一个分片)
- 定期合并历史版本(每周日凌晨执行)
3.2 权限管理系统
权限系统采用RBAC模型,但增加了文档级的细粒度控制:
code复制[用户] → [角色] → [权限] → [文档]
数据库设计关键表:
sql复制CREATE TABLE `doc_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`doc_id` bigint(20) NOT NULL COMMENT '文档ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`permission` varchar(50) NOT NULL COMMENT '权限(read/edit/manage)',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_doc_user` (`doc_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
权限校验拦截器实现:
java复制public class PermissionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
Long docId = Long.valueOf(request.getParameter("docId"));
Long userId = getCurrentUserId();
String requiredPerm = getRequiredPermission(handler);
// 检查缓存
String cacheKey = "perm:" + userId + ":" + docId;
String cachedPerm = redisTemplate.opsForValue().get(cacheKey);
if (cachedPerm != null) {
return checkPermission(cachedPerm, requiredPerm);
}
// 查数据库
DocPermission perm = permissionMapper.selectByUserAndDoc(userId, docId);
if (perm == null) {
throw new PermissionDeniedException();
}
// 写入缓存
redisTemplate.opsForValue().set(cacheKey, perm.getPermission(), 1, TimeUnit.HOURS);
return checkPermission(perm.getPermission(), requiredPerm);
}
}
权限继承:
- 支持从文件夹继承权限
- 支持从团队/项目组继承权限
- 显式设置的权限优先级最高
4. 数据库设计与优化
4.1 核心表结构
- 文档表(doc):
sql复制CREATE TABLE `doc` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL COMMENT '标题',
`content` longtext COMMENT '最新内容',
`creator_id` bigint(20) NOT NULL COMMENT '创建人',
`team_id` bigint(20) DEFAULT NULL COMMENT '所属团队',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父文件夹',
`current_version` int(11) NOT NULL DEFAULT '0' COMMENT '当前版本号',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态(1正常 2封存 3删除)',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_team_parent` (`team_id`,`parent_id`),
KEY `idx_creator` (`creator_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 文档版本表(doc_version):
sql复制CREATE TABLE `doc_version` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`doc_id` bigint(20) NOT NULL,
`version` int(11) NOT NULL COMMENT '版本号',
`content` longtext COMMENT '完整内容',
`changes` text COMMENT '变更说明',
`creator_id` bigint(20) NOT NULL COMMENT '创建人',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_doc_version` (`doc_id`,`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 查询优化实践
- 文档列表查询:
sql复制-- 优化前
SELECT * FROM doc WHERE team_id = ? AND status = 1;
-- 优化后
SELECT id, title, creator_id, updated_at
FROM doc
WHERE team_id = ? AND status = 1
ORDER BY updated_at DESC
LIMIT 20 OFFSET 0;
优化措施:
- 只查询必要字段
- 添加合适的索引
- 分页查询避免内存溢出
- 大内容处理:
- 超过1MB的内容单独存储在MongoDB
- 使用MySQL的COMPRESS()函数压缩存储
- 添加content_length字段用于智能预加载
5. 部署与性能调优
5.1 生产环境部署
我们的部署架构:
code复制[Nginx] ←负载均衡→ [SpringBoot×3] ←→ [Redis哨兵] ←→ [MySQL主从]
↑
|
[ElasticSearch集群]
关键配置:
- SpringBoot应用:
yaml复制server:
port: 8080
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 30000
redis:
lettuce:
pool:
max-active: 50
max-wait: 10000
- Nginx配置:
nginx复制upstream backend {
server 192.168.1.101:8080 weight=3;
server 192.168.1.102:8080;
server 192.168.1.103:8080;
keepalive 32;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
5.2 性能测试数据
使用JMeter进行压力测试:
| 场景 | 并发用户 | 平均响应时间 | 错误率 | QPS |
|---|---|---|---|---|
| 文档查看 | 500 | 230ms | 0% | 1250 |
| 文档编辑 | 200 | 350ms | 0.2% | 400 |
| 全文搜索 | 300 | 180ms | 0% | 800 |
优化手段:
-
缓存策略:
- 热点文档缓存命中率提升到90%
- 使用Redis Pipeline批量获取权限数据
-
JVM调优:
bash复制java -jar -Xms1g -Xmx2g -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:ParallelGCThreads=4 \
-XX:ConcGCThreads=2 \
collab-doc.jar
- SQL优化:
- 添加了覆盖索引
- 优化了JOIN查询
- 使用了连接池监控
6. 踩坑经验分享
6.1 实时协作的坑
问题1:初期使用简单的最后写入获胜(LWW)策略,导致用户编辑频繁丢失。
解决方案:
- 实现OT算法处理冲突
- 客户端增加操作队列
- 添加冲突解决提示UI
问题2:网络抖动导致操作丢失。
解决方案:
- 实现操作确认机制
- 客户端本地缓存未确认操作
- 添加断线自动恢复功能
6.2 性能优化经验
- N+1查询问题:
java复制// 错误示范
List<Doc> docs = docMapper.selectByTeam(teamId);
docs.forEach(doc -> {
User creator = userMapper.selectById(doc.getCreatorId()); // 每次循环都查询
doc.setCreator(creator);
});
// 正确做法
List<Doc> docs = docMapper.selectByTeamWithCreator(teamId); // 一次联表查询
- 缓存穿透:
- 使用布隆过滤器过滤非法ID
- 对空结果也进行缓存(设置较短过期时间)
- 大事务问题:
- 将文档保存拆分为多个小事务
- 异步处理非核心流程(如通知、审计日志)
6.3 安全防护措施
- XSS防护:
java复制@Bean
public FilterRegistrationBean<XssFilter> xssFilter() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XssFilter());
registration.addUrlPatterns("/*");
return registration;
}
- CSRF防护:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
- 敏感操作审计:
java复制@Aspect
@Component
public class AuditLogAspect {
@AfterReturning(pointcut = "@annotation(auditLog)", returning = "result")
public void afterReturning(JoinPoint joinPoint, AuditLog auditLog, Object result) {
String action = auditLog.value();
// 记录审计日志
auditLogService.log(action, getCurrentUser(), getIpAddress());
}
}
7. 扩展与演进
7.1 后续优化方向
- 移动端优化:
- 实现PWA离线编辑功能
- 针对小屏幕优化编辑体验
- 增加图片/语音等富媒体支持
- 智能功能:
- 基于NLP的智能排版
- 自动生成文档摘要
- 智能错误检测(如死链、拼写错误)
- 集成能力:
- 与GitHub/GitLab集成
- 支持Markdown双向转换
- 开放API供第三方调用
7.2 架构演进
当前架构已经为扩展做好准备:
- 微服务拆分:
code复制[API Gateway] ←→ [文档服务]
←→ [用户服务]
←→ [搜索服务]
←→ [通知服务]
- 数据分片:
- 按团队ID分片文档数据
- 使用ShardingSphere实现透明分片
- 多云部署:
- 核心服务多可用区部署
- 使用Kubernetes实现弹性伸缩
这个项目从设计到上线历时4个月,期间遇到了各种技术挑战,但最终效果超出了预期。最大的体会是:在协作类系统中,实时性和一致性之间的平衡需要根据业务场景仔细权衡。我们的选择是以最终一致性为代价换取更好的实时体验,这在文档协作场景中被证明是正确的决策。