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

Spring Boot JPA中PostgreSQL数据库锁未按预期生效问题排查

问题分析与解决方案

1. 事务代理失效导致锁未生效

如果updateDb方法是被同一个类内部方法调用,Spring声明式事务(@Transactional)会因AOP代理机制失效,导致事务未开启,悲观锁自然无法生效。

  • 解决:将updateDb抽离到独立Service类,或通过Spring上下文获取自身代理对象调用该方法。

2. JPA一级缓存绕过数据库锁

JPA一级缓存(EntityManager级别)会缓存已加载实体,若同一EntityManager重复调用findById,会直接返回缓存数据,不会再次查询数据库获取锁。

  • 解决:
    • 确保每次updateDb调用使用独立EntityManager(默认@Transactional会为每个事务创建新EntityManager,事务正常生效则无此问题)。
    • 更新后手动调用entityManager.clear()清除缓存,或用entityManager.refresh(counterRow)确保从数据库获取加锁后的最新实体。

3. 锁范围与更新逻辑不匹配

若更新流程中加锁查询-修改-保存未在同一事务内完成,或保存时未操作加锁后的实体对象,会导致锁的作用范围失效。

  • 解决:在单个事务内完成完整的加锁、修改、保存操作,确保save方法操作的是加锁后获取的实体。

4. PostgreSQL悲观锁行为验证

PostgreSQL的PESSIMISTIC_WRITE对应SELECT ... FOR UPDATE语句,会锁定行至事务结束。需确认findById的主键查询走索引(主键默认带索引,此问题概率极低),可通过开启PostgreSQL日志验证是否生成了带FOR UPDATE的查询语句。

修正代码示例

正确的Service层实现

@Service
public class CounterService {

    @Autowired
    private CounterRepository counterRepository;

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void updateDb(Long id) {
        // 加锁查询,确保获取行锁
        CounterRow counterRow = counterRepository.findById(id).orElseThrow();
        counterRow.setCount(counterRow.getCount() + 1);
        counterRepository.save(counterRow);
    }

    @Transactional(readOnly = true)
    public CounterRow getCounter(Long id) {
        return counterRepository.findById(id).orElseThrow();
    }
}

正确的Repository接口

public interface CounterRepository extends JpaRepository<CounterRow, Long> {

    @Override
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<CounterRow> findById(Long id);
}

线程调用示例

@SpringBootApplication
public class CounterApplication implements CommandLineRunner {

    @Autowired
    private CounterService counterService;

    public static void main(String[] args) {
        SpringApplication.run(CounterApplication.class, args);
    }

    @Override
    public void run(String... args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                counterService.updateDb(1L);
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        CounterRow counter = counterService.getCounter(1L);
        System.out.println("最终计数器值:" + counter.getCount());
    }
}

额外验证步骤

  • 开启Spring事务日志(设置logging.level.org.springframework.transaction=DEBUG),确认事务的创建与提交流程正常。
  • 开启PostgreSQL查询日志,验证每次findById都执行了SELECT ... FOR UPDATE语句。

内容的提问来源于stack exchange,提问作者Ravi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 12:56:11