题目
设计支持动态切换和事务一致性的多数据源读写分离系统
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
抽象工厂模式,动态代理,线程局部存储,连接池管理,事务一致性
快速回答
实现要点:
- 使用抽象工厂模式创建主从数据源及连接对象
- 通过动态代理拦截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实现垂直/水平拆分
- 多活架构:异地多活场景下的数据路由策略