侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

设计支持动态切换和事务一致性的多数据源读写分离系统

2025-12-12 / 0 评论 / 10 阅读

题目

设计支持动态切换和事务一致性的多数据源读写分离系统

信息

  • 类型:问答
  • 难度:⭐⭐⭐

考点

抽象工厂模式,动态代理,线程局部存储,连接池管理,事务一致性

快速回答

实现要点:

  • 使用抽象工厂模式创建主从数据源及连接对象
  • 通过动态代理拦截DAO方法,根据语义自动路由数据源
  • 利用ThreadLocal保持事务内数据源一致性
  • 采用连接池(如HikariCP)管理物理连接
  • 通过Spring事务管理器扩展保证跨数据源事务一致性
## 解析

1. 核心架构设计

系统组件:

  • AbstractDataSourceFactory:抽象工厂接口,定义创建连接方法
  • MasterDataSourceFactory/SlaveDataSourceFactory:具体工厂实现
  • RoutingDataSource:动态代理类,实现数据源路由逻辑
  • DataSourceContextHolder:基于ThreadLocal的上下文管理器
  • TransactionalConnectionProxy:事务连接代理类

2. 关键代码实现

2.1 抽象工厂模式实现

// 抽象工厂接口
public interface DataSourceFactory {
    DataSource createDataSource();
}

// 主库工厂实现
public class MasterDataSourceFactory implements DataSourceFactory {
    @Override
    public DataSource createDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://master-host:3306/db");
        // ...其他主库配置
        return new HikariDataSource(config);
    }
}

// 从库工厂实现(支持多个从库)
public class SlaveDataSourceFactory implements DataSourceFactory {
    private final String slaveId;

    public SlaveDataSourceFactory(String slaveId) {
        this.slaveId = slaveId;
    }

    @Override
    public DataSource createDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://" + slaveId + ":3306/db");
        // ...负载均衡配置
        return new HikariDataSource(config);
    }
}

2.2 动态代理实现路由

public class DataSourceRouter implements InvocationHandler {
    private final Object target;

    public DataSourceRouter(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();

        // 读方法路由到从库
        if (methodName.startsWith("get") || methodName.startsWith("find")) {
            DataSourceContextHolder.setSlave();
        } 
        // 写方法路由到主库
        else {
            DataSourceContextHolder.setMaster();
        }

        try {
            return method.invoke(target, args);
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

// 使用示例
UserService proxy = (UserService) Proxy.newProxyInstance(
    ClassLoader.getSystemClassLoader(),
    new Class[]{UserService.class},
    new DataSourceRouter(realService)
);

2.3 线程级数据源上下文

public class DataSourceContextHolder {
    private static final ThreadLocal<Boolean> isMaster = ThreadLocal.withInitial(() -> false);

    public static void setMaster() {
        isMaster.set(true);
    }

    public static void setSlave() {
        isMaster.set(false);
    }

    public static boolean isMasterRoute() {
        return isMaster.get();
    }

    public static void clear() {
        isMaster.remove();
    }
}

3. 事务一致性处理

挑战:跨数据源事务需保证ACID特性

解决方案:

  • 使用TransactionalConnectionProxy包装实际Connection
  • 在事务开始时绑定主库连接,事务内强制使用主库
  • 集成Spring的AbstractPlatformTransactionManager
public class MultiDataSourceTransactionManager extends AbstractPlatformTransactionManager {

    @Override
    protected Object doGetTransaction() {
        // 创建事务对象并绑定主库连接
        TransactionContext ctx = new TransactionContext();
        ctx.bindConnection(masterDataSource.getConnection());
        return ctx;
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        // 提交时统一提交所有参与事务的连接
        getConnections().forEach(conn -> {
            try { conn.commit(); } 
            catch (SQLException e) { /* 异常处理 */ }
        });
    }
}

4. 最佳实践

  • 连接池配置:主库连接池大小应大于从库,写操作需要更多连接
  • 故障转移:从库不可用时自动降级到主库
  • 监控:实现DataSource健康检查接口,实时监控连接状态
  • 动态配置:支持运行时增减从库节点(结合ZooKeeper或Nacos)

5. 常见错误

  • 线程泄漏:未在finally块中清除ThreadLocal变量
  • 事务失效:自调用导致动态代理失效(Spring AOP的经典问题)
  • 连接泄漏:未正确关闭物理连接(建议使用try-with-resources)
  • 路由死循环:在数据源选择方法中调用其他DAO方法

6. 扩展知识

  • 柔性事务:对于跨库事务,可考虑Seata框架的AT模式
  • 读写分离延迟:主从同步延迟导致数据不一致,解决方案:
    • 关键业务强制读主库
    • 基于GTID的读写策略
  • 分库分表集成:结合ShardingSphere实现垂直/水平拆分
  • 多活架构:异地多活场景下的数据路由策略