检查和更新 table 中的记录时潜在的并发问题
Potential concurrency issue while checking and updating a record in table
情况是这样的,
会员必须兑换令牌才能访问(解锁)给定项目。相关数据库table是:
Table 1
Table MEMBER_BALANCE: MEMBER_ID, TOKEN_BALANCE
Table 2
Table UNLOCKED_ITEM: MEMBER_ID, DATE_UNLOCKED, ITEM_ID
我需要执行的检查或约束是
当用户尝试解锁项目时,- TOKEN_BALANCE 必须 > 0,并且
- 用户之前没有解锁过相同的项目。
我的直觉是在 MemberService.java 中写一个简单的方法:
@Transactional
public void unlockItem(Member member, Item item){
memberBalanceDAO.decrementBalance(member);
itemDAO.unlockItem(member, item);
}
我通过在 UNLOCKED_ITEM
table 上的 MEMBER_ID
/ ITEM_ID
对上添加 unique
约束来处理第二个要求。
我认为,我唯一需要注意的是,用户试图同时解锁许多项目,但未满足 TOKEN_BALANCE
要求。例如,TOKEN_BALANCE
为 1,但用户实际上同时单击以解锁两个项目。
下面是我的MemberBalanceDAO.decrementBalance
方法:
@Transactional
public void decrementBalance(Member member) {
MemberBalance memberBalance = this.findMemberBalance(member);
if (memberBalance.getTokens() >= 1) {
memberBalance.setTokens(memberBalance.getTokens() - 1);
this.save(memberBalance);
} else {
throw new SomeCustomRTException("No balance");
}
}
我认为这不会保护我免受 TOKEN_BALANCE
= 1 用例的影响。我担心同时有多个解锁请求。如果余额为 1,我可以同时调用两次 decrementBalance()
来将余额提交为 0,但也可以调用两次成功的 itemDAO.unlockItem(...)
,对吗?
我该如何实现? 我应该将服务级别方法的事务设置为 isolation = Isolation.SERIALIZABLE
吗?还是有 cleaner/better 方法来解决这个问题?
我更愿意建议您在 member_balance
table 中引入 version
专栏。参考文档,Optimistic Locking.
正如您提到的,您无法修改架构;你可以使用无版本乐观锁,explained here.
或者您可能喜欢悲观锁定,explained here。然后,你可以修改你的方法,decrementBalance()
,在那里获取会员余额,不要使用findMemberBalance()
。例如,
@Transactional
public void decrementBalance(Member member) {
MemberBalance memberBalance = entityManager.find(
MemberBalance.class, member.id, LockModeType.PESSIMISTIC_WRITE,
Collections.singletonMap( "javax.persistence.lock.timeout", 200 ) //If not supported, the Hibernate dialect ignores this query hint.
);
if (memberBalance.getTokens() >= 1) {
memberBalance.setTokens(memberBalance.getTokens() - 1);
this.save(memberBalance);
} else {
throw new SomeCustomRTException("No balance");
}
}
注意: 它可能无法正常工作;只是给你一些提示。
情况是这样的,
会员必须兑换令牌才能访问(解锁)给定项目。相关数据库table是:
Table 1
Table MEMBER_BALANCE: MEMBER_ID, TOKEN_BALANCE
Table 2
Table UNLOCKED_ITEM: MEMBER_ID, DATE_UNLOCKED, ITEM_ID
我需要执行的检查或约束是
-
当用户尝试解锁项目时,
- TOKEN_BALANCE 必须 > 0,并且
- 用户之前没有解锁过相同的项目。
我的直觉是在 MemberService.java 中写一个简单的方法:
@Transactional
public void unlockItem(Member member, Item item){
memberBalanceDAO.decrementBalance(member);
itemDAO.unlockItem(member, item);
}
我通过在 UNLOCKED_ITEM
table 上的 MEMBER_ID
/ ITEM_ID
对上添加 unique
约束来处理第二个要求。
我认为,我唯一需要注意的是,用户试图同时解锁许多项目,但未满足 TOKEN_BALANCE
要求。例如,TOKEN_BALANCE
为 1,但用户实际上同时单击以解锁两个项目。
下面是我的MemberBalanceDAO.decrementBalance
方法:
@Transactional
public void decrementBalance(Member member) {
MemberBalance memberBalance = this.findMemberBalance(member);
if (memberBalance.getTokens() >= 1) {
memberBalance.setTokens(memberBalance.getTokens() - 1);
this.save(memberBalance);
} else {
throw new SomeCustomRTException("No balance");
}
}
我认为这不会保护我免受 TOKEN_BALANCE
= 1 用例的影响。我担心同时有多个解锁请求。如果余额为 1,我可以同时调用两次 decrementBalance()
来将余额提交为 0,但也可以调用两次成功的 itemDAO.unlockItem(...)
,对吗?
我该如何实现? 我应该将服务级别方法的事务设置为 isolation = Isolation.SERIALIZABLE
吗?还是有 cleaner/better 方法来解决这个问题?
我更愿意建议您在 member_balance
table 中引入 version
专栏。参考文档,Optimistic Locking.
正如您提到的,您无法修改架构;你可以使用无版本乐观锁,explained here.
或者您可能喜欢悲观锁定,explained here。然后,你可以修改你的方法,decrementBalance()
,在那里获取会员余额,不要使用findMemberBalance()
。例如,
@Transactional
public void decrementBalance(Member member) {
MemberBalance memberBalance = entityManager.find(
MemberBalance.class, member.id, LockModeType.PESSIMISTIC_WRITE,
Collections.singletonMap( "javax.persistence.lock.timeout", 200 ) //If not supported, the Hibernate dialect ignores this query hint.
);
if (memberBalance.getTokens() >= 1) {
memberBalance.setTokens(memberBalance.getTokens() - 1);
this.save(memberBalance);
} else {
throw new SomeCustomRTException("No balance");
}
}
注意: 它可能无法正常工作;只是给你一些提示。