場景:
用戶消耗積分兌換商品。mysql
user_point(用戶積分):redis
id | point |
---|---|
1 | 2000 |
point_item(積分商品):sql
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,再提交事務
。app
事務是何時提交完成的? 事務提交也須要花費必定的時間code
因爲先釋放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請求的事務可能未提交完成形成!get
解決方案:
暫時是將lock改寫到controller層,保證在事務提交成功後才釋放鎖!
(畫圖苦手,時序圖有緣再見)