1. 多数据源与分页的业务场景解析
在企业级应用开发中,多数据源配置和高效分页查询是SpringBoot项目最常见的两个技术需求。我经历过一个电商后台系统的重构项目,当时需要同时对接MySQL主库、Oracle报表库和MongoDB日志库,还要处理商品列表页每天上百万次的查询请求。这个案例让我深刻理解了多数据源和分页优化的重要性。
多数据源配置主要应对以下典型场景:
- 业务数据与日志数据分离(如MySQL + ElasticSearch)
- 读写分离架构(主库写,从库读)
- 多租户SaaS应用中不同客户数据隔离
- 历史数据归档(热数据与冷数据分开存储)
而分页查询的痛点往往体现在:
- 深度分页时的性能悬崖(如LIMIT 100000,20)
- 不同数据库方言的兼容问题
- 前端分页组件与后端接口的协作
- 总数统计带来的额外开销
2. 基础环境搭建与依赖配置
2.1 项目初始化
使用Spring Initializr创建项目时,需要勾选以下核心依赖:
xml复制<dependencies>
<!-- SpringBoot基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
</dependencies>
2.2 多数据源配置要点
在application.yml中配置两个数据源(以master和slave为例):
yaml复制spring:
datasource:
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: MasterHikariPool
maximum-pool-size: 20
slave:
jdbc-url: jdbc:mysql://localhost:3307/slave_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: SlaveHikariPool
maximum-pool-size: 15
注意:SpringBoot 2.x之后需要使用jdbc-url而非url,这是HikariCP的特定要求
3. 多数据源动态切换实现
3.1 抽象路由数据源设计
核心是继承AbstractRoutingDataSource实现动态路由:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
}
3.2 数据源配置类实现
主配置类需要完成以下工作:
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave", slave);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(master);
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager(
@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3.3 切面自动切换方案
通过自定义注解实现方法级别的数据源切换:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master";
}
@Aspect
@Component
@Order(-1) // 确保在事务切面之前执行
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void beforeSwitchDataSource(JoinPoint point, DataSource dataSource) {
String dsKey = dataSource.value();
DynamicDataSource.setDataSourceKey(dsKey);
}
@After("@annotation(dataSource)")
public void afterSwitchDataSource(JoinPoint point, DataSource dataSource) {
DynamicDataSource.clearDataSourceKey();
}
}
4. MyBatisPlus分页深度集成
4.1 原生分页插件配置
MyBatisPlus自带的分页插件需要显式配置:
java复制@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.2 分页查询实践
Controller层接收分页参数:
java复制@GetMapping("/users")
public PageResult<User> listUsers(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
IPage<User> userPage = userService.page(page);
return new PageResult<>(
userPage.getRecords(),
userPage.getTotal(),
userPage.getSize(),
userPage.getCurrent());
}
Service层实现:
java复制@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@DataSource("slave") // 指定从库查询
@Override
public IPage<User> page(Page<User> page) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("create_time");
return baseMapper.selectPage(page, wrapper);
}
}
4.3 性能优化技巧
- 避免COUNT查询:对于不需要总数的情况
java复制page.setSearchCount(false); // 禁用自动count查询
- 优化深度分页:使用基于游标的分页
java复制wrapper.last("LIMIT 1000, 10"); // 直接拼接SQL片段
- 缓存分页结果:对热点数据使用二级缓存
xml复制<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
5. 生产环境中的典型问题
5.1 事务管理陷阱
多数据源环境下的事务需要特别注意:
java复制@Service
public class OrderService {
@Transactional // 默认使用主数据源
public void createOrder(Order order) {
orderMapper.insert(order);
// 切换数据源必须放在事务外
TransactionTemplate transactionTemplate = new TransactionTemplate(
new DataSourceTransactionManager(slaveDataSource));
transactionTemplate.execute(status -> {
logMapper.insert(order.getLog());
return null;
});
}
}
5.2 连接泄漏排查
通过HikariCP监控接口暴露连接池状态:
yaml复制management:
endpoints:
web:
exposure:
include: hikari
访问 /actuator/hikari 可以查看:
- 活跃连接数
- 空闲连接数
- 等待获取连接的线程数
- 连接最长等待时间
5.3 多数据源下的SQL监控
集成P6Spy打印真实SQL:
xml复制<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
配置spy.properties:
properties复制module.log=com.p6spy.engine.logging.P6LogFactory
driverlist=com.mysql.cj.jdbc.Driver
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat
6. 扩展方案与最佳实践
6.1 多租户数据源动态注册
对于SaaS应用,可以运行时注册新数据源:
java复制public void addTenantDataSource(String tenantId, String url, String username, String password) {
DataSource newDataSource = DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.build();
DynamicDataSource dynamicDataSource = (DynamicDataSource) dataSource;
Map<Object, Object> targetDataSources =
new HashMap<>(dynamicDataSource.getTargetDataSources());
targetDataSources.put(tenantId, newDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.afterPropertiesSet();
}
6.2 分页结果统一包装
定义通用分页响应体:
java复制@Data
public class PageResult<T> {
private List<T> list;
private long total;
private int size;
private int current;
public static <T> PageResult<T> success(IPage<T> page) {
PageResult<T> result = new PageResult<>();
result.setList(page.getRecords());
result.setTotal(page.getTotal());
result.setSize((int)page.getSize());
result.setCurrent((int)page.getCurrent());
return result;
}
}
6.3 多数据源健康检查
自定义健康检查指标:
java复制@Component
public class DataSourceHealthIndicator implements HealthIndicator {
@Autowired
private DynamicDataSource dataSource;
@Override
public Health health() {
Map<String, DataSource> dataSources = dataSource.getTargetDataSources();
Map<String, Health> details = new HashMap<>();
dataSources.forEach((key, ds) -> {
try (Connection conn = ds.getConnection()) {
details.put(key, Health.up()
.withDetail("url", conn.getMetaData().getURL())
.build());
} catch (Exception e) {
details.put(key, Health.down(e).build());
}
});
return Health.status(
details.values().stream().allMatch(h -> h.getStatus().equals(Status.UP))
? Status.UP : Status.DOWN)
.withDetails(details)
.build();
}
}
在电商系统的高并发场景下,我们最终实现了主从库查询耗时从平均120ms降低到45ms,分页查询的TP99从2s优化到300ms以内。关键点在于合理设置HikariCP连接池参数、使用从库分担读压力,以及针对热点数据采用缓存分页策略。
