多数据源那套代码你写了三遍——你其实一直在手动实现桥接模式

去年接手一个老项目,数据源切换逻辑散落在 40 多个 Service 里。每个方法开头都是同一段话:

java 复制代码
if ("slave".equals(DataSourceContextHolder.getDataSourceType())) {
    // 从库逻辑
} else {
    // 主库逻辑  
}

我数了一下,同样的 if-else 出现了 47 次。读写分离搞成这样,不是架构问题,是连 Spring 给你铺好的桥接模式都没看懂。

AbstractRoutingDataSource 就是个桥接模式

Spring 的 AbstractRoutingDataSource 只有 100 多行代码,但它把桥接模式讲得比任何教科书都清楚:

java 复制代码
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
    private Map<Object, DataSource> targetDataSources;
    private DataSource defaultTargetDataSource;
    
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }
    
    protected DataSource determineTargetDataSource() {
        Object lookupKey = determineCurrentLookupKey();
        DataSource ds = this.targetDataSources.get(lookupKey);
        return ds != null ? ds : this.defaultTargetDataSource;
    }
    
    protected abstract Object determineCurrentLookupKey();
}

桥接模式的精髓是「把抽象和实现拆成两个独立维度」。在这里,抽象维度 是「选哪个数据源」------由 determineCurrentLookupKey() 决定;实现维度 是「具体的数据源连接」------由 targetDataSources 这个 Map 存储。

你只需要继承这个类,重写一个方法:

java 复制代码
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

抽象层 3 行代码,实现层全在配置里。这就是桥接模式的威力------两个维度的变化不会互相污染 。你加一个新从库,只改配置不改代码;你换一种路由策略,只改 determineCurrentLookupKey() 不改数据源定义。

那 47 个 if-else 到底错在哪

回到开头那段散落在 40 多个 Service 里的代码。它的问题是:

把桥接模式的「桥」拆了,直接在调用方写死选择逻辑。

桥接模式要求调用方只依赖抽象(DataSource 接口),不知道有「主库」「从库」这回事。但 if ("slave".equals(...)) 这种写法等于在 Service 层把实现维度暴露了------每次加一个数据源角色,所有 Service 都要改。

我见过最离谱的一个案例:团队加了一个「归档库」,结果在 83 个地方加了 else if ("archive".equals(...))。Code Review 的时候没人觉得有问题,因为「大家都在这么写」。

实际上,你以为你写的是业务代码,其实你每写一次这种 if-else,都在手动实现一个蹩脚的桥接模式------而且每次实现的都比上一次更乱。

读写分离不是唯一的桥接场景

多租户数据源隔离:

java 复制代码
public class TenantAwareDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getCurrentTenant();
    }
}

租户 A 的请求自动路由到数据源 A,租户 B 到数据源 B。新接入一个租户,只需要在配置里加一个数据源,代码零改动。

分库分表前的灰度切换:

java 复制代码
public class ShardingGrayDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 灰度规则:userId 尾号 0-3 走新库,4-9 走老库
        long userId = UserContext.getCurrentUserId();
        return userId % 10 < 4 ? "new_" + (userId % 4) : "old";
    }
}

桥接模式把「怎么选」和「选什么」解耦了。灰度策略再怎么变,数据源的定义不变;数据源再怎么加,灰度策略的逻辑不受影响。这就是两个独立变化维度的正交。

真有人把桥接模式写反了

桥接模式最容易踩的坑是把两个维度的耦合反过来写。

错误写法:

java 复制代码
public class MasterDataSource implements DataSource {
    private HikariDataSource delegate;
    // 主库特化的实现
}

public class SlaveDataSource implements DataSource {
    private HikariDataSource delegate;
    // 从库特化的实现
}

// 然后在配置里写:
@Bean("masterDataSource")
public DataSource masterDataSource() { ... }
@Bean("slaveDataSource") 
public DataSource slaveDataSource() { ... }

// Service 里:
@Autowired @Qualifier("masterDataSource")
private DataSource masterDataSource;
@Autowired @Qualifier("slaveDataSource")
private DataSource slaveDataSource;

这叫什么?这叫在 Bean 级别把实现硬编码了。每个 Service 要知道所有数据源的存在,选择逻辑又回到了调用方。桥接模式被你写成了「在抽象层重新定义实现细节」。

正确的姿势是:调用方只注入一个 DataSource,桥在框架层帮你做完选择。

java 复制代码
@Autowired
private DataSource dataSource; // 就这一个,别写 @Qualifier

一个容易忽视的并发问题

AbstractRoutingDataSource 的 determineCurrentLookupKey() 通常依赖 ThreadLocal:

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSourceType(String type) { contextHolder.set(type); }
    public static String getDataSourceType() { return contextHolder.get(); }
    public static void clear() { contextHolder.remove(); }
}

如果你在异步方法里忘了传递 ThreadLocal(比如用了 @Async),子线程拿到的是 null,路由到默认数据源。读操作变成了写,写操作变成了读------这种 Bug 不会抛异常,测试环境可能三个月才发现。

java 复制代码
@Async
public void sendNotification(Long orderId) {
    // ThreadLocal 丢了!这里默认走到主库,你以为是读从库
    Order order = orderService.getById(orderId);  
}

解法:用 InheritableThreadLocal 或者 TransmittableThreadLocal(阿里的开源库),或者明确在异步方法入口设置数据源标识。

桥接模式和策略模式的区别

很多人分不清这两个。区别很简单:

  • 策略模式是「算法族,让它们可以互相替换」------比如支付策略(微信/支付宝/银行卡),调用方主动选
  • 桥接模式是「抽象和实现独立变化」------比如数据源路由,调用方不感知选择逻辑

AbstractRoutingDataSource 内部确实用了策略模式的思路(Map 里存多个实现,按 key 选),但整体结构是桥接模式------因为目的不是「替换算法」,而是「让抽象维度和实现维度独立演进」。

这点区别面试不会被问到,但实际做架构的时候很重要。该用桥接的地方用了策略,会导致调用方承担了本不该承担的选择责任。

关键代码骨架

最简洁的桥接模式多数据源配置:

java 复制代码
@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        
        HikariDataSource master = createDataSource("jdbc:mysql://master:3306/db");
        HikariDataSource slave1 = createDataSource("jdbc:mysql://slave1:3306/db");
        HikariDataSource slave2 = createDataSource("jdbc:mysql://slave2:3306/db");
        
        targetDataSources.put("master", master);
        targetDataSources.put("slave1", slave1);
        targetDataSources.put("slave2", slave2);
        
        DynamicDataSource routingDS = new DynamicDataSource();
        routingDS.setDefaultTargetDataSource(master);
        routingDS.setTargetDataSources(targetDataSources);
        return routingDS;
    }
    
    // 读写分离切面:读方法走从库,写方法走主库
    @Around("@annotation(com.example.ReadOnly)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        DataSourceContextHolder.setDataSourceType("slave1");
        try {
            return pjp.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

整个读写分离骨架不到 50 行代码。桥接模式帮你把抽象做了,AbstractRoutingDataSource 帮你把实现做了,你只需要填配置。


我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,每个模式都用漫画还原真实踩坑场景。桥接模式这章我刚画完------卡皮巴拉在多数据源迷宫里找路。感兴趣的可以微信搜一下「爪爪代码冒险记」。

相关推荐
专业程序开发源1 小时前
springboot生命故事书制作小程序65290-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·mysql·小程序·课程设计
AINative软件工程1 小时前
LLM Prompt Registry 工程实践:集中管理 Prompt,让模型调用不再散落在代码各处
后端·python·llm
IT_陈寒1 小时前
SpringBoot自动配置的坑我帮你踩过了
前端·人工智能·后端
vx_Biye_Design1 小时前
springboot角色扮演服务平台65161-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·spring·课程设计
vx_Biye_Design2 小时前
springboot咖啡厅顾客点单管理系统12080-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
专业程序开发源2 小时前
flask动漫推荐系统32319-计算机课程设计、毕业设计
java·spring boot·后端·django·flask·php·课程设计
FYKJ_20102 小时前
springboot手工蜀绣在线销售系统60947-计算机课程设计、毕业设计
java·spring boot·后端·python·mysql·spark·课程设计
vx_Biye_Design2 小时前
springboot老年人用药智能管理系统38605-计算机课程设计、毕业设计
java·spring boot·后端·python·django·课程设计·express
苏离~Hack2 小时前
惠民云平台开发实战:基于 Vue3 + Django 的社区服务系统设计与实现
后端·python·django
明月_清风11 小时前
Muse 登顶 App Store 第一,SDK 直接开源:AI Agent 开始进入下一个阶段
人工智能·后端