Spring Boot中使用Envers时如何避免findRevisions请求过慢?
我来帮你拆解这个问题的核心原因,以及对应的落地解决方案:
问题根源
你碰到的慢查询,本质是Envers默认生成的findRevisions SQL依赖嵌套子查询(比如你提到的select max(data_au1.rev)...逻辑),当data_aud表数据量达到3500条时,数据库需要频繁做全表/范围扫描来定位最大修订号,再叠加分页逻辑的开销,导致查询效率骤降。另外,如果你的data_aud表没有针对**实体主键(对应你的key参数)和修订号(rev)**的复合索引,这个性能问题会被进一步放大。
解决方案
1. 给审计表添加针对性复合索引
这是最立竿见影的优化手段。给data_aud表创建包含实体主键列和修订号的复合索引,并且把修订号设为降序(毕竟我们通常更关心最新的修订记录):
CREATE INDEX idx_data_aud_key_rev ON data_aud (data_id, rev DESC);
注意:把
data_id替换成你data_aud表中对应Data实体主键的实际列名(比如如果你的Data主键字段是key,列名可能是key_或者data_key,取决于你的JPA映射配置)。
这个索引能让数据库快速过滤出指定key的所有修订记录,同时无需额外排序就能满足分页时的“最新版本优先”需求。
2. 自定义查询替代默认的findRevisions
Envers默认的RevisionRepository方法生成的SQL不一定是最优解,我们可以自定义更高效的查询来实现分页获取修订记录。在你的DataAudRepository中添加自定义方法:
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface DataAudRepository extends RevisionRepository<Data, String, Integer>, JpaRepository<Data, String> { // 直接查询审计实体并分页,返回结果更直观 @Query("SELECT da FROM DataAud da WHERE da.key = :key ORDER BY da.rev DESC") Page<DataAud> findDataRevisions(@Param("key") String key, Pageable pageable); // 如果需要返回Envers标准的Revision对象,可使用这个写法 @Query("SELECT new org.springframework.data.envers.repository.support.DefaultRevisionMetadata(r.rev, r.timestamp), d " + "FROM RevInfo r JOIN DataAud d ON r.rev = d.rev " + "WHERE d.key = :key ORDER BY r.rev DESC") Page<Object[]> findCustomRevisions(@Param("key") String key, Pageable pageable); }
说明:
DataAud是Data实体对应的审计表实体(Envers自动生成或你自定义的),RevInfo是Envers默认的修订信息表实体(存储修订号和时间戳)。调用时可以直接用DataAud的分页结果,或者把Object[]转换成Revision对象(如果需要Envers的标准包装类)。
3. 用Envers原生查询API手动构建查询
如果上述方法还不够灵活,你可以在Service层直接使用Envers的查询API,完全控制查询逻辑:
import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.hibernate.envers.query.AuditEntity; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import javax.persistence.EntityManager; import java.util.List; @Service public class DataAuditService { private final EntityManager entityManager; public DataAuditService(EntityManager entityManager) { this.entityManager = entityManager; } public Page<Data> findRevisions(String key, Pageable pageable) { AuditReader auditReader = AuditReaderFactory.get(entityManager); // 查询分页数据 List<Data> revisions = auditReader.createQuery() .forRevisionsOfEntity(Data.class, false, true) .add(AuditEntity.id().eq(key)) .addOrder(AuditEntity.revisionNumber().desc()) .setFirstResult((int) pageable.getOffset()) .setMaxResults(pageable.getPageSize()) .getResultList(); // 查询总条数 long total = (long) auditReader.createQuery() .forRevisionsOfEntity(Data.class, false, true) .add(AuditEntity.id().eq(key)) .addProjection(AuditEntity.revisionNumber().count()) .getSingleResult(); return new PageImpl<>(revisions, pageable, total); } }
这种方式可以彻底避免默认Repository方法的冗余子查询,同时完美实现分页逻辑。
验证优化效果
优化完成后,你可以用数据库的执行计划工具(比如MySQL的EXPLAIN)查看查询是否命中了新创建的索引,确认全表扫描是否被规避——只要索引生效,大数据量下的查询速度会有明显提升。
内容的提问来源于stack exchange,提问作者user7393582

