1. 多数据源到底在解决什么问题
做后端开发的朋友大概率都遇到过这样的场景:项目一开始只有一个数据库,所有业务读写都走它,跑得好好的。突然有一天产品经理跑过来说,咱们要接一个历史遗留系统的库,或者运营那边要单独拉一个报表库出来,又或者主库压力太大了需要做读写分离。这时候你打开项目一看,所有Mapper都绑在同一个SqlSessionFactory上,动一个地方牵一发动全身。
这就是多数据源要解决的核心问题:让不同的业务模块或不同的操作类型,走不同的数据库连接。
我见过不少团队在这个环节翻车。有人直接复制一套完整的Mybatis配置,改个包名就完事,结果两个数据源互相污染,事务管理器打架,启动就报错。也有人用AOP切面硬切,切点写得太宽,把不该切的也切过去了,导致写操作跑到了从库上,数据一致性直接崩掉。
这篇文章要聊的,就是怎么用SpringBoot配合Mybatis,干净利落地把多数据源配好。我会把读写分离作为主要场景来展开,因为这是最常见也最典型的用法。但思路是通用的,你换成多个业务库、多个租户库,逻辑完全一样。
适合谁看:已经用过SpringBoot和Mybatis,但对多数据源配置还没摸清门道的开发者。如果你连Mybatis的基本配置都没写过,建议先把单数据源跑通再来看这篇。
读完你能得到什么:一套可以直接抄的配置方案,理解每个配置项为什么这么写,知道哪些坑一定会踩以及怎么绕过去。
2. 整体设计思路与方案选型
2.1 为什么不用现成的多数据源中间件
市面上有一些现成的多数据源组件,比如动态数据源路由框架。它们确实能省事,加个注解就切过去了。但我不推荐在读写分离这个场景下直接用,原因有两个。
第一,读写分离的切换逻辑应该是确定的、可预期的。写操作走主库,读操作走从库,这个规则非常清晰。用中间件做动态路由,反而引入了一层不确定性——万一路由规则配错了,写请求跑到从库,排查起来非常痛苦。
第二,中间件会掩盖底层细节。当你需要做一些精细控制的时候,比如某个查询必须走主库(因为刚写完立刻要读,从库可能有延迟),中间件提供的扩展点往往不够灵活。自己配的多数据源,想怎么改就怎么改。
当然,如果你面对的是十几个数据源的复杂场景,那确实需要考虑中间件。但读写分离这种两数据源的情况,手写配置是最稳的。
2.2 核心设计原则:一个数据源一套独立配置
多数据源配置最容易犯的错误,就是让两个数据源共享某些组件。比如共用一个DataSource,或者共用一个SqlSessionFactory。这样做的后果是,你以为是两个数据源,实际上底层还是同一个连接池,切换根本没生效。
正确的做法是:每个数据源拥有完全独立的DataSource、SqlSessionFactory、SqlSessionTemplate和事务管理器。它们之间不共享任何实例,通过包路径来区分各自的Mapper接口。
打个比方,这就像两个独立的厨房。主库厨房负责炒菜(写),从库厨房负责摆盘(读)。两个厨房各有自己的灶台、锅具、食材,互不干扰。你不能让两个厨房共用一个灶台,那样菜就串味了。
2.3 读写分离的切换策略选择
读写分离的切换方式主要有三种:
| 切换方式 | 实现难度 | 灵活性 | 适用场景 |
|---|---|---|---|
| 包路径区分 | 低 | 中 | 读写Mapper天然分离的项目 |
| AOP注解切换 | 中 | 高 | 需要细粒度控制的方法级切换 |
| 方法名前缀匹配 | 低 | 低 | 命名规范严格统一的团队 |
我推荐包路径区分作为基础方案。把读操作的Mapper放在一个包下,写操作的Mapper放在另一个包下,各自绑定不同的数据源。这种方式最直观,出问题最容易排查。
如果项目里读写操作混在同一个Mapper里,那就需要配合AOP注解来做方法级切换。但这种情况我建议先重构代码,把读写Mapper拆开,而不是用AOP去硬切。
2.4 事务管理的边界问题
多数据源场景下,事务管理器必须和SqlSessionFactory绑定。也就是说,主库的事务管理器只管主库的SqlSessionFactory,从库的事务管理器只管从库的。
这里有个关键点:跨数据源的事务是不支持的。你不能在一个事务里同时写主库和读从库,然后期望它们一起提交或回滚。这是分布式事务的范畴,需要额外的框架支持。
在实际操作中,我的建议是:写操作的事务只走主库,读操作不开事务或者只开只读事务。这样边界清晰,不会出现事务管理器找不到对应数据源的情况。
3. 核心配置细节与实操要点
3.1 依赖引入与版本选择
先看依赖。SpringBoot项目里,Mybatis的starter是必须的,数据库驱动按你实际用的选。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>版本选择上有个经验:Mybatis starter的版本要和SpringBoot版本匹配。SpringBoot 2.7.x配Mybatis starter 2.3.x,SpringBoot 3.x配Mybatis starter 3.x。版本不匹配最常见的表现是启动时报NoSuchMethodError,找半天找不到原因。
数据库驱动这块,MySQL 8.x用com.mysql.cj.jdbc.Driver,5.x用com.mysql.jdbc.Driver。这个细节很多人会忽略,配错了启动直接报驱动加载失败。
3.2 配置文件的结构设计
配置文件我建议用application.yml,结构清晰,层级分明。核心配置如下:
spring: datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://主库地址:3306/数据库名?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: 主库用户名 password: 主库密码 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 slave: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://从库地址:3306/数据库名?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: 从库用户名 password: 从库密码 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000这里有几个关键点需要展开说。
第一,jdbc-url而不是url。当你在SpringBoot里手动创建DataSource Bean的时候,必须用jdbc-url。用url的话,HikariCP会报错说找不到这个属性。这是因为SpringBoot的自动配置和手动配置对属性名的要求不一样,很多人在这里卡住。
第二,连接池参数要分开配。主库和从库的负载特征不一样,主库写多,从库读多。从库的maximum-pool-size可以适当调大,因为读请求通常并发更高。但也不要无脑调大,连接数超过数据库的max_connections限制,反而会导致连接失败。
第三,serverTimezone必须配。MySQL 8.x的驱动如果不配时区,会报时区错误。Asia/Shanghai是东八区,国内项目用这个就行。
3.3 主数据源的配置类写法
配置类是多数据源的核心。先看主数据源的:
@Configuration @MapperScan( basePackages = "com.example.project.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory" ) public class MasterDataSourceConfig { @Primary @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Primary @Bean(name = "masterSqlSessionFactory") public SqlSessionFactory masterSqlSessionFactory( @Qualifier("masterDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/master/*.xml") ); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); bean.setConfiguration(configuration); return bean.getObject(); } @Primary @Bean(name = "masterSqlSessionTemplate") public SqlSessionTemplate masterSqlSessionTemplate( @Qualifier("masterSqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } @Primary @Bean(name = "masterTransactionManager") public DataSourceTransactionManager masterTransactionManager( @Qualifier("masterDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }这段代码里有几个地方需要特别注意。
@Primary注解不能省。Spring容器里有两个DataSource Bean的时候,如果不指定哪个是主,自动注入会报错。加上@Primary告诉Spring,默认用这个。
@MapperScan的sqlSessionFactoryRef必须指定。如果不指定,Mybatis会去找默认的SqlSessionFactory,在多数据源环境下就会找错。
basePackages的路径要精确。主库的Mapper放在mapper.master包下,从库的放在mapper.slave包下。包名不要重叠,否则一个Mapper会被两个数据源都扫描到,行为不可预期。
mapperLocations的路径也要分开。XML文件和Mapper接口的包路径最好保持一致的结构,这样维护起来清晰。
3.4 从数据源的配置类写法
从数据源的配置和主数据源结构一样,但有几个关键区别:
@Configuration @MapperScan( basePackages = "com.example.project.mapper.slave", sqlSessionFactoryRef = "slaveSqlSessionFactory" ) public class SlaveDataSourceConfig { @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "slaveSqlSessionFactory") public SqlSessionFactory slaveSqlSessionFactory( @Qualifier("slaveDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/slave/*.xml") ); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); bean.setConfiguration(configuration); return bean.getObject(); } @Bean(name = "slaveSqlSessionTemplate") public SqlSessionTemplate slaveSqlSessionTemplate( @Qualifier("slaveSqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } @Bean(name = "slaveTransactionManager") public DataSourceTransactionManager slaveTransactionManager( @Qualifier("slaveDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }从数据源不加@Primary。这是和主数据源最大的区别。不加@Primary意味着它不是默认的,只有在明确指定的时候才会被注入。
从库的事务管理器通常用不上。因为读操作一般不开事务,或者只开只读事务。但为了配置完整性,还是把它创建出来,万一有需要呢。
从库的cacheEnabled建议设为false。Mybatis的二级缓存是SqlSessionFactory级别的,两个数据源各有一份缓存,如果开启的话,主库更新了数据,从库的缓存不会自动失效,会读到脏数据。关掉最省心。
3.5 包结构规划建议
包结构规划看起来是小事,但直接影响后期的维护成本。我推荐的结构是这样的:
com.example.project ├── config │ ├── MasterDataSourceConfig.java │ └── SlaveDataSourceConfig.java ├── mapper │ ├── master │ │ ├── UserWriteMapper.java │ │ └── OrderWriteMapper.java │ └── slave │ ├── UserReadMapper.java │ └── OrderReadMapper.java └── service ├── UserService.java └── OrderService.javaMapper接口按读写分开放。写操作的Mapper放mapper.master,读操作的放mapper.slave。Service层调用的时候,根据业务需要注入对应的Mapper。
XML文件路径和Mapper接口路径对应。resources/mapper/master/下放主库的XML,resources/mapper/slave/下放从库的XML。这样找文件的时候一目了然。
不要在Service层做数据源切换。Service层只负责业务逻辑,它不关心数据从哪来。数据源的切换在Mapper层就决定了,Service层只管调用。
4. 完整实操流程与关键环节
4.1 环境准备与项目初始化
假设你已经有一个SpringBoot项目,现在要给它加上多数据源。第一步是确认现有的单数据源配置,把原来的DataSource自动配置排除掉。
在启动类上加:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }为什么要排除自动配置:SpringBoot默认会自动创建一个DataSource,如果我们手动创建了两个,加上自动配置的那个就是三个,容器会混乱。排除掉自动配置,完全由我们自己控制。
这一步很多人会忘,结果启动时报DataSource注入冲突,找半天原因。
4.2 主从数据源的创建与验证
配置类写完之后,怎么验证数据源真的生效了?我常用的方法是写一个简单的测试接口:
@RestController public class DataSourceCheckController { @Autowired @Qualifier("masterDataSource") private DataSource masterDataSource; @Autowired @Qualifier("slaveDataSource") private DataSource slaveDataSource; @GetMapping("/check/datasource") public String checkDataSource() throws SQLException { try (Connection masterConn = masterDataSource.getConnection(); Connection slaveConn = slaveDataSource.getConnection()) { String masterUrl = masterConn.getMetaData().getURL(); String slaveUrl = slaveConn.getMetaData().getURL(); return "master: " + masterUrl + "\nslave: " + slaveUrl; } } }访问这个接口,如果返回的两个URL不一样,说明数据源配置成功了。如果返回一样的URL,那说明两个数据源指向了同一个库,配置有问题。
这个验证步骤非常关键。我见过有人配了半天,以为生效了,结果两个数据源连的是同一个库,读写分离根本没起作用。
4.3 Mapper层的读写分离实现
数据源配好之后,接下来是Mapper层的实现。先看写操作的Mapper:
@Mapper public interface UserWriteMapper { int insertUser(User user); int updateUser(User user); int deleteUserById(Long id); }对应的XML:
<mapper namespace="com.example.project.mapper.master.UserWriteMapper"> <insert id="insertUser" parameterType="com.example.project.entity.User"> INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email}) </insert> <update id="updateUser" parameterType="com.example.project.entity.User"> UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id} </update> <delete id="deleteUserById"> DELETE FROM user WHERE id = #{id} </delete> </mapper>再看读操作的Mapper:
@Mapper public interface UserReadMapper { User selectUserById(Long id); List<User> selectUserList(); }对应的XML:
<mapper namespace="com.example.project.mapper.slave.UserReadMapper"> <select id="selectUserById" resultType="com.example.project.entity.User"> SELECT * FROM user WHERE id = #{id} </select> <select id="selectUserList" resultType="com.example.project.entity.User"> SELECT * FROM user </select> </mapper>关键点:写Mapper的XML放在mapper/master/下,读Mapper的XML放在mapper/slave/下。两个XML的namespace分别对应各自的Mapper接口全限定名。
4.4 Service层的调用方式
Service层注入Mapper的时候,直接按类型注入就行,因为两个Mapper接口是不同的类型,不会冲突:
@Service public class UserService { @Autowired private UserWriteMapper userWriteMapper; @Autowired private UserReadMapper userReadMapper; @Transactional(transactionManager = "masterTransactionManager") public void createUser(User user) { userWriteMapper.insertUser(user); } public User getUserById(Long id) { return userReadMapper.selectUserById(id); } public List<User> getUserList() { return userReadMapper.selectUserList(); } }写操作加@Transactional并指定事务管理器。transactionManager = "masterTransactionManager"明确告诉Spring用主库的事务管理器。不指定的话,Spring会去找默认的事务管理器,在多数据源环境下可能找错。
读操作不加事务。读操作本身不需要事务,加了反而多一层开销。如果非要加,用@Transactional(readOnly = true),但还是要指定事务管理器。
4.5 读写分离后的数据一致性处理
读写分离有一个经典问题:写完主库立刻读从库,可能读不到刚写的数据。因为主从同步有延迟,从库的数据可能还是旧的。
处理这个问题有几种策略:
策略一:写后读走主库。在写完之后的短时间内,强制读主库。实现方式是在读Mapper里加一个走主库的方法,或者用AOP注解标记。
策略二:关键业务不走读写分离。比如支付、订单创建这种对一致性要求极高的场景,读写都走主库。读写分离只用在报表查询、列表展示这种对延迟不敏感的场景。
策略三:使用半同步复制。在数据库层面配置半同步复制,确保主库提交的事务至少同步到一个从库。但这会增加写操作的延迟。
我个人的选择是策略一和策略二结合。核心业务读写都走主库,非核心的查询走从库。这样既减轻了主库压力,又保证了关键业务的一致性。
4.6 连接池参数调优实操
连接池参数不是拍脑袋定的,要根据实际负载来算。以HikariCP为例:
maximum-pool-size的计算:假设你的数据库max_connections是500,有3个应用实例,每个实例配了两个数据源。那么每个数据源的连接数上限是500 / (3 * 2) ≈ 83。但实际不需要这么多,一般按CPU核心数 * 2 + 磁盘数来估算。4核8G的机器,配20个连接足够了。
connection-timeout的设置:默认30秒。如果数据库响应慢,可以适当调大。但不要超过60秒,否则请求会堆积。
idle-timeout和max-lifetime:idle-timeout建议设10分钟,max-lifetime建议设30分钟。max-lifetime要比数据库的wait_timeout小,否则连接会被数据库主动断开,应用这边还以为连接是好的,用的时候报错。
hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1connection-test-query:MySQL 8.x的驱动支持JDBC4的isValid()方法,可以不配这个。但如果用的是老版本驱动,配一个SELECT 1更保险。
5. 常见问题与排查技巧实录
5.1 启动报错:DataSource注入冲突
现象:启动时报NoUniqueBeanDefinitionException,说找到了多个DataSource Bean。
原因:没有排除SpringBoot的DataSource自动配置,或者两个DataSource都没有加@Primary。
解决:在启动类上加@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}),并确保主数据源加了@Primary。
5.2 Mapper扫描不到:Invalid bound statement
现象:调用Mapper方法时报Invalid bound statement (not found)。
原因:通常是@MapperScan的basePackages路径写错了,或者XML文件的路径和Mapper接口的namespace不匹配。
排查步骤:
- 检查
@MapperScan的basePackages是否包含了Mapper接口所在的包。 - 检查XML文件的namespace是否等于Mapper接口的全限定名。
- 检查
mapperLocations的路径是否能匹配到XML文件。 - 检查XML文件中的
<select>、<insert>等标签的id是否和Mapper接口的方法名一致。
5.3 事务不生效:写操作没有回滚
现象:写操作抛异常了,但数据没有回滚。
原因:事务管理器指定错了,或者@Transactional注解用在了非public方法上。
排查步骤:
- 检查
@Transactional的transactionManager属性是否指定了正确的事务管理器。 - 检查方法是否是public的。Spring的AOP代理只对public方法生效。
- 检查是否在同一个类内部调用了带
@Transactional的方法。内部调用不走代理,事务不生效。
5.4 读写分离失效:读操作走了主库
现象:配置了读写分离,但读操作还是走主库。
原因:读Mapper被扫描到了主数据源的@MapperScan范围内,或者读Mapper的包路径和写Mapper的包路径重叠了。
排查步骤:
- 检查两个
@MapperScan的basePackages是否有重叠。 - 检查读Mapper的接口是否放在了
mapper.master包下。 - 在数据库层面开启慢查询日志,观察读操作的SQL是否出现在主库的日志里。
5.5 连接池耗尽:Connection is not available
现象:请求量大的时候报Connection is not available, request timed out。
原因:连接池的maximum-pool-size设小了,或者有连接泄漏。
排查步骤:
- 检查
maximum-pool-size是否够用。可以临时调大观察是否缓解。 - 检查代码里是否有手动获取Connection但没有关闭的地方。
- 检查是否有长事务占着连接不放。长事务会一直占用连接,导致其他请求拿不到连接。
- 开启HikariCP的泄漏检测:
spring.datasource.master.hikari.leak-detection-threshold=60000,超过60秒没归还的连接会打印堆栈。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 快速排查方法 |
|---|---|---|
| 启动报DataSource冲突 | 未排除自动配置或未加@Primary | 检查启动类exclude和@Primary注解 |
| Invalid bound statement | MapperScan路径错或XML namespace不匹配 | 核对包路径和namespace |
| 事务不回滚 | 事务管理器指定错或方法非public | 检查@Transactional配置和方法修饰符 |
| 读操作走主库 | Mapper包路径重叠 | 检查两个MapperScan的basePackages |
| 连接池耗尽 | 连接数不够或连接泄漏 | 调大连接数并开启泄漏检测 |
| 从库读到旧数据 | 主从同步延迟 | 关键业务走主库或加缓存 |
5.7 几个我踩过的坑
坑一:@ConfigurationProperties的prefix写错。spring.datasource.master和spring.datasource.master.hikari是两个不同的层级。如果prefix写成spring.datasource.master.hikari,那jdbc-url就绑不上了。正确的写法是prefix写到spring.datasource.master,HikariCP的参数会自动绑定。
坑二:两个数据源用了同一个连接池名字。HikariCP默认的连接池名字是HikariPool-1、HikariPool-2这样自动生成的。但如果手动指定了pool-name,两个数据源用了同一个名字,日志里就分不清是哪个数据源的连接了。建议给每个数据源指定不同的pool-name。
坑三:从库的readOnly没设。从库的连接建议设为只读,spring.datasource.slave.hikari.read-only=true。这样即使代码里不小心写了写操作,数据库层面也会拒绝,多一层保护。
坑四:忘了配serverTimezone。MySQL 8.x的驱动不配时区会报错,而且报错信息不太直观,容易让人以为是别的问题。serverTimezone=Asia/Shanghai是标配。
坑五:主从库的数据库名不一样。有些团队主库和从库的数据库名不同,但表结构一样。这种情况下,jdbc-url里的数据库名要分别写对。如果写错了,会报表不存在,但表其实是在另一个库名下的。
6. 进阶扩展与生产环境建议
6.1 多数据源的监控
生产环境上,两个数据源的连接池状态需要分别监控。SpringBoot Actuator可以暴露HikariCP的指标:
management: endpoints: web: exposure: include: health,metrics,hikaricp访问/actuator/metrics/hikaricp.connections.active可以看到活跃连接数。但默认只暴露一个数据源的指标,多数据源需要自定义Metrics注册。
更简单的做法是写一个定时任务,定期打印两个连接池的状态:
@Component public class DataSourceMonitor { @Autowired @Qualifier("masterDataSource") private DataSource masterDataSource; @Autowired @Qualifier("slaveDataSource") private DataSource slaveDataSource; @Scheduled(fixedRate = 60000) public void monitor() { printPoolStatus("master", masterDataSource); printPoolStatus("slave", slaveDataSource); } private void printPoolStatus(String name, DataSource dataSource) { if (dataSource instanceof HikariDataSource) { HikariDataSource hikari = (HikariDataSource) dataSource; System.out.println(name + " pool - active: " + hikari.getHikariPoolMXBean().getActiveConnections() + ", idle: " + hikari.getHikariPoolMXBean().getIdleConnections() + ", total: " + hikari.getHikariPoolMXBean().getTotalConnections()); } } }6.2 动态数据源切换的扩展
如果项目后期需要支持更多的数据源,或者需要运行时动态添加数据源,可以基于AbstractRoutingDataSource做扩展。核心思路是:
- 创建一个
DynamicDataSource继承AbstractRoutingDataSource。 - 重写
determineCurrentLookupKey()方法,从ThreadLocal里获取当前线程的数据源标识。 - 写一个AOP切面,在方法执行前设置数据源标识,执行后清除。
但这种方案复杂度较高,除非确实有动态切换的需求,否则不建议上。读写分离这种固定规则,用包路径区分就够了。
6.3 主从延迟的应对策略
主从延迟是读写分离绕不开的问题。除了前面说的写后读走主库,还有几个工程上的处理方式:
方式一:缓存兜底。写操作完成后,把数据写入Redis,读操作先查Redis,查不到再查从库。这样即使从库有延迟,也能从缓存拿到最新数据。
方式二:延迟感知。在从库上执行SHOW SLAVE STATUS,获取Seconds_Behind_Master的值。如果延迟超过阈值,读操作自动降级到主库。但这个方案需要额外的监控组件。
方式三:业务容忍。对于列表查询、报表统计这类场景,几秒的延迟是可以接受的。只要核心业务的实时性得到保证,非核心业务走从库完全没问题。
6.4 生产环境配置清单
最后整理一份生产环境的配置清单,可以直接对照检查:
| 配置项 | 建议值 | 说明 |
|---|---|---|
| maximum-pool-size | 20-50 | 根据数据库max_connections和应用实例数计算 |
| minimum-idle | 5-10 | 保持一定数量的热连接 |
| connection-timeout | 30000 | 30秒,不宜过长 |
| idle-timeout | 600000 | 10分钟 |
| max-lifetime | 1800000 | 30分钟,小于数据库wait_timeout |
| read-only(从库) | true | 从库连接设为只读 |
| pool-name | master-pool/slave-pool | 区分两个连接池 |
| connection-test-query | SELECT 1 | 老版本驱动需要 |
| serverTimezone | Asia/Shanghai | MySQL 8.x必须配 |
这套配置我在几个项目里用过,稳定性没问题。但每个项目的负载特征不一样,连接数需要根据实际情况调整。上线前建议做一轮压测,观察连接池的活跃连接数峰值,再反推合适的maximum-pool-size。
我个人在实际操作中的体会是,多数据源配置本身不难,难的是边界清晰。主库管写,从库管读,事务各管各的,包路径各扫各的。只要这几个边界守住了,后面基本不会出问题。最怕的就是边界模糊,一会儿读走主库一会儿写走从库,那排查起来就是噩梦。