redisson分布式锁跨微服务请求进程的加锁(重入)问题

来源:4-19 基于Redis实现真正高可用的锁--RedLock

weixin_慕移动7390343

2022-06-14

老师好,我在自己写一个团菜的微服务项目,有场景需要使用分布式锁,在使用redisson来控制时,发现原本是锁重入却导致了死锁,自己把自己锁死了。

我使用分布式锁的场景:

  • 团长在团单活动进行中可以补充菜品的库存
  • 跟单用户下单时会扣减菜品的库存

order微服务的跟单后台逻辑:

package com.pf.zjt.microservices.order.service.impl;

import ...

@Service
@Slf4j
public class OrderFollowSaveServiceImpl implements OrderFollowSaveService {

    ...

    @Resource
    private RedissonClient redissonClient;

    @Override
    public FoodCheckResultDto createOrderFollow(OrderFollowCreateDto followCreateDto) throws BusinessException {

        // 前置检查操作
        ...

        /*
         * 先检查库存,再更新库存,在food微服务中提供了这两个rest api
         * 为了防止多个跟单请求并发执行时,相互干扰,比如一个跟单请求在更新库存前,另一个跟单请求取到旧值下单,就会发生超卖的问题
         * 因此这两块操作在一个分布式锁中
         */
        String lockKey = ResourceTypes.FOOD_MAIN.formatKey(foodMainId);
        RLock rLock = redissonClient.getLock(lockKey);
        rLock.lock();

        // 检查库存
        FoodCheckResultDto followResultDto = foodFeignService.checkStock(foodMainId, checkList).getBody();
        log.info("检查完库存");
        ...
        // 更新库存
        foodFeignService.updateStock(stockUpdateDto);
        rLock.unlock();

        // 跟单记录入库操作
        ...

        return followResultDto;
    }

    ...
}

food微服务提供一个更新库存的api,主要逻辑:

package com.pf.zjt.microservices.food.service.impl;

import ...

@Service
@Slf4j
public class FoodServiceImpl implements FoodService {

    ...

    @Resource
    private RedissonClient redissonClient;

    ...

    @Override
    public void updateStock(FoodStockUpdateDto updateDto) {
        
        String lockKey = ResourceTypes.FOOD_MAIN.formatKey(updateDto.getFoodMainId());
        log.info("准备更新库存,尝试获取锁...");
        RLock rLock = redissonClient.getLock(lockKey);
        /*
         * 团长调用补充库存的rest api调用下面的更新库存逻辑前,需要加分布式锁
         * 因为在没有获取锁就执行时,很可能在一个跟单请求执行扣减库存之前补充了库存,然后紧接着就被扣减库存覆盖更新了
         */
        rLock.lock();

        String foodKey = getRedisKey(updateDto.getFoodMainId());
        boolean useCache = redisTemplate.hasKey(foodKey);
        if (useCache) {
            // 更新redis缓存的库存
        }
        // 更新数据库库存
        rLock.unlock();
    }
    
    ...
}

因此跟单请求再去调更新库存的接口时,其实做的应该时锁重入的操作。但是在本地调试时发现,这种跨服务进程的对同一资源的重复加锁操作却造成了死锁,难道说redisson的锁重入只是实现了对同一个服务器jvm线程级别的重入锁实现嘛?因为具体的源码我还没用调试跟踪。

以下是我的测试情况

首先准备好服务:
图片描述

仅发送一个跟单请求来测试下:
图片描述
order微服务控制台:
图片描述
food微服务的控制台
图片描述
一直在等待锁,最后超时了。。。

关于这个问题,老师能帮忙看下吗,在线等。。。感谢!

写回答

1回答

大能老师

2022-06-16

同学你好,抱歉这个问题刚看到。对于你这个案例我总结了以下几点内容,你可以参考一下:

  1. 首先这样肯定就不是重入锁了,重入锁是针对同一个线程来说的,也就是这里要同一个线程才可以多次加锁

  2. 这个地方这样写的话,相当于就是一个线程获取到了锁但还没有释放,另一个线程尝试来获取同一把锁(并且没有设置等待时间),所以会造成死锁问题

  3. 这里我不太确定的你库存检查,是否只是为了保证去更新库存的时候有足够的库存,如果是这样的话,可以在更新操作里面同时增加库存的判断,这一点我们在课程里面也有相关案例。如果是这样的话,foodFeignService.checkStock这一步其实可以不用的。然后在createOrderFollow这个方法里面不需要锁的处理,只需要保留updateStock里面的锁,这样不同线程要并发更新库存的时候,就有锁的约束。

  4. 最后假如说checkStock确实是需要和updateStock一起加锁处理,那么有两种处理方式:在createOrderFollow和updateStock这两个方法里面,要么使用两个不同的锁;要么把updateStock里面的锁给去掉(假如其他地方没有调用的话)。但是这种方式其实还是不建议,这样锁的粒度太大了。

如果同学对这个内容有疑惑的话,可以到咱们的QQ群来继续探讨和交流

0
1
weixin_慕移动7390343
感谢老师的解答,知道怎么改造了,谢谢啦
2022-06-24
共1条回复

Java分布式架构设计与开发实战

项目贯穿式讲解,真正将理论与实战相结合

325 学习 · 74 问题

查看课程