
本文深入解析 hibernate 一级缓存机制导致的“刚 save 却查不到”现象,重点说明为何 findbyid 可立即命中而 findbyemailandtype 失败,并提供 flush、事务边界、缓存同步等生产级解决方案。
本文深入解析 hibernate 一级缓存机制导致的“刚 save 却查不到”现象,重点说明为何 findbyid 可立即命中而 findbyemailandtype 失败,并提供 flush、事务边界、缓存同步等生产级解决方案。
在使用 Spring Data JPA 开发时,一个常见却易被忽视的问题是:新实体调用 save() 后,立即通过非主键字段(如 email + type)查询返回空,但用主键 findById() 却能立刻查到。这并非数据库延迟或 SQL 错误,而是 Hibernate 一级缓存(Session 级缓存)与写入时机不一致引发的典型一致性问题。
根本原因:一级缓存与延迟写入机制
Hibernate 默认采用 延迟写入(write-behind)策略:save() 仅将实体标记为“已托管(managed)”,并放入当前 Session 的一级缓存中,并不立即执行 INSERT 语句。真正的 SQL 插入通常推迟到以下任一时刻:
- Session 显式调用 flush();
- 事务提交前(@Transactional 结束时);
- 执行需同步数据库状态的查询(如 count() 或某些 SELECT)。
而 findById(id) 是典型的 ID-based cache lookup:Hibernate 直接从一级缓存中按主键返回实体,无需访问数据库,因此“秒出”。
但 findByEmailAndTypeAndEmailIsNotNull(...) 是 基于数据库的查询(query-by-example):它会生成 SELECT ... WHERE email = ? AND type = ? 并发送给数据库。此时若 Session 尚未 flush,INSERT 还未执行,数据库中自然查不到该记录——这就是你观察到“1 秒后才查到”的本质:事务提交或后台异步 flush 最终使数据落库。
关键证据:你的代码暴露了问题链
你提供的代码片段清晰揭示了问题闭环:
@Transactional
public synchronized Patient addPatient(...) {
// ... 验证逻辑在 save 前执行
performPatientCreationValidations(...); // ← 此处调用 findByEmailAndType!
patientRepository.saveAndFlush(patient); // ← 此处才 flush!但已晚了
// ... 其他逻辑
}
private void performPatientCreationValidations(...) {
if (patientRepository.findByEmailAndTypeAndEmailIsNotNull(...).isPresent()) {
throw new ResourceAlreadyExistsException();
}
}⚠️ 注意:performPatientCreationValidations() 在 saveAndFlush() 之前 调用!此时实体尚未持久化,findByEmailAndType... 查询数据库必然为空 → 验证通过 → 重复创建发生。即使方法加了 synchronized,也只保证线程串行执行,无法解决单次请求内缓存与数据库的可见性错位。
正确解决方案(按推荐顺序)
✅ 方案 1:调整验证时机 —— 最简洁可靠
将唯一性校验移至 saveAndFlush() 之后,确保数据已落库:
@Transactional
public Patient addPatient(PatientProfileDto patientProfileDto, Integer facilityId) {
// ... 构建 patient 实体
// 先保存并强制刷入数据库
Patient savedPatient = patientRepository.saveAndFlush(patient);
// 再校验:此时数据库已有该记录,后续并发请求可被拦截
if (patientRepository.findByEmailAndTypeAndEmailIsNotNull(
savedPatient.getEmail(),
PatientType.OWNER.getId()
).filter(p -> !p.getId().equals(savedPatient.getId())).isPresent()) {
throw new ResourceAlreadyExistsException("Patient with same email and type already exists");
}
// ... 关联其他实体
return savedPatient;
}? 提示:校验时增加 !p.getId().equals(savedPatient.getId()) 排除自身,避免误判。
✅ 方案 2:使用数据库唯一约束(强烈推荐作为兜底)
在数据库层面为 (email, type) 添加唯一索引,从根本上杜绝脏数据:
CREATE UNIQUE INDEX idx_patient_email_type ON patient(email, type);
配合 @Transactional,当并发插入相同 (email, type) 时,后提交的事务将抛出 DataIntegrityViolationException,可在 Service 层捕获并转换为业务异常。这是最终一致性与性能的最佳平衡点。
⚠️ 方案 3:慎用 @Query + @Modifying(flushAutomatically = true)
若必须在保存前校验(极少见),可强制刷新后再查,但违背直觉且易出错:
@Modifying(flushAutomatically = true)
@Query("SELECT p FROM Patient p WHERE p.email = :email AND p.type = :type")
List<Patient> findExistingByEmailAndType(@Param("email") String email, @Param("type") Integer type);不推荐——它混淆了“验证”与“持久化”的职责边界,且 flushAutomatically = true 仅对当前 Query 生效,无法保证整个事务一致性。
总结:牢记三个原则
- 主键查询走缓存,非主键查询走数据库:这是 Hibernate 的设计基石,不是 bug;
- 验证逻辑必须在数据持久化之后:否则永远存在竞态窗口;
- 应用层校验 + 数据库约束双保险:前者提升用户体验,后者保障数据绝对正确。
通过理解一级缓存生命周期,并合理编排 saveAndFlush() 与业务校验的顺序,即可彻底解决此类“刚存就查不到”的困扰,构建出高可靠、易维护的 JPA 应用。










