Hibernate的多租户功能:用于ID生成的序列混乱了
看起来Hibernate似乎把来自分离租户的序列中缓存的ID值混在一起了。我无法找到问题所在或复现,但在生产环境中会出现序列的下一个值小于表中最大id的情况。例如:最大id = 100,但seq.next_value = 90。这样最终会导致id已存在的异常。
技术栈:
- Spring Boot 3.5.13
- Hibernate 6.6.45
- MariaDB 10.6.23
使用Spring的 AbstractRoutingDataSource配置:
public class RoutingDataSource extends AbstractRoutingDataSource { ... }
@Bean
@Primary
protected DataSource dataSource(TenantService tenantService, DeploymentMutex deploymentMutex) {
return new RoutingDataSource(tenantService, flywayProps, deploymentMutex);
}
@Bean
protected LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource,
TenantService tenantService,
CustomHibernateConnectionProvider connectionProvider,
CustomHibernateTenantIdentifierResolver tenantIdentifierResolver) {
var em = new LocalContainerEntityManagerFactoryBean();
em.getJpaPropertyMap().put(CacheSettings.USE_SECOND_LEVEL_CACHE, false);
em.getJpaPropertyMap().put("hibernate.cache.region.factory_class", "infinispan");
em.getJpaPropertyMap().put("hibernate.cache.infinispan.cfg", "org/infinispan/hibernate/cache/commons/builder/infinispan-configs-local.xml");
em.getJpaPropertyMap().put(MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider);
em.getJpaPropertyMap().put(MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
em.setDataSource(dataSource);
em.setPackagesToScan(scanPackages);
em.setPersistenceUnitName(persistenceUnitName);
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return em;
}
@Component
class CustomHibernateConnectionProvider extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl<String> {
private DataSource dataSource;
public CustomHibernateConnectionProvider(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected DataSource selectAnyDataSource() {
return dataSource;
}
@Override
protected DataSource selectDataSource(String tenantIdentifier) {
return dataSource;
}
}
@Component
class CustomHibernateTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
private final TenantService tenantService;
CustomHibernateTenantIdentifierResolver(TenantService tenantService) {
this.tenantService = tenantService;
}
public String resolveCurrentTenantIdentifier() {
return tenantService.getCurrentTenantName();
}
public boolean validateExistingCurrentSessions() {
return true;
}
}
@Entity
@Audited
public class EntityA {
@Id
@GeneratedValue(strategy = SEQUENCE, generator = "seq_entity_a")
@SequenceGenerator(name = "seq_entity_a", sequenceName = "seq_entity_a", allocationSize = 30)
private Long id;
...
}
CREATE OR REPLACE SEQUENCE seq_entity_a START WITH 1 INCREMENT BY 30
tenantService.getCurrentTenantName() 正在使用 TenantContextHolder 将租户标识符存储在一个 ThreadLocal 中。
你能看到我的配置有什么问题吗?
解决方案
根据你的实现,以下是一些观察点:
-
模型定义:请检查每个租户是否拥有自己的序列对象(seq_entity_a_tenant1、seq_entity_a_tenant2),而不是共享seq_entity_a。
-
分配大小匹配:让Hibernate的 allocationSize与数据库序列的增量保持一致。如果数据库增量为1,而Hibernate使用30,就会产生不匹配。
-
检查JPA配置:尝试使用属性hibernate.id.sequence_mismatch_strategy=FIX。当检测到不一致时,这会强制Hibernate与数据库序列重新同步。
-
其它:如果多租户环境中的序列不可靠,考虑减小分配大小,或使用GenerationType.IDENTITY。
-
让Hibernate通过MultiTenantConnectionProvider专门处理租户选择。
-
确保在每次切换租户后清除tenantContext。
可能的修复方法:尝试为每个租户使用单独的序列。
@SequenceGenerator(
name = "seq_entity_a"
sequenceName = "seq_entity_a_#{tenantIdentifier}"
allocationSize = 30
)
你可能需要一个自定义的序列生成器,或者可以使用Hibernate的 ImplicitNamingStrategy,在序列名中追加租户ID。