場景:
用戶消耗積分兌換商品。
user_point(用戶積分):
id | point |
---|---|
1 | 2000 |
point_item(積分商品):
id | point | num |
---|---|---|
101 | 200 | 10 |
傳統的controller、service、dao三層架構,數據庫事務控制在service層(數據庫MYSQL)。
@RestController
@RequestMapping(value = {"point"})
public class UserPointController{
@Autowired
private UserPointService userPointService;
@RequestMapping("/exchange")
public boolean exchange(HttpServletRequest request, Long userId, Long itemId){
return userPointService.exchange(userId, itemId);
}
}
@Service
public class UserPointService {
@Resource
private RedissonClient redissonClient;
@Transaction
public boolean exchange(Long userId, Long itemId) throws Exception {
RLock lock = redissonClient.getLock("lock:" + itemId);
try {
boolean bool = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!bool){
throw new Exception("操作失敗,請稍后重試");
}
UserPoint user = "select * from user_point where id = :userId";
PointItem item = "select * from point_item where id = :itemId";
if(user.point - item.point > 0 && item.num > 0){
// 扣減積分
>> update user_point set point = point - :item.point where id = :userId;
// 扣減庫存
>> update point_item set num = num - 1 where id = :itemId;
return true;
}
return false;
} catch (Exception e) {
throw e;
} finally {
if(lock != null && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
觀察以上代碼思考:
-
lock是什么時候釋放的?
調用lock.unlock()
就是釋放redisson-lock。 -
事務是什么時候提交的?
事務的提交是在方法UserPointService#exchange()
執行完成后。所以,示例代碼中其實會先釋放lock,再提交事務
。 -
事務是什么時候提交完成的?
事務提交也需要花費一定的時間
由於先釋放lock,再提交事務。並且由於mysql默認的事務隔離級別為 repetable-read
,這導致的問題就是:
假設現在有2個並發請求{"userId": 1, "itemId": 101}
,user剩余積分201。
假設A請求先獲得lock,此時B請求等待獲取鎖。
A請求得到的數據信息是user_point#point=201,此時允許兌換執行扣減,返回true。
在返回true前,會先釋放lock,再提交事務。
釋放lock后,B請求可以馬上獲取到鎖,查詢user可能得到剩余積分: 201(正確的應該是剩余積分: 1),因為A請求的事務可能未提交完成造成!
解決方案:
暫時是將lock改寫到controller層,保證在事務提交成功后才釋放鎖!
(畫圖苦手,時序圖有緣再見)