芋道源码剖析之多租户架构设计与实现

张开发
2026/4/19 21:09:19 15 分钟阅读

分享文章

芋道源码剖析之多租户架构设计与实现
1. 多租户架构概述多租户Multi-Tenancy是云计算时代广泛采用的一种架构模式它允许单个软件实例为多个租户提供服务。每个租户在逻辑上相互隔离但共享相同的基础设施和应用代码。这种架构能显著降低运维成本提高资源利用率。在实际项目中我们通常把租户理解为独立的企业、组织或部门。比如SaaS化的CRM系统A公司和B公司使用同一套代码部署但数据完全隔离。多租户架构的核心挑战在于如何优雅地实现这种共享中隔离的特性。芋道系统的多租户实现采用了记录级隔离方案通过统一的tenant_id字段区分不同租户的数据。这种方案在隔离性、扩展性和维护成本之间取得了较好的平衡。整个架构围绕ThreadLocal机制构建租户ID贯穿从Web请求到数据库操作的整个调用链路。2. 数据库层面的租户隔离2.1 三种隔离方案对比多租户在数据库层面主要有三种实现方式方案类型隔离性性能影响扩展性运维复杂度适用场景独立数据库最高无影响差高租户数量少数据敏感独立Schema高较小中中中等规模租户共享表租户ID一般较大好低租户数量多成本敏感芋道选择了第三种方案主要基于以下考虑SaaS场景下租户数量可能快速增长中小型租户的数据量差异不大希望保持较低的运维复杂度2.2 MyBatis-Plus实现细节芋道利用MyBatis-Plus的插件机制实现了自动化的SQL改写。核心类是TenantLineInnerInterceptor它会拦截所有Mapper方法调用public class TenantDatabaseInterceptor implements TenantLineHandler { Override public Expression getTenantId() { return new LongValue(TenantContextHolder.getRequiredTenantId()); } Override public boolean ignoreTable(String tableName) { return TenantContextHolder.isIgnore() || ignoreTables.contains(tableName.toLowerCase()); } }这个拦截器会对SQL进行以下处理SELECT语句自动追加WHERE tenant_id ?INSERT语句自动设置tenant_id字段值UPDATE/DELETE语句自动增加租户条件对于不需要租户隔离的表如系统字典表可以通过ignoreTables配置排除。我们在实际项目中发现合理的表分类能显著提升查询性能。3. Redis缓存隔离方案3.1 键前缀隔离模式与数据库不同Redis作为KV存储芋道采用键名前缀实现隔离。具体实现是通过自定义TenantRedisCacheManagerpublic Cache getCache(String name) { if (!TenantContextHolder.isIgnore() TenantContextHolder.getTenantId() ! null !ignoreCaches.contains(name)) { name name : TenantContextHolder.getTenantId(); } return super.getCache(name); }这种方案的优势在于实现简单无需修改业务代码天然支持Spring Cache注解不同租户缓存完全隔离但需要注意缓存雪崩问题我们建议为不同租户设置差异化的过期时间。3.2 缓存穿透防护在多租户环境下缓存穿透问题会被放大。我们采用布隆过滤器空值缓存的组合方案Cacheable(value users, unless #result null) public User getUser(Long id) { if (!bloomFilter.mightContain(id)) { return null; } // 实际查询逻辑 }实测表明这种方案能减少90%以上的无效查询特别是在租户数量较多时效果显著。4. 消息队列的租户传递4.1 消息头携带模式芋道在各种消息中间件中统一采用消息头携带租户ID的方案// RocketMQ示例 public void sendMessageBefore(SendMessageContext context) { Long tenantId TenantContextHolder.getTenantId(); if (tenantId ! null) { context.getMessage().putUserProperty(tenant-id, tenantId.toString()); } }这种设计保证了生产者自动注入当前租户信息消费者自动识别并设置租户上下文支持跨服务调用时租户信息传递4.2 主流MQ实现对比消息中间件实现方式注意事项RabbitMQMessagePostProcessor需要处理二进制消息头RocketMQSendMessageHook注意消息重试时的租户一致性KafkaProducerInterceptor需确保拦截器加载顺序Redis自定义消息头Pub/Sub模式无持久化保证我们在实际项目中更推荐使用RocketMQ它的消息轨迹功能便于排查租户相关问题。5. 定时任务的特殊处理5.1 租户并行执行芋道的定时任务采用TenantJob注解实现跨租户批量执行Aspect public class TenantJobAspect { public Object around(ProceedingJoinPoint joinPoint) { tenantIds.parallelStream().forEach(tenantId - { TenantUtils.execute(tenantId, () - { joinPoint.proceed(); }); }); } }这种设计带来了两个好处一个任务自动在所有租户执行不同租户的任务真正并行处理5.2 失败处理策略我们建议为定时任务添加以下容错机制单租户任务失败不影响其他租户记录详细的执行日志实现任务重试机制try { // 业务逻辑 } catch (Exception e) { log.error([execute][租户({})任务执行失败], tenantId, e); retryLater(task); }6. 异步调用的上下文传递6.1 TransmittableThreadLocal芋道使用阿里开源的TTL组件解决异步场景下的上下文传递Bean public ThreadPoolTaskExecutor executor() { executor.setTaskDecorator(TtlRunnable::get); return executor; }相比普通的ThreadLocalTTL具有以下特性支持线程池场景支持嵌套调用低性能开销实测3%6.2 异步最佳实践我们总结了以下经验异步方法尽量自包含减少上下文依赖关键业务信息显式传递设置合理的线程池大小Async public void asyncProcess(Long orderId) { // 显式传递必要参数 processOrder(orderId, TenantContextHolder.getTenantId()); }7. 边缘场景处理7.1 租户忽略机制通过TenantIgnore注解可临时跳过租户过滤TenantIgnore public ListDictData getDictAll() { return dictMapper.selectList(); }适用场景包括全局数据查询系统初始化跨租户统计分析7.2 指定租户执行TenantUtils工具类支持在特定租户上下文执行代码public void migrateData(Long sourceTenant, Long targetTenant) { TenantUtils.execute(sourceTenant, () - { ListUser users userMapper.selectList(); TenantUtils.execute(targetTenant, () - { users.forEach(this::importUser); }); }); }这种模式特别适合数据迁移和批量处理场景。8. 性能优化实践8.1 索引优化建议多租户系统必须为tenant_id创建索引ALTER TABLE sys_user ADD INDEX idx_tenant (tenant_id);组合索引应将tenant_id放在首位CREATE INDEX idx_tenant_dept ON sys_user(tenant_id, dept_id);8.2 缓存命中率提升我们通过以下手段将缓存命中率从75%提升到92%租户级缓存预热差异化过期策略热点数据特殊处理监控数据显示优化后Redis集群负载下降40%。9. 安全防护措施9.1 租户越权防护在Web层通过双重校验确保数据安全public User getById(Long id) { User user userMapper.selectById(id); if (!user.getTenantId().equals(TenantContextHolder.getTenantId())) { throw new ForbiddenException(无权访问该数据); } return user; }9.2 审计日志规范建议记录关键操作的租户信息Log(title 用户管理, businessType UPDATE) public void updateUser(User user) { user.setUpdateBy(TenantContextHolder.getTenantId() : getUserId()); userMapper.updateById(user); }10. 开发注意事项避免在静态变量中存储租户相关数据批量操作时注意检查租户一致性跨服务调用必须传递租户上下文单元测试需要模拟租户环境Test public void testUserCrud() { TenantUtils.execute(1L, () - { // 测试逻辑 }); }这套多租户架构已在多个百万级用户的生产环境稳定运行。实际使用中开发者只需要关注业务逻辑实现基础设施层的租户隔离由框架自动完成。对于需要特殊处理的场景框架提供了足够的扩展点供定制开发。

更多文章