JPA:锁定Account父对象仍出现重复Primary地址的问题求助
解决JPA+EclipseLink+MySQL并发下重复Primary地址的问题
问题根源
你当前的逻辑虽然给Account加了PESSIMISTIC_WRITE锁,但存在两个关键漏洞:
- 关联的Address如果是延迟加载,读取时未附带锁,导致其他事务仍能修改Address行;
- REPEATABLE_READ隔离级别下,事务读取的是快照数据,两个并发事务可能都读到“无Primary地址”的旧状态,进而都插入新的Primary地址。
无需升级到SERIALIZABLE的解决方案
1. 锁定Account时同时锁定关联的Address
查询Account时用JOIN FETCH强制加载Addresses,确保锁覆盖关联数据,彻底避免并发冲突。JPQL示例:
TypedQuery<Account> query = em.createQuery( "SELECT a FROM Account a JOIN FETCH a.addresses WHERE a.id = :accountId", Account.class ); query.setParameter("accountId", accountId); query.setLockMode(LockModeType.PESSIMISTIC_WRITE); Account account = query.getSingleResult();
这样整个Account及其关联的Addresses都会被加写锁,其他事务必须等待当前事务提交才能操作该Account的地址。
2. 数据库层面加唯一约束兜底
这是最可靠的防线,即使应用层逻辑出错,数据库也能拦截非法数据。在Address表创建唯一约束:
ALTER TABLE address ADD CONSTRAINT uk_account_primary UNIQUE (account_id, is_primary);
注意:如果is_primary是布尔类型,需确保非Primary地址的is_primary值统一(比如用0表示非Primary),这样一个Account只能有一条is_primary=1的记录,多条is_primary=0的记录不受限制。当并发插入违反约束时,应用层捕获PersistenceException后可重试事务或提示用户。
3. 用DML语句直接更新现有Primary地址
放弃“先读取再修改”的逻辑,改用JPQL直接更新数据库中的Primary地址,避免依赖快照数据:
// 先将该账户下所有Primary地址设为非Primary em.createQuery( "UPDATE Address a SET a.isPrimary = false WHERE a.account.id = :accountId AND a.isPrimary = true" ) .setParameter("accountId", accountId) .executeUpdate(); // 再插入新的Primary地址 Address newPrimaryAddr = new Address(); newPrimaryAddr.setPrimary(true); newPrimaryAddr.setAccount(account); em.persist(newPrimaryAddr);
DML语句会直接在数据库层面加行锁,确保更新操作原子性,两个并发事务中只有一个能成功更新现有Primary地址,另一个会更新0行,之后插入新地址时如果有唯一约束就会触发拦截。
为什么不用SERIALIZABLE?
SERIALIZABLE隔离级别会强制所有事务串行执行,无论操作的是不是同一个Account,这会大幅降低系统吞吐量。上面的方案都是针对单个Account的粒度加锁或约束,完全不影响其他Account的并发操作,性能损耗极小。
内容的提问来源于stack exchange,提问作者mjaggard
相关产品推荐
相关产品推荐

