异常链通过封装带租户id的业务级异常并保留原始cause,实现逻辑层故障隔离;需在dao层注入租户上下文构建异常链,网关据此做租户级熔断,监控按链归因告警。

异常链本身不直接隔离物理库异常,但它能帮你把底层数据库错误“翻译”成租户可理解、可拦截、可归因的业务级异常,从而在逻辑层实现故障隔离。
异常链不是兜底,而是上下文传递的桥梁
当某个租户触发 MySQL 连接超时或表不存在错误时,JDBC 抛出的 SQLException 是进程级、无租户标识的原始异常。若直接向上抛,整个服务可能被一个租户的库问题拖慢甚至雪崩。异常链的作用,是把原始异常作为 cause 封装进带租户上下文的新异常中,让后续拦截器、熔断器或监控系统能识别“这是 tenant-003 的专属问题”,而非全局故障。
- 必须保留原始 cause(如
new DataAccessException("Failed to query order for tenant-003", e)),否则丢失根因线索 - 新异常类型需携带租户 ID 字段(如
TenantDatabaseException),不能只靠 message 拼接 - 避免在 catch 块里吞掉异常又不 rethrow —— 这等于切断异常链,让租户隔离失效
在 DAO 层注入租户上下文并构建异常链
每个数据库操作前,应从 TenantContext.get() 获取当前租户 ID,并在异常发生时将其纳入异常构造过程。尤其在使用 MyBatis 或 JPA 时,可在自定义 Interceptor 或 Repository 方法中统一处理:
- MyBatis 拦截器中捕获
PersistenceException,提取 cause 中的SQLException,再包装为TenantSqlException(tenantId, sql, e) - JPA
@Repository方法用@ExceptionHandler捕获DataAccessException,检查是否由租户专属表(如tenant_003_orders)引发,再重抛带租户信息的子类 - 不要在 service 层才开始包装异常——此时已丢失 SQL 执行上下文和租户绑定时机
网关层用异常链做租户级熔断开关
Spring Cloud Gateway 等入口组件可基于异常链中的租户标识,决定是否触发租户粒度的限流或降级,而不是全站熔断:
- 收到
TenantDatabaseException且tenantId.equals("tenant-004")→ 触发该租户专属熔断规则(如 5 分钟内拒绝其所有写请求) - 若 cause 是
CommunicationsException(连接中断),说明可能是租户专属库宕机,立即标记其数据源为 DOWN,并通知 DBA - 若 cause 是
SQLSyntaxErrorException且表名含租户前缀(如tenant_005_config),大概率是租户配置错误,返回 400 而非 500
监控告警按异常链归因到具体租户
日志采集时,不应只记录顶层异常类名,而要递归遍历异常链,提取最外层租户异常 + 最内层 JDBC 异常,组合打标:
- ELK 或 Loki 中存为字段:
tenant_id: "tenant-007"、root_cause: "com.mysql.cj.jdbc.exceptions.CommunicationsException"、error_level: "database" - Prometheus 告警规则可设:当
tenant_database_error_total{tenant_id="tenant-007"} > 10且持续 2 分钟,仅通知该租户运维接口人 - 避免聚合统计“全部租户数据库错误总数”——这会掩盖单个租户正在经历严重故障
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











