Hibernate JPA保存后无法立即通过非主键查询新记录排查
问题描述
系统中有一个Patient实体,包含email(字符串)、type(整数)字段,二者均为非主键、非唯一且非空字段,同时还有其他字段及主键id。存在两个异常现象:
- 保存新
Patient实体后,通过JPA的findById可立即获取刚保存的实体;但调用findByEmailAndTypeAndEmailIsNotNull查询时,短时间内返回空结果,延迟1秒后执行相同查询才能获取该实体。 - 连续间隔极短地调用API 5次,会创建2条重复的
Patient记录,后续3次调用才会抛出预期的重复错误。尽管addPatient方法已加synchronized修饰,但并未完全避免重复创建问题。
补充代码:
@Transactional public synchronized Patient addPatient(PatientProfileDto patientProfileDto, Integer facilityId) throws ResourceAlreadyExistsException, EntityNotFoundException, ClientException { // 其他业务代码 performPatientCreationValidations(ownerDto, ownerDemographicDto.getNationality().getId(), Boolean.FALSE, Boolean.FALSE); // Patient实体创建逻辑 patientRepository.saveAndFlush(patient); // 关联Patient到其他实体的代码 } private void performPatientCreationValidations(...params) throws ResourceAlreadyExistsException, ClientException { if (patientRepository.findByEmailAndTypeAndEmailIsNotNull(patientDto.getEmail(), PatientType.OWNER.getId()).isPresent()) { // 抛出重复错误 } }
问题原因与诊断方案
核心原因:JPA缓存机制 + 事务隔离特性 + 并发锁时序错误
1. 查询延迟问题
findById能立即返回结果,是因为它优先读取JPA一级缓存(当前事务内的实体缓存),刚执行saveAndFlush的实体已经存在于该缓存中,无需查询数据库。findByEmailAndTypeAndEmailIsNotNull属于自定义条件查询,默认直接访问数据库。如果当前事务尚未提交,数据库默认的READ COMMITTED隔离级别会阻止其他事务读取未提交的数据;即使是同一事务内,部分JPA实现不会自动将saveAndFlush的实体同步到查询上下文,导致查询无法立即获取最新数据。
2. 重复创建问题
synchronized仅能保证单个JVM进程内的方法串行执行,若系统为集群部署,该锁完全失效。- 更关键的是
@Transactional与synchronized的时序冲突:Spring会在addPatient方法执行前开启事务,方法执行完毕才提交事务。线程A执行完saveAndFlush但事务未提交时,线程B已进入synchronized方法执行校验查询,此时数据库还看不到线程A未提交的数据,校验通过后也创建了实体,最终两个事务提交后产生重复记录。
诊断步骤
排查查询延迟
- 在
saveAndFlush后手动调用entityManager.flush()和entityManager.refresh(patient),再执行自定义查询,验证是否能立即获取数据,以此确认是否为缓存同步问题。 - 查询数据库事务隔离级别:MySQL执行
SELECT @@transaction_isolation;,PostgreSQL执行SHOW TRANSACTION ISOLATION LEVEL;,确认隔离级别是否导致未提交数据不可见。 - 开启JPA SQL日志(如Hibernate设置
show_sql=true),观察saveAndFlush和自定义查询的SQL执行时机,确认查询是否在事务提交前执行,以及SQL语句是否正确。
排查重复创建
- 并发调用时打印线程ID、事务ID,观察线程执行时序:确认线程B的校验查询是否发生在线程A的事务提交之前。
- 临时移除
@Transactional注解(仅用于测试),手动在方法内控制事务开启与提交,验证synchronized是否能正常阻止重复创建(此操作仅用于验证锁与事务的时序问题,不可用于生产环境)。 - 检查系统是否为多JVM集群部署,若为集群模式,
synchronized无法跨进程生效,必须使用数据库级别的锁机制。
内容的提问来源于stack exchange,提问作者Ali Azlan
相关产品推荐
相关产品推荐

