【redisson】分布式锁与数据库事务

场景:
  用户消耗积分兑换商品。

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();
            }
        }
    }

}

观察以上代码思考:

  1. lock是什么时候释放的?
      调用lock.unlock()就是释放redisson-lock。

  2. 事务是什么时候提交的?
      事务的提交是在方法UserPointService#exchange()执行完成后。所以,示例代码中其实会先释放lock,再提交事务

  3. 事务是什么时候提交完成的?
      事务提交也需要花费一定的时间

由于先释放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层,保证在事务提交成功后才释放锁!

(画图苦手,时序图有缘再见)

posted @ 2019-09-11 15:56  淡丶无欲  阅读(1901)  评论(1编辑  收藏  举报