若依框架多租户改造实战:从数据库到前端的全流程设计

张开发
2026/4/20 1:17:50 15 分钟阅读

分享文章

若依框架多租户改造实战:从数据库到前端的全流程设计
1. 多租户架构的核心设计思路多租户系统最核心的设计目标就是实现数据隔离与资源共享的平衡。我在实际项目中遇到过两种典型方案独立数据库和共享数据库。独立数据库方案虽然隔离彻底但运维成本高共享数据库方案性价比更高也是若依框架改造的首选。在共享数据库方案中关键技术点在于如何优雅地实现租户标识的传递与处理。整个流程可以拆解为三个关键环节租户标识的获取通常来自登录请求或HTTP头租户上下文的维护ThreadLocal是Java中的经典方案SQL自动注入MyBatis拦截器是实现利器这里特别要注意租户上下文的设计。我推荐采用获取即清理的原则在过滤器链的最外层设置try-finally块确保即使业务逻辑抛出异常也不会发生租户ID泄露到其他请求的情况。这种防御性编程在实际运维中能避免很多诡异的问题。2. 数据库层面的改造实战2.1 表结构设计方案租户表的设计要兼顾扩展性和查询效率。我通常会保留这几个核心字段CREATE TABLE sys_tenant ( id bigint NOT NULL COMMENT 租户ID, name varchar(100) NOT NULL COMMENT 租户名称, code varchar(50) NOT NULL COMMENT 租户编码, status tinyint DEFAULT 0 COMMENT 状态0正常 1停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_code (code) ) ENGINEInnoDB COMMENT租户信息表;对于业务表的改造有个实用技巧不要直接修改原表而是通过视图或MyBatis拦截器实现透明访问。比如用户表可以保持原结构通过视图实现租户过滤CREATE VIEW v_sys_user AS SELECT * FROM sys_user WHERE tenant_id CURRENT_TENANT_ID();2.2 数据隔离实现方案MyBatis拦截器是实现SQL自动注入的最佳选择。这里分享一个经过生产验证的拦截器核心代码Intercepts(Signature(type Executor.class, methodquery, args{MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public class TenantInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { Object parameter invocation.getArgs()[1]; BoundSql boundSql ((MappedStatement)invocation.getArgs()[0]) .getBoundSql(parameter); String newSql boundSql.getSql() AND tenant_id TenantContext.getCurrentTenant(); resetSql(invocation, newSql); return invocation.proceed(); } private void resetSql(Invocation invocation, String sql) { // 使用反射修改SQL内容 } }3. 后端服务改造关键点3.1 租户上下文管理租户上下文工具类要特别注意线程安全问题。我推荐使用InheritableThreadLocal而不是普通的ThreadLocal这样可以支持异步线程场景public class TenantContext { private static final InheritableThreadLocalLong CONTEXT new InheritableThreadLocal(); public static void setTenantId(Long tenantId) { CONTEXT.set(tenantId); } public static Long getTenantId() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }3.2 Spring Security适配改造权限验证需要增加租户维度校验。改造UserDetailsService时要注意缓存策略Service public class TenantAwareUserService implements UserDetailsService { Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Long tenantId TenantContext.getTenantId(); SysUser user userMapper.selectByUsername(username, tenantId); if (user null) { throw new UsernameNotFoundException(用户不存在); } ListGrantedAuthority authorities getAuthorities(user.getId(), tenantId); return new org.springframework.security.core.userdetails.User( user.getUserName(), user.getPassword(), authorities); } }4. 前端改造全流程4.1 登录流程改造实战登录页面改造要注意用户体验。我推荐使用级联选择器来组织大型租户列表template el-cascader v-modeltenantPath :optionstenantTree :props{value: id, label: name, children: children} placeholder请选择租户 / /template script export default { data() { return { tenantPath: [], tenantTree: [] } }, async created() { const { data } await getTenantTree() this.tenantTree data } } /script4.2 动态菜单加载方案菜单加载要支持缓存策略避免每次刷新都请求接口// 在vuex中管理租户菜单 const store new Vuex.Store({ state: { menus: [] }, mutations: { setMenus(state, menus) { state.menus menus localStorage.setItem(menus_${TenantContext.id}, JSON.stringify(menus)) } }, actions: { async loadMenus({ commit }) { let menus localStorage.getItem(menus_${TenantContext.id}) if (!menus) { const { data } await getMenusByTenant() menus data commit(setMenus, menus) } return JSON.parse(menus) } } })5. 生产环境注意事项在实际部署时有几点血泪教训值得分享分页查询一定要测试大数据量场景我曾遇到过租户过滤导致索引失效的性能问题导出功能要特别检查确保不会泄露其他租户数据定时任务要明确指定租户范围避免产生脏数据缓存KEY一定要包含租户ID这是个容易忽视的坑对于数据归档建议采用按租户分表的策略。这里有个实用的MyBatis动态表名拦截器实现public class DynamicTableInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { String originalTable sys_log; String tenantId TenantContext.getCurrentTenant(); String dynamicTable originalTable _ tenantId; BoundSql boundSql ((MappedStatement)invocation.getArgs()[0]) .getBoundSql(invocation.getArgs()[1]); String newSql boundSql.getSql().replace(originalTable, dynamicTable); resetSql(invocation, newSql); return invocation.proceed(); } }6. 调试与监控方案多租户系统的日志一定要包含租户信息。推荐使用MDC实现public class TenantFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String tenantId ((HttpServletRequest)request).getHeader(X-Tenant-ID); MDC.put(tenantId, tenantId); try { chain.doFilter(request, response); } finally { MDC.remove(tenantId); } } }在Logback配置中增加tenantId的显示pattern%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} [tenant:%X{tenantId}] - %msg%n/pattern监控方面建议在Prometheus中为每个指标添加tenant标签但要注意基数爆炸问题。可以采用白名单机制只监控重点租户的关键指标。

更多文章