JPA悲观锁失效致点击量统计异常:Hibernate先查后加锁如何修复?
修复高并发下URL点击量统计的悲观锁失效问题
问题原因
你遇到的计数丢失问题,核心是悲观写锁没有在读取数据的瞬间生效。Hibernate先从一级缓存中加载了未加锁的旧实体,之后才执行select ... for update语句,导致多个并发线程拿到的都是更新前的旧值,最终更新时互相覆盖,出现统计偏差。
修复方案
方案一:直接使用EntityManager带锁查询(推荐)
跳过Spring Data JPA的Repository封装,直接用EntityManager的带锁模式查询,确保读取数据时立即锁定数据库行,避免缓存干扰:
@Override @Transactional public Link getLinkByShortCutURL(String shortUrl) { Long id = encoder.decode(shortUrl); // 读取时直接加悲观写锁,锁定目标行 Link link = em.find(Link.class, id, LockModeType.PESSIMISTIC_WRITE); if (link == null) { throw new RuntimeException("链接不存在"); } link.incressClicks(); // 事务提交时Hibernate自动同步托管实体的修改,无需手动调用save return link; }
注意:事务内的托管实体在提交时会自动更新到数据库,手动调用
save()反而可能触发额外的查询操作。
方案二:修正Spring Data JPA Repository的锁配置
如果坚持使用Repository方法,需要确保锁模式被正确应用,同时禁用查询缓存避免旧数据读取:
- 删除实体类
NamedQuery中的lockMode配置,改用Spring Data JPA的@Lock注解:
// Spring Data JPA Repository @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT l FROM Link l WHERE l.id = :id") Optional<Link> findByIdForIncressClicks(@Param("id") Long id);
- 添加
@QueryHints禁用查询缓存,强制Hibernate直接执行带锁查询:
@Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT l FROM Link l WHERE l.id = :id") @QueryHints({@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "false")}) Optional<Link> findByIdForIncressClicks(@Param("id") Long id);
方案三:使用数据库原子更新(高并发场景备选)
放弃悲观锁,直接通过JPQL的原子更新语句在数据库层面完成计数增加,从根源避免竞态条件:
// 在Repository中添加原子更新方法 @Modifying @Query("UPDATE Link l SET l.clicks = l.clicks + 1 WHERE l.id = :id") int incrementClicksById(@Param("id") Long id); // Service层调用 @Override @Transactional public Link getLinkByShortCutURL(String shortUrl) { Long id = encoder.decode(shortUrl); int updatedRows = linkRepository.incrementClicksById(id); if (updatedRows == 0) { throw new RuntimeException("链接不存在"); } // 查询并返回更新后的实体 return linkRepository.findById(id).orElseThrow(() -> new RuntimeException("链接不存在")); }
这种方式无需加锁,利用数据库的原子操作保证计数准确,适合超高并发场景。
验证方式
修改完成后重新用JMeter测试,观察Hibernate日志:
- 方案一、二应只输出一条
select ... for update语句 - 方案三应只输出一条
update语句
此时点击计数会和请求次数完全匹配,不再出现丢失情况。
内容的提问来源于stack exchange,提问作者Andrzej Jankowski
相关产品推荐
相关产品推荐

