'# Java多数据源几种实现方式以及Demo
一、背景与问题
在分布式系统中,业务系统常需要访问多个独立的数据源(如多个数据库、多个云数据库实例、不同的数据存储系统等)。这种需求在微服务架构中尤为常见,例如:
- 电商平台需要分离订单数据库和库存数据库
- 金融系统需要隔离核心交易数据与日志数据
- 分布式系统需要访问不同地域的数据库实例
传统的单数据源模式无法满足这种需求,需要通过多数据源技术实现动态切换。但多数据源技术存在诸多挑战:
- 数据源路由逻辑的实现
- 事务管理的复杂性
- 性能瓶颈的控制
- 安全隔离的保障
- 系统维护的复杂度
本文将深入探讨Java中实现多数据源的多种方式,结合实际案例分析其原理、适用场景和常见问题。
二、基本原理
Java多数据源技术的核心原理是通过数据源路由机制,在运行时根据业务需求动态选择目标数据源。其核心要素包括:
- 数据源配置:定义多个数据源的连接信息
- 路由策略:决定何时、如何选择数据源
- 事务管理:确保跨数据源事务的一致性
- 连接池管理:控制数据库连接资源
在Spring框架中,AbstractRoutingDataSource是实现多数据源的核心组件,它通过抽象方法determineCurrentLookupKey()实现路由逻辑,支持通过ThreadLocal变量、请求参数、业务标识等方式动态选择数据源。
三、环境准备
# application.yml
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/slave_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver四、核心实现
方式一:AbstractRoutingDataSource(推荐)
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource() {
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
}public class RoutingDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}关键代码解释:
AbstractRoutingDataSource通过determineCurrentLookupKey()方法实现路由逻辑setTargetDataSources配置多个数据源setDataSource()方法通过ThreadLocal保存当前数据源标识- 在业务代码中通过
setDataSource("slave")切换数据源
方式二:多数据源手动配置
@Configuration
public class MultiDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}@Service
public class OrderService {
@Autowired
private JdbcTemplate masterJdbcTemplate;
@Autowired
private JdbcTemplate slaveJdbcTemplate;
public void processOrder(String orderId) {
// 写入主库
masterJdbcTemplate.update("INSERT INTO orders...", ...);
// 查询从库
List<Order> orders = slaveJdbcTemplate.query("SELECT * FROM orders...", ...);
}
}关键代码解释:
- 需要同时配置多个
JdbcTemplate实例 - 通过
@Qualifier注解区分不同数据源 - 事务管理需要特别注意数据源一致性
方式三:AOP动态代理
@Aspect
@Component
public class DataSourceAspect {
@Around("execution(* com.example.service..*.*(..))")
public Object switchDataSource(ProceedingJoinPoint pjp) throws Throwable {
String dataSource = getDataSourceFromContext();
if (dataSource == null) {
dataSource = "master";
}
try {
RoutingDataSourceContextHolder.setDataSource(dataSource);
return pjp.proceed();
} finally {
RoutingDataSourceContextHolder.clearDataSource();
}
}
private String getDataSourceFromContext() {
// 从请求参数、业务标识等获取数据源标识
return "slave";
}
}关键代码解释:
- 使用AOP拦截业务方法
- 在方法执行前后动态切换数据源
- 需要处理事务管理器的兼容性问题
五、完整案例
电商系统多数据源案例
@Entity
public class Order {
@Id
private Long id;
private String orderNo;
private BigDecimal amount;
// 省略getter/setter
}public interface OrderMapper {
@Insert("INSERT INTO orders (order_no, amount) VALUES (#{order.orderNo}, #{order.amount})")
void insertOrder(Order order);
}@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void createOrder(Order order) {
// 写入主库
orderMapper.insertOrder(order);
// 查询从库
List<Order> orders = queryOrdersFromSlave();
}
private List<Order> queryOrdersFromSlave() {
// 使用从库数据源
return jdbcTemplateSlave.query("SELECT * FROM orders", ...);
}
}完整案例说明:
- 使用AbstractRoutingDataSource配置主从数据源
- 通过ThreadLocal保存当前数据源标识
- 在业务方法中动态切换数据源
- 使用
@Transactional管理跨数据源事务
六、源码解析
以AbstractRoutingDataSource为核心进行源码分析:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements DataSource, Serializable {
protected final Map<Object, Object> targetDataSources = new LinkedHashMap<>();
protected Object defaultTargetDataSource;
protected abstract Object determineCurrentLookupKey();
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
// 实现数据源路由逻辑
Object lookupKey = determineCurrentLookupKey();
DataSource targetDataSource = (DataSource) targetDataSources.get(lookupKey);
if (targetDataSource == null) {
targetDataSource = defaultTargetDataSource;
}
return targetDataSource;
}
}关键源码分析:
determineCurrentLookupKey()方法决定当前数据源- 使用
ThreadLocal保存上下文信息 - 通过
targetDataSources映射不同数据源 - 支持默认数据源配置
七、进阶使用
- 动态数据源切换:根据请求参数动态选择数据源
- 多租户支持:根据租户ID自动路由到对应数据库
- 读写分离:根据SQL类型自动路由到主库或从库
- 数据库分片:根据业务逻辑字段进行分片路由
public class ShardingDataSource {
public String getShardingKey(String businessKey) {
// 根据业务键生成分片标识
return businessKey.hashCode() % 2 == 0 ? "master" : "slave";
}
}八、性能与工程实践
性能优化建议
- 连接池配置:使用HikariCP等高性能连接池
- 缓存机制:对频繁查询的数据源进行缓存
- 异步处理:对非关键业务操作使用异步处理
- SQL优化:对关键SQL进行索引优化
安全风险
- SQL注入:使用预编译语句防止注入攻击
- 数据隔离:确保不同数据源的SQL语句不会相互干扰
- 权限控制:对不同数据源的访问进行权限控制
- 日志安全:避免在日志中暴露敏感数据源信息
九、常见问题与踩坑
常见错误及解决办法
数据源配置错误:
- 问题:
targetDataSources未正确配置 - 解决:检查配置文件和
AbstractRoutingDataSource的配置
- 问题:
事务管理问题:
- 问题:跨数据源事务失效
- 解决:使用
@Transactional注解并配置正确的事务管理器
连接池耗尽:
- 问题:高并发下连接池耗尽
- 解决:增加连接池最大连接数,优化SQL性能
数据源切换失效:
- 问题:
setDataSource()未正确调用 - 解决:确保在业务方法调用前设置数据源
- 问题:
缓存失效:
- 问题:缓存数据未及时更新
- 解决:设置合理的缓存过期时间和更新策略
十、最佳实践
推荐方案:
- 使用
AbstractRoutingDataSource实现动态路由 - 通过
ThreadLocal保存数据源上下文 - 使用
@Transactional管理事务 - 采用HikariCP作为连接池
- 使用
适用场景:
- 复杂的多数据源场景(如读写分离、分库分表)
- 需要精细控制数据源切换的场景
- 对性能要求较高的系统
注意事项:
- 避免在事务中动态切换数据源
- 确保所有数据源配置正确
- 定期监控数据库连接池状态
- 对关键数据源进行备份和监控
十一、总结
Java多数据源技术是构建分布式系统的重要基础,不同实现方式各有优劣。AbstractRoutingDataSource提供了灵活的数据源路由机制,适合复杂场景;手动配置适合简单场景;AOP方式适合需要统一管理的场景。在实际开发中,需要根据业务需求选择合适的实现方式,同时注意性能优化、安全控制和事务管理。通过合理的设计和实践,可以有效应对多数据源带来的挑战,构建稳定可靠的分布式系统。