
1. 项目第11天团队空间基础模块拆解1.1 这一天我们到底做了什么走到第十一天团队空间这个项目的骨架已经搭起来了。前面十来天的时间花在了技术选型、环境搭建、基础框架整合和核心数据模型设计上说实话前两天还被几个配置问题折腾得够呛。到了这一天目标只有一个把基础功能做扎实同时把权限系统和数据分层这两块硬骨头啃下来。这里说的基础功能包括这么几个部分空间创建与管理、成员邀请与加入、文档的创建和编辑、任务的分配与状态流转。这四块是团队协作类产品的底座没有它们后面加再多花哨的功能都是空中楼阁。权限控制是所有协作系统的中场发动机分库分表则是数据量上来之后的逃生通道不提前铺好路等线上出问题再回头看就晚了。我的惯例做法是先把业务需求画成一张简单的流程图贴在白板上把模块边界划分清楚再动手写代码。团队空间的业务天然有一个“空间”维度几乎所有操作都挂在某个空间下面。基于这个前提数据库模型、接口路径、权限标识都要围绕这个维度来设计不能自嗨地套用标准REST风格就完事。1.2 核心业务表结构设计思路业务表设计是基础功能实现的第一刀刀法直接影响后面所有功能的复杂度。团队空间这套系统涉及的核心表包括空间表记录空间名称、创建者、当前成员数、状态等基础信息成员表记录用户与空间的关联关系包含角色信息和加入时间文档表记录文档标题、内容、所属空间、创建人、最后编辑人、版本号任务表记录任务标题、描述、负责人、截止时间、优先级、状态、关联文档操作日志表记录用户在空间内的重要操作便于审计和追溯字段命名上我采用了下划线风格主键统一使用雪花ID。这个方案优化的核心逻辑是在分布式场景下自增ID太容易撞车UUID又有索引效率问题雪花ID既能保证全局唯一又能保持趋势递增对数据库索引友好。团队空间业务虽然现阶段用户量不大但设计上一定要按照未来数据量扩容的方向走否则分库分表那天就是重写系统的开始。文档表和任务表都要加版本号字段这是协作系统中特别容易忽略的点。多人编辑同一份文档时版本号可以避免后写覆盖先写的惨剧。我设计的是简单乐观锁策略更新时带上versionold_version的条件受影响行数为0就说明有冲突提示用户刷新后再操作。这个策略在第11天就实现成本极低收益巨大。1.3 基础功能接口规划与落地顺序接口规划上我没有一上来就铺开写CRUD而是按照业务依赖关系排了个序空间 → 成员 → 文档 → 任务。空间是所有业务的容器先实现空间相关接口成员需要挂在空间下所以第二步文档和任务都依赖前两者放在后面。每一步实现完成后我会用Postman跑通完整的业务流确保接口之间能衔接上。这个顺序的意义在于权限控制的实现需要建立在“空间-成员”这套关系之上没有这个前置条件RBAC就成了无源之水。另一个考虑是先跑通业务主链路团队可以早点看到系统雏形给整个项目一个信心锚点。接口路径我统一使用/api/{version}/space/{spaceId}/xxx这样的风格把空间维度显式体现在路径上。这样做的直接好处是一眼就能看出某个接口操作的是哪个空间下的资源权限校验时也可以直接从路径中提取spaceId进行数据范围校验省去从请求体中解析的麻烦。2. RBAC权限控制从理论表设计到代码落地的完整链路2.1 RBAC模型为什么是这个系统的首选RBACRole-Based Access Control基于角色的访问控制可以说是权限控制领域最成熟、最经典、落地性最强的模型。它的核心思想很直观不直接给用户分配权限而是在用户和权限之间加一个“角色”作为中间层。用户关联角色角色关联权限用户通过角色间接获得权限。为什么这样做直接给用户挂权限在团队只有三五人的时候很灵活但人一多就变成灾难。某个空间有四十个成员每个人的权限都要单独维护光是配置工作就能让人崩溃。引入角色层之后空间管理员只需定义“管理员”“编辑者”“访客”等几个角色把权限挂在角色上然后给用户分配角色操作量呈指数级下降。生活化的类比就像门禁卡。每个房间有对应的门禁权限你不会直接给每个人复制几十张门禁卡而是定义几种员工卡类型每个类型预设好能进哪些门人事只需要发卡的时候选择一下员工类型就行。换部门了改一下类型离职了直接把卡注销不需要一个个房间去删权限。RBAC对团队空间系统的另外一层价值在于权限变更的响应速度极快。当某个空间的管理员把某人从“编辑者”调成“访客”这个人的文档编辑权限立即被收回不需要修改任何业务代码只需要解绑角色关系。这种灵活性在后来的实际使用中非常重要因为团队成员的职责变动是非常频繁的。2.2 RBAC权限模型的五张核心表怎么设计标准RBAC模型落地到关系型数据库最少需要五张表。我这里的表设计还针对团队空间的场景做了一点扩展加入了空间维度的概念。第一张是用户表记录用户的基本信息包括ID、用户名、密码摘要、状态等。第二张是角色表记录角色信息。这张表需要特别说明的一点是角色本身要区分类型因为是团队空间场景既有平台级别的角色超级管理员也有空间级别的角色空间管理员、普通成员所以我加了一个role_type字段来区分。第三张是权限表也叫菜单权限表或资源权限表。我采用的是权限标识字符串的方式去描述权限比如space:document:create代表创建文档的权限space:member:invite代表邀请成员权限。字符串比数字ID直观得多在日志、错误提示、权限配置中都能直接看出来含义。第四张和第五张就是关联表了用户-角色关联表和角色-权限关联表。两张关联表的结构非常相似都是纯关联关系没有业务字段。唯一要注意的是索引设计联合索引的字段顺序要遵循查询习惯。团队空间场景还需要解决一个“数据范围”问题。经典RBAC解决的是“能不能做某件事”但没解决“能对哪些数据做”。举个例子两个用户都有“查看文档”的权限但一个属于A空间一个属于B空间他们各自只能看到自己空间下的文档。我的方案是在用户-角色关联表中增加scope_type和space_id字段。当scope_type为GLOBAL时该角色权限作用于全平台为SPACE时权限只作用于space_id指定的空间。2.3 权限校验的完整链路权限校验不能只停留在“登录了就能访问”的层面那顶多算认证不算授权。完整流程应该是登录认证 → 加载用户角色 → 加载角色权限 → 请求时校验权限 → 鉴权通过放行或失败拦截。登录认证我用的是JWT Token方案。用户登录成功后服务端签发一个Token里面包含用户ID和过期时间。后续请求头带着这个Token拦截器解析Token拿到用户ID再根据用户ID去查询权限信息。这里有一个性能上的关键决策权限信息要不要走缓存答案是必然要的。如果每次请求都去数据库关联查询用户角色、角色权限四张表的关联查询在高并发下会直接拖垮数据库而且权限数据是典型的读多写少。我用Redis缓存用户ID对应的角色和权限标识集合key设计成auth:user:{userId}:permissions过期时间30分钟。角色或权限变更时可以主动删除对应用户的缓存key强制下次请求时重新加载。接口层的权限校验我自定义了一个RequirePermission注解标注在Controller方法上属性值为需要的权限标识。例如RequirePermission(space:document:delete) public ResultVoid deleteDocument(String docId) { // 业务逻辑 }拦截器会先检查目标方法上是否有这个注解有就取当前请求用户的权限缓存集合进行匹配。匹配不通过就抛出权限不足异常由全局异常处理器返回统一的403响应结构。这套链路理解了之后再去看Spring Security或者Shiro的过滤器链会觉得思路完全一致只是实现细节不同。2.4 RBAC落地中容易踩的三个深坑第一个坑以为权限数据只在登录时加载一次就够了。很多初写权限系统的开发者会在登录成功后将用户的全部权限写入Session之后就不管了。问题是用户权限被调整后要等Session过期才生效。虽然在Redis中设置了30分钟过期看起来能接受但在权限敏感的场景里30分钟太久。我的做法是预留一个强制刷新接口同时在做角色变更操作时事务提交成功后立即手动删除该用户的权限缓存。第二个坑校验权限时只关注“权限标识”而忽略“数据范围”。很多团队照着RBAC的教程做完了功能发现用户在A空间拥有编辑权限请求B空间的编辑接口也能通过这就是典型的只校验了功能权限没有校验数据权限。团队空间的每个接口都要把spaceId提取出来去用户-空间-角色关系中校验该用户在此空间下是否拥有对应角色。第三个坑表设计里忽略了角色与空间的绑定。如果一张角色表里的角色是全局唯一的那么“A空间的编辑者”和“B空间的编辑者”会变成两个角色空间一多角色表就会爆炸。正确做法是角色模板化和角色实例化分成两层模板角色定义权限集合实例角色绑定具体的space_id。比如编辑者是模板编辑者(空间A)和编辑者(空间B)是实例各自保存自己的角色ID。3. ShardingSphere详解团队空间的数据分片实战3.1 数据量还没大为什么要这么早引入分库分表这是团队空间项目里最容易被挑战的技术决策。团队刚起步数据库表里可能就几百行数据这时候谈分库分表是不是过度设计我得说清楚这个逻辑引入ShardingSphere确实会增加系统复杂度但我要的不是在现有数据规模上用分片而是要提前把数据访问层的能力储备好。具体来说有两个驱动力第一团队空间是典型的SaaS多租户系统每个空间的数据是天然隔离的。当空间数量增长到一定规模单库单表的数据量会急速膨胀。特别是文档表和操作日志表这两类数据的特点是只增不减过个一两年单表轻松破亿。真到那时候再改架构业务代码全部要调成本高到无法接受。第二分库分表考验的是数据模型设计。一张业务表如果从一开始就没有考虑分片键后期强行分片会非常痛苦比如有些查询无法路由到具体分片只能全库扫描。现在就把分片键定义好让数据访问层从一开始就在分片模式下运转后续扩容只需要加机器、调配置不用改业务代码。3.2 ShardingSphere-JDBC和ShardingSphere-Proxy怎么选ShardingSphere生态下有两个模式JDBC模式和Proxy模式。JDBC模式以JAR包形式嵌入应用通过实现DataSource接口接管应用的所有SQL请求直接与底层数据库交互。Proxy模式则是独立部署一个代理服务应用通过连接代理来访问数据库对应用来说代理就像普通的MySQL服务。团队空间项目选型时我最终选了JDBC模式。理由很直接我们的应用是Java单体应用JDBC模式对性能的影响更小没有额外的网络跳转开销配置和使用也最简单。JDBC模式直接把数据分片逻辑埋在了业务进程内调试定位问题时可以直接看日志拿到完整SQL执行链路对早期开发阶段是关键优势。Proxy模式适合什么样的场景呢跨语言团队、非Java应用、或者需要将分片能力集中治理的复杂环境。如果你的团队同时有Java和Go的项目JDBC模式没法做到代码复用Proxy模式就是唯一选择。但在我们的技术约束下JDBC模式是“杀鸡用牛刀但刀刀见血”的方案简单直接有效。3.3 分片键选型为什么空间ID是最自然的选择分片键的选择直接决定分片效果和查询性能这是整个分库分表设计中我认为最不能妥协的一环。团队空间系统的数据访问模式有非常明显的特点绝大多数查询都带有space_id条件。例如查某个空间下的文档列表、查某个空间下的任务列表、查某个空间下的操作日志。这意味着空间ID天然适合作为分片键因为按空间维度切分数据后同一个空间的数据会被路由到同一分片单空间的数据操作不需要跨库跨表查询效率最大化。具体分片算法我用的是哈希取模公式很简单分片序号 hash(space_id) % 分片数量。这个算法的优点是数据分布均匀度好只要空间ID足够随机不会出现某个分片撑爆而其他分片空闲的问题。相比按范围分片比如按ID区间哈希取模不需要对业务规则做假设对新空间也能均匀打散。分片数量初始化设计为4张表分库暂不做因为单库4张表已经能扛住现阶段的数据访问压力。未来扩容时只需要将分片数量扩展到8张或更多然后借助ShardingSphere的扩容工具做数据迁移即可。我在设计上从一开始就避免了范围分片就是为了防止未来换算法时数据无法平滑迁移。ShardingSphere的配置示例如下rules: sharding: tables: document: actualDataNodes: ds.document_${0..3} tableStrategy: standard: shardingColumn: space_id shardingAlgorithmName: document_space_hash keyGenerateStrategy: column: id keyGeneratorName: snowflake shardingAlgorithms: document_space_hash: type: HASH_MOD props: sharding-count: 4 bindingTables: - document,task broadcastTables: - role - permission这里需要注意几个细节actualDataNodes里的${0..3}是行表达式写死了4张物理表的名字shardingColumn指定为space_id这是路由的依据bindingTables声明了文档表和任务表的绑定关系保证关联查询时不会跨分片执行联合查询角色表和权限表配置成广播表因为角色权限这类数据量小且需要全分片可见。3.4 分布式ID雪花算法在ShardingSphere中的配置内幕分库分表之后最直接的一个问题数据库自增ID还能不能用答案是不能。两个分片各自维护自增序号必然产生重复ID冲突事故在所难免。ShardingSphere内置的分布式ID生成器基于雪花算法。雪花算法生成的ID是一个64位Long整数结构上分为几段未使用的1位符号位、41位时间戳、10位工作机器ID、12位序列号。41位时间戳能支撑约69年的时间跨度10位工作机器ID最多支持1024台机器12位序列号在单机单毫秒内最多生成4096个ID。就团队空间系统现阶段规模而言雪花算法绰绰有余。在配置层面上面YAML中已经写明了通过keyGenerateStrategy把主键生成方式指定为snowflake。生成出来的ID是全局唯一的并且趋势递增直接作为主键插入分片表没有任何问题。需要注意的是主键ID必须由应用侧生成还是由ShardingSphere生成。推荐的方案是在插入语句中不写ID字段由ShardingSphere拦截SQL后自动补全这样应用代码完全不用感知ID生成逻辑。顺带提一句雪花算法强依赖机器时钟如果部署的服务器时钟发生回拨可能生成重复ID。实践中要给运维人员提个醒不要随意NTP同步时钟频繁回调。真要严谨可以在生成器里加一段时钟回拨检测逻辑发现回拨就延迟等待或重新初始化机器ID。3.5 分片环境下SQL执行的一些边界问题很多人分库分表踩坑不是因为配置不会写而是因为业务SQL超出了分片中间件的能力边界。ShardingSphere不是万能的它也有自己的SQL支持边界。跨分片的JOIN查询是重灾区。如果A表的记录和B表的记录被分到了不同的物理表那么这两个表做JOIN的时候ShardingSphere无法在单库内完成关联只能把各分片数据拉取到内存中合并数据量大时内存直接被打满。解决思路有两个要么把要关联的表配置为bindingTables让两个表按同样的分片键和分片数量分片保证关联数据一定落在同一个分片上要么在应用层面做二次查询将关联逻辑拆成两次单表查询再内存拼接。分页排序问题是另一个高频异常。MySQL的LIMIT关键字在分片环境下会产生语义偏差。假设LIMIT 10, 20这样的语句如果直接下推到4个分片每个分片各取出20条然后在内存中合并排序再取第10到30条这个逻辑看起来对。但当排序字段不是分片键时ShardingSphere为了拿到正确顺序需要把所有分片满足条件的数据全部拉到内存排序有的版本会有全查询再过滤的温吞行为性能极差。规避分页深翻页的常见做法是游标分页通过条件定位例如“按ID小于上次查询的最小ID取前20条”。这个方案跳过了记录偏移量让查询条件能直接带分片键或排序键快速定位于单个分片也是应对深分页性能瓶颈的标准解法。聚合函数方面像COUNT、SUM、AVG这类函数ShardingSphere会自动改写下推到各分片再合并结果可以放心使用。常用的GROUP BY也能正确处理但要注意分组字段和排序字段的最佳做法是包含分片键否则还是会有合并排序的额外开销。4. 运行期问题排查实录亲历的四个最磨人的Bug4.1 分片键丢失导致的全库路由扫描排查过最令人头疼的一个问题是某条查询SQL明明条件里带了space_id日志显示却对4张表全部执行了查询。一开始我以为是ShardingSphere版本Bug仔细排查之后才发现问题不在中间件而在代码写法。场景是这样的查询语句写成了WHERE id ? AND space_id IN (?)按理说space_id条件存在应该能路由到指定分片。但ShardingSphere的字段提取逻辑只识别对等比较条件如space_id ?放在IN子句中的条件当参数为复杂表达式时会被判定为不可路由转为全分片扫描执行。这不是什么隐藏Bug就是使用边界问题。排查这类问题最快的方法打开ShardingSphere的SQL解析日志。配置log.level.org.apache.shardingsphereDEBUG后控制台能看到SQL解析的结果类型包括逻辑SQL和真实的绑定SQL。一旦发现路由逻辑不符合预期先检查分片键在SQL中是否以对等形式出现再看字段名是否拼写有误。4.2 分布式事务的问题两阶段到底要不要上分库分表后事务跨分片的问题就无法回避了。一个操作如果同时修改了文档表和任务表且它们落在不同的物理分片上一个分片提交成功了另一个分片却失败回滚了数据一致性就破了。团队空间系统对这个问题的策略分阶段考虑。现阶段业务操作大多是单空间下的数据变更一条SQL只能路由到一个分片事务用本地事务就够了。真正的跨分片事务场景比如平台级的管理员操作要同时修改多个空间的数据现阶段很少碰到所以我压住了两阶段提交的引入冲动。两阶段提交的代价在于同步阻塞带来的性能下降还有协调者故障导致的事务悬挂风险。在团队空间这种读写比例悬殊、事务链路并不复杂的系统中杀鸡用牛刀的成本可能高于收益。真到了需要跨分片事务的那天我更倾向于采用本地消息表定时任务补偿的方案因为它实现简单、易理解、对现有代码侵入性低而且最终的最终一致性能满足团队协作场景。4.3 缓存穿透的问题没有发生但缓存一致性真的发生了权限缓存和业务数据缓存都用了Redis之后缓存和数据库的一致性维护是个绕不开的话题。我的做法是先更新数据库再删除缓存。为什么不是先删缓存再更新数据库后者存在窗口期删缓存后旧请求把新数据又写回缓存而数据库还没来得及更新缓存里的值就是脏数据。先更新数据库再删缓存也并非完美。极端情况下A请求更新数据库完成后B请求读取数据库拿到新值并写回缓存之后A请求才删除缓存结果新值被误删缓存退化为旧值下次请求又把旧值从数据库读回来。这个问题在低并发下几乎不可能发生在团队空间场景可以接受。真要追求极致可以在删除缓存前加一个短暂延迟或者用版本号让缓存写入基于时间戳判断是否过期数据。4.4 分片算法选择失误造成的数据倾斜预警哈希取模的分片算法理论上是均匀分布的但我在压测时发现某个分片的行数比其他分片多了快一倍。定位之后发现罪魁祸首是测试环境的空间ID生成方式不随机而是从固定数字递增生成而space_id太小的时候哈希值分布也会有偏差。这种数据倾斜如果持续积累会导致单分片成为性能瓶颈。应对方案是在应用侧做两层兜底第一分片键生成上严格控制对空间ID再做一次加盐哈希再取模第二监控脚本定期统计各物理分片的行数和查询耗时出现超过阈值的情况就提前告警而不是等到用户反馈页面变慢再排查。5. 写给同样在路上的开发者几点实操心得经过这一天的高强度开发有几条体会特别深。聊参数和配置的人很多但能把边界约束讲清楚的人不多这里帮大家做一个集中总结。权限系统的切入顺序不要颠倒。先把用户-空间-角色的数据模型建好再看接口面需要哪些权限点最后才动手写拦截器。很多团队上来就选定Spring Security框架堆了一堆过滤器配置结果业务模型一改权限配置全部推翻重来。RBAC的复杂度99%在表设计和权限点划分上框架只是帮我们省了重复工作。ShardingSphere的引入时机我建议越早越好但前提是数据模型必须足够规范。如果业务代码里大量使用SELECT *、不带分片键的条件查询、跨表JOIN分片之后必然重构一片。反过来如果从一开始就遵循“所有查询必须携带分片键”的纪律中间件带来的额外开销其实很小而且为未来的数据增长铺平了道路。关于分片键有一个反复确认过的经验值得摆到台面上讲分片键必须和查询模式强绑定不要为了“看起来专业”选用业务主键或者ID作为分片键。团队空间的自然分片键是space_id因为这个维度主导了绝大多数查询条件和数据归属关系。如果硬要用文档ID做分片键单个空间下的文档列表查询就变成了扫描全部分片再合并结果性能直接崩。第十一天的开发到这里基础功能已经能连贯跑通权限控制做到了功能权限与数据权限的双层校验数据访问层也具备分库分表的扩展能力。接下来要推进的是文档在线预览、任务看板视图、消息通知机制这些锦上添花的功能。底层已经稳固往后每一层的开发都能安心停留在业务逻辑本身而不是战战兢兢担心架构塌方。