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