场景:
用户消耗积分兑换商品。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层,保证在事务提交成功后才释放锁!
(画图苦手,时序图有缘再见)