因为是老项目, Spring Boot 是1.4, 使用 AbstractRoutingDataSource 来做主从切换, 配置切面类在进入事务时切换成主库, 但实际运行起来却失败, 写操作路由到了从库
查了很多文章, 试了很多方法都无效, 包括修改注解 @Transactional 的
propagation 属性, 清空主从标记等等
打断点跟踪代码发现, 进入事务时并没有触发获取数据库连接, 而是事务里第一个查询触发了数据库连接的建立, 选择了从库, 后面的所有操作都不会触发数据源选择
后来查到这篇文章
JPA hibernate AbstractRoutingDataSource 在同一方法内使用不同数据源失败_user springboo jpa controller abstractroutingdatas-CSDN博客
通过这篇文章又找到下面的文章
Spring Data JPA 原理与实战第十一天 Session相关、CompletableFuture、LazyInitializationException_org.hibernate.resource.jdbc.spi.physicalconnection-CSDN博客
了解到JPA是有会话保持的, 一旦数据库链接创建在会话期内不会改变, 要想在进入事务时就选择数据源为主库, 需要修改 hibernate.connection.handling_mode
调整处理物理连接的模式。
因为我 Spring Boot 是1.4, 匹配的 hibernate 版本是5.0, 所以是把
release_mode 设置为AFTER_TRANSACTION
@Configuration
public class JpaConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("routingDataSource") DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.my.domain");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
// 设置 JPA 属性
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
properties.setProperty("hibernate.implicit_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy");
properties.setProperty("hibernate.connection.release_mode", "AFTER_TRANSACTION");
em.setJpaProperties(properties);
return em;
}
}
部署后又出现了新问题, 进入事务之前如果有查询操作, 还是会路由到从库, 进入事务时, 不会再触发重新获取数据源, 原因出在这部分代码
public class LogicalConnectionManagedImpl extends AbstractLogicalConnectionImplementor {
private Connection acquireConnectionIfNeeded() {
if ( physicalConnection == null ) {
// todo : is this the right place for these observer calls?
observer.jdbcConnectionAcquisitionStart();
try {
physicalConnection = jdbcConnectionAccess.obtainConnection();
}
catch (SQLException e) {
throw sqlExceptionHelper.convert( e, "Unable to acquire JDBC Connection" );
}
finally {
observer.jdbcConnectionAcquisitionEnd( physicalConnection );
}
}
return physicalConnection;
}
}
如果之前已经获取了链接, jdbcConnectionAccess.obtainConnection() 会直接返回之前创建好的链接进行复用, 解决办法是进入事务之前不要有数据库查询操作, 把查询放到事务里