Hibernate PESSIMISTIC_WRITE未锁定PostgreSQL行,数据更新异常求助
我来帮你分析下这个问题——你明明用了PESSIMISTIC_WRITE行锁,但两个线程更新后结果不是预期的5,而是8或7,说明两个线程都读到了初始的quantity=10,各自更新后覆盖了对方的结果,锁根本没起到阻塞作用。咱们一步步拆解原因和解决办法:
可能的原因
1. Hibernate没正确生成FOR UPDATE锁语句
悲观锁生效的核心是Hibernate要生成带FOR UPDATE的SELECT语句,这样PostgreSQL才会对目标行加排他锁。如果你的lockArticle方法用Criteria生成的SQL里没有FOR UPDATE,那等于没加锁,两个线程自然会同时读取初始值。
这种情况可能是旧版本Hibernate的Criteria锁模式配置有问题,或者锁模式没正确绑定到实体上。
2. 多余的事务注解导致事务边界混乱
你的updateArticle方法上也加了@Transactional,而doProcess本身已经是事务方法。虽然Spring默认的事务传播是REQUIRED(会加入当前事务),但如果是同一个类内部调用updateArticle,Spring的AOP代理不会生效,这个注解其实没起作用——但更关键的是,如果这个注解的传播行为被错误配置成REQUIRES_NEW,会新建一个独立事务,导致原来的锁逻辑混乱。
3. 实体类或Hibernate配置冲突
比如你的Article实体加了@Version乐观锁注解,悲观锁和乐观锁混用可能导致锁逻辑冲突;或者Hibernate的事务隔离级别被错误设置成READ UNCOMMITTED,不过PostgreSQL默认是READ COMMITTED,这个概率较低。
解决办法
步骤1:移除updateArticle的@Transactional注解
doProcess已经是事务方法,整个流程应该在同一个事务中执行,锁需要持有到事务提交才释放。updateArticle作为内部方法,不需要额外的事务注解,去掉它避免事务边界混乱。
修改后的updateArticle:
updateArticle(Article article){ sessionFactory.getCurrentSession().saveOrUpdate(article); }
步骤2:验证锁语句是否生成
开启Hibernate的SQL日志,查看lockArticle执行的SQL是否包含FOR UPDATE。在Spring Boot中,添加以下配置到application.properties:
logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
如果日志里的SELECT语句没有FOR UPDATE,改用HQL方式加锁(比Criteria更可靠):
Article lockArticle(int id){ return sessionFactory.getCurrentSession() .createQuery("SELECT a FROM Article a WHERE a.id = :id", Article.class) .setParameter("id", id) .setLockMode(LockMode.PESSIMISTIC_WRITE) .getSingleResult(); }
如果坚持用Criteria,可以明确指定锁到实体:
return sessionFactory.getCurrentSession().createCriteria(Article.class) .add(Restrictions.eq("id", id)) .setLockMode("this", LockMode.PESSIMISTIC_WRITE) // 指定锁当前实体 .setTimeout(10000) .getUniqueResult();
步骤3:手动验证数据库锁机制
在数据库客户端开两个会话,先执行:
SELECT * FROM article WHERE id = ? FOR UPDATE;
然后在第二个会话执行同样的语句,看是否会被阻塞。如果手动执行时锁生效,说明是Hibernate配置问题;如果不阻塞,检查表的id是否是主键(PostgreSQL对非主键行的锁可能有差异,但一般还是会阻塞)。
步骤4:检查Hibernate版本
如果用的是Hibernate 5.x之前的旧版本,可能存在PESSIMISTIC_WRITE的bug,升级到5.6.x或更高的稳定版本试试。
总结
大概率是Hibernate没正确生成FOR UPDATE语句,或者多余的事务注解搞乱了事务边界。先去掉updateArticle的事务注解,再验证SQL锁语句,应该就能解决问题,让两个线程按顺序执行,最终得到预期的quantity=5。
内容的提问来源于stack exchange,提问作者Niharika

