基于 dynamic-datasource 构建企业级数据访问层:多源切换、事务一致性与读写分离实战

张开发
2026/6/10 1:32:24 15 分钟阅读
基于 dynamic-datasource 构建企业级数据访问层:多源切换、事务一致性与读写分离实战
1. 为什么需要dynamic-datasource在微服务架构盛行的今天数据库访问面临着前所未有的复杂性。我经历过一个电商项目用户中心、订单中心、商品中心各自使用独立的MySQL实例还有三个从库做读写分离。每次新增数据源都要手动修改几十处配置开发人员苦不堪言。直到发现了dynamic-datasource这个神器才真正解决了我们的痛点。dynamic-datasource-spring-boot-starter是一个基于SpringBoot的多数据源管理框架。它最大的特点是配置简单、切换灵活。相比传统方式需要手动创建多个DataSource的繁琐操作它只需要几行配置就能轻松管理数十个数据源。我在实际项目中最喜欢它的这几个特性分组管理可以把多个数据源划分为一个逻辑组比如把3个订单库标记为order组懒加载只有真正访问时才会初始化连接避免项目启动时连接数爆炸动态增减运行时可以动态添加或移除数据源特别适合云环境多种模式支持纯多库、读写分离、混合模式等场景2. 快速搭建多数据源环境2.1 基础配置实战先来看一个最简单的多数据源配置。假设我们有两个数据库db1和db2spring: datasource: dynamic: primary: master # 默认数据源 datasource: master: url: jdbc:mysql://localhost:3306/db1 username: root password: 123456 slave_1: url: jdbc:mysql://localhost:3306/db2 username: root password: 123456然后在DAO层使用DS注解即可切换数据源Repository DS(master) // 默认使用master数据源 public class UserDao { DS(slave_1) // 这个方法使用slave_1数据源 public User findById(Long id) { // 查询逻辑 } }2.2 踩坑指南这里有几个新手容易踩的坑连接池配置建议使用Druid连接池要正确配置maxActive、minIdle等参数事务问题不要在同一个方法内切换多个数据源会导致事务失效MyBatis缓存多数据源环境下要特别注意二级缓存的隔离我曾经遇到过一个性能问题在高并发场景下频繁切换数据源导致连接泄漏。后来通过调整连接池参数和增加监控才解决。建议大家在测试环境一定要做压力测试。3. 分布式事务一致性方案3.1 本地多数据源事务对于单个服务内跨多数据源的事务dynamic-datasource提供了DSTransactional注解Service public class OrderService { DSTransactional public void createOrder(Order order) { // 操作主库 orderDao.insert(order); // 操作日志库 logDao.insert(orderLog); // 如果抛出异常两个操作都会回滚 } }这个注解底层是基于JDBC的XA协议实现的。但要注意它不能和Spring的Transactional混用否则会导致事务失效。3.2 集成Seata方案对于跨服务的分布式事务推荐集成Seata。配置步骤如下添加Seata依赖在配置文件中开启Seata支持spring: datasource: dynamic: seata: true seata-mode: AT # 使用AT模式在事务方法上使用GlobalTransactionalGlobalTransactional public void crossServiceTransaction() { // 调用多个服务的操作 }在实际项目中我们遇到过一个坑Seata的undo_log表没有自动创建导致事务回滚失败。所以记得检查undo_log表是否存在。4. 高性能读写分离实现4.1 基础读写分离最简单的读写分离配置spring: datasource: dynamic: primary: master datasource: master: url: jdbc:mysql://localhost:3306/master slave_1: url: jdbc:mysql://localhost:3306/slave1 slave_2: url: jdbc:mysql://localhost:3306/slave2然后在DAO层通过注解控制Repository public class UserDao { DS(master) public void insert(User user) { // 写入操作 } DS(slave) public User findById(Long id) { // 读操作 } }4.2 自动路由方案手动写注解太麻烦可以通过AOP自动路由Aspect Component public class ReadWriteSplitAspect { Around(annotation(org.springframework.web.bind.annotation.GetMapping)) public Object readRoute(ProceedingJoinPoint joinPoint) throws Throwable { DynamicDataSourceContextHolder.push(slave); try { return joinPoint.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } Around(annotation(org.springframework.web.bind.annotation.PostMapping)) public Object writeRoute(ProceedingJoinPoint joinPoint) throws Throwable { DynamicDataSourceContextHolder.push(master); try { return joinPoint.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } }这种方案虽然方便但要注意事务上下文的问题。我们曾经因为这个问题导致生产环境数据不一致后来增加了事务ID追踪才解决。5. 高级特性与生产实践5.1 动态增减数据源有时候需要在运行时动态添加数据源比如多租户场景Autowired private DynamicDataSourceProvider provider; public void addDataSource(String name, DataSourceProperty property) { DynamicRoutingDataSource ds (DynamicRoutingDataSource) dataSource; ds.addDataSource(name, provider.createDataSource(property)); }5.2 数据源健康检查生产环境必须要有健康检查机制Scheduled(fixedRate 60000) public void checkDataSourceHealth() { DynamicRoutingDataSource ds (DynamicRoutingDataSource) dataSource; ds.getCurrentDataSources().forEach((name, dataSource) - { try (Connection conn dataSource.getConnection()) { conn.createStatement().execute(SELECT 1); } catch (Exception e) { // 告警处理 } }); }5.3 性能优化建议连接池配置根据实际负载调整maxActive、minIdle等参数监控指标暴露DataSource指标到Prometheus故障转移为主数据源配置多个备用节点在我们金融项目中通过优化连接池参数和增加从库负载均衡查询性能提升了40%。关键是要根据实际业务特点来调整参数。

更多文章