You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方法,需要确保锁模式被正确应用,同时禁用查询缓存避免旧数据读取:

  1. 删除实体类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);
  1. 添加@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 05:42:53