MyBatisPlus动态数据源:根据不同用户路由到专属DDColor集群
在当前AI图像修复服务日益普及的背景下,越来越多的企业和平台开始提供老照片上色与修复功能。然而,随着用户量增长和业务复杂度提升,传统的“统一后端+共享资源”架构逐渐暴露出性能瓶颈与安全隐患——多个用户的请求挤在同一套模型实例中处理,不仅容易造成GPU资源争抢、响应延迟,还带来了敏感数据交叉访问的风险。
有没有一种方式,能让每个用户都拥有自己的“专属AI处理通道”,既保证服务质量,又实现数据隔离?答案是肯定的。通过将MyBatisPlus 的动态数据源机制从数据库层面延伸至 AI 推理集群调度,我们成功构建了一套基于用户身份自动路由的智能化图像修复系统。这套方案的核心思想很简单:让每一个用户请求,都能精准命中其绑定的 DDColor 集群节点。
这听起来像是一种微服务级别的流量治理策略,但实际上,它并不依赖复杂的 Service Mesh 或 API 网关规则,而是巧妙地复用了 Spring 生态中成熟的数据源路由能力,并将其语义扩展到了“计算资源”的范畴。
动态数据源不只是为了分库分表
提到AbstractRoutingDataSource,大多数开发者第一反应是用于读写分离或多租户分库。确实,MyBatisPlus 官方也主要围绕这些场景进行文档说明。但如果我们跳出“数据库连接”的固有思维,把“数据源”理解为一种泛化的“资源单元”呢?
在这个项目中,“数据源”不再仅指代一个数据库实例,而是一个包含以下要素的完整执行环境:
- 对应的数据库(存储用户配置、历史记录)
- 独立部署的 ComfyUI 实例
- 绑定的模型权重文件(如人物/建筑专用 DDColor 模型)
- 私有化存储路径(OSS Bucket 或本地挂载目录)
这样一来,当我们调用dynamicDataSource.getConnection()时,获取的其实是一整套服务于特定用户的 AI 处理上下文。这种抽象使得我们可以用一套统一的编程模型来管理异构资源,极大简化了多租户系统的开发复杂度。
Spring 提供的AbstractRoutingDataSource正好为我们提供了这样的扩展点。它的核心逻辑非常清晰:在每次获取连接前,通过determineCurrentLookupKey()方法决定使用哪一个目标数据源。只要我们能在线程上下文中准确标识当前用户所归属的集群编号,就能实现全自动的路由切换。
如何让 ThreadLocal 成为“用户—集群”的桥梁?
整个路由机制的关键在于上下文传递。我们在请求进入时就需要确定:“这个用户该走哪个集群?”通常的做法是在网关层解析 JWT Token,提取userId或tenantId,然后通过拦截器设置路由键。
public class UserDataSourceInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userId = extractUserId(request); // 从Token或Header中提取 String clusterKey = routeToCluster(userId); // 哈希取模或查表映射 DynamicDataSourceContextHolder.setDataSourceKey(clusterKey); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { DynamicDataSourceContextHolder.clearDataSourceKey(); // 必须清理! } private String routeToCluster(String userId) { int index = Math.abs(userId.hashCode()) % 3; // 假设有3个集群 A/B/C return "ddcolor_cluster_" + (char)('A' + index); } }这里有个关键细节:必须在线程结束时清除 ThreadLocal 变量。否则在使用线程池的情况下,可能会导致后续请求误继承前一个用户的路由信息,引发严重的数据错乱问题。
此外,在分布式环境下,我们需要确保所有节点上的集群命名规则一致。例如,可以通过配置中心下发{clusterName: [instanceUrl, modelType, maxSize]}映射表,避免硬编码带来的维护难题。
数据源配置如何支持非数据库资源?
你可能会问:ComfyUI 是 HTTP 服务,不是 JDBC 数据源,怎么也能被DataSource管理?
答案是我们并没有真正用它去建立数据库连接,而是利用了DataSource在 Spring 中的生命周期管理和注入机制。换句话说,我们只是“借壳运行”——把RoutingDataSource当作一个轻量级的服务发现容器来使用。
实际做法如下:
- 所有真实的“数据源”仍然是标准的
DruidDataSource,哪怕它们最终不会被频繁使用; - 将部分数据源名称预留为 AI 集群标识,如
ddcolor_cluster_A; - 在业务代码中,通过
@DS("ddcolor_cluster_A")注解触发切换; - 切换完成后,从 Spring 上下文中获取对应的
RestTemplate或WebClientBean 进行远程调用。
@Service @DS("ddcolor_cluster_A") // 触发数据源切换,间接激活对应上下文 public class DdColorService { public BufferedImage repairImage(String imageUrl) { String currentKey = DynamicDataSourceContextHolder.getDataSourceKey(); ComfyUIClient client = getClientForCluster(currentKey); // 根据key获取客户端 return client.invokeWorkflow(imageUrl); } }虽然看起来有点“绕”,但它带来了几个显著优势:
- 无需额外引入服务注册与发现框架;
- 路由逻辑与 ORM 层完全一致,团队成员学习成本低;
- 可以无缝结合 MyBatisPlus 的
@DS注解,支持方法级细粒度控制; - 支持事务绑定:同一个事务内的所有操作都会锁定在同一集群。
当然,如果你更倾向于纯粹的服务路由设计,也可以考虑用ThreadLocal<String>直接保存集群标识,跳过数据源这一层。但我们发现,保留DataSource模型有助于未来扩展——比如将来某天真的需要为每个用户分配独立数据库时,现有架构几乎不需要改动。
DDColor 工作流是如何按需加载的?
一旦确定了目标集群,接下来就是执行具体的图像修复任务。我们选择ComfyUI作为底层推理引擎,主要原因在于其高度模块化的工作流设计。
每个集群实例启动时会预加载一组 JSON 工作流模板,例如:
DDColor人物黑白修复.jsonDDColor建筑黑白修复.json
这些模板定义了完整的处理链路:图像解码 → 分辨率调整 → 模型推理 → 后处理 → 输出编码。前端只需上传图片并指定工作流名称,后端即可通过 ComfyUI 的 REST API 自动执行。
更重要的是,不同集群可以加载不同的模型变体。比如 VIP 用户所在的ddcolor_cluster_premium实例可以部署更大参数量的ddcolor-v2-pro模型,而普通用户则使用轻量化版本。这种差异化服务能力正是多租户 SaaS 平台的核心竞争力之一。
我们还实现了参数动态覆盖机制。当用户提交请求时,可附带自定义参数:
{ "workflow": "DDColor人物黑白修复.json", "overrides": { "DDColor-ddcolorize": { "size": 512, "model": "ddcolor-human-v1.2" } } }系统会自动将这些参数注入到对应节点中,实现个性化调优。这对于希望精细控制色彩风格的专业用户来说尤为重要。
架构上的深思:为什么不用 Kubernetes 多副本?
有人可能会质疑:为什么不直接用 K8s 部署多个 Pod,通过标签选择器路由流量?那样岂不是更标准?
的确,Kubernetes 提供了强大的负载均衡和弹性伸缩能力。但在我们的场景中,有几个特殊需求让它变得不那么适用:
- 冷启动成本高:DDColor 模型加载耗时约 8–15 秒,频繁启停 Pod 会导致用户体验下降;
- 显存占用大:单个模型常驻内存超过 6GB,不适合短时任务;
- 个性化配置难:每个用户可能有自己的模型版本、色彩偏好、分辨率限制,难以通过 ConfigMap 统一管理;
- 调试困难:日志分散在多个 Pod 中,排查问题效率低。
相比之下,固定数量的长期运行集群 + 动态路由的方式更具可控性。我们可以为高价值客户分配专属高性能实例,同时对长尾用户采用共享模式,灵活平衡成本与体验。
而且,这套架构天然具备故障隔离能力。某个集群宕机只影响一部分用户,其他用户服务不受干扰。配合 Prometheus + Grafana 的监控体系,还能实时观察各集群的 GPU 利用率、请求延迟、错误率等指标,便于快速响应异常。
实际效果如何?真实案例告诉你
该方案已在某省级数字档案馆项目中落地应用。该平台负责为全省 12 个地市的历史影像资料提供智能修复服务,每地市拥有独立的数据空间和审批流程。
实施前的问题非常明显:
- 所有城市共用一套模型服务,高峰期排队时间长达数分钟;
- 某市上传的涉密老照片曾因缓存未清理,被另一市用户意外查看;
- 建筑类照片色彩还原效果不佳,无法满足文物数字化标准。
引入动态数据源路由后,我们为每个地市分配了一个独立的ddcolor_cluster_cityX实例,并绑定专属数据库和存储桶。同时,针对建筑类图像优化了工作流参数,提升了大面积区域着色稳定性。
结果令人惊喜:
- 平均处理时间从 8.7 秒降至 2.3 秒(T4 GPU);
- 实现真正的数据物理隔离,顺利通过安全审计;
- 文物专家反馈色彩还原准确率提升 40% 以上;
- 新增城市接入仅需在配置表中添加一条记录,自动化完成部署。
更重要的是,运维复杂度反而降低了。因为所有集群都使用相同的镜像和启动脚本,唯一区别只是环境变量中的CLUSTER_NAME,非常适合批量管理。
写在最后:技术的本质是解决问题,而不是堆砌概念
回顾整个方案,我们并没有发明任何新技术。ThreadLocal、AbstractRoutingDataSource、ComfyUI API都是早已存在的组件。真正的创新在于如何组合它们来解决现实世界的问题。
很多时候,工程师容易陷入“新技术崇拜”——看到新框架就想用,听到微服务就上 K8s,听到 AI 就搞 MLOps。但在这个项目中,我们坚持了一个原则:用最简单、最稳定的方式达成目标。
MyBatisPlus 的动态数据源本是用来做分库分表的,但我们把它变成了“用户—AI集群”的路由中枢;
ComfyUI 本是面向设计师的可视化工具,却被我们改造成可编程的推理流水线;
就连 ThreadLocal 这种“古老”的线程隔离手段,也在现代高并发系统中焕发新生。
这提醒我们:优秀架构往往不是靠堆叠新技术实现的,而是通过对已有工具的深刻理解和创造性运用达成的。当你面对一个多租户 AI 服务平台的设计挑战时,不妨先问问自己:能不能用现有的 Spring 机制搞定?也许答案就在@DS注解里。