面试题:对Redis分布式锁有了解吗?在开发中遇到过哪些问题,怎么解决的?

面试题对Redis分布式锁有了解吗?在开发中遇到过哪些问题,怎么解决的?

前言


今天来分享一道比较好的面试题,“对Redis分布式锁有了解吗?在开发中遇到过哪些问题,怎么解决的?”对于这个问题,我们一起看看考察点和比较好的回答吧!

考察点


Redis作为企业级开发中经常使用的缓存中间件,几乎每个程序员都接触过或者听说过,这个问题就是面试官想考察我们对Reids分布式锁没有深刻的认识,以及日常开发中是否善于积累,认真思考。

回答

下面我从3个点方面来回答:

  • 分布式锁的应用场景。

  • 通过多线程并发案例说下Redis分布式锁可能遇到的问题。

  • 使用Redission解决分布式锁出现的问题。

分布式锁的应用场景

  1. 防止缓存穿透,当redis中热点数据过期的时候,大量的线程访问数据库,可能导致数据库崩溃。

  2. 防止秒杀超卖,在秒杀场景中,库存数量同步给redis后,直接对redis 数据进行扣减,存在诸多原子性的安全问题。

  3. 双写一致性,缓存的数据被修改,导致数据库和缓存数据不一致。

  4. 接口幂等性,由于网络波动或者快速点击导致发出多次请求。

多线程并发案例说明

        在分布式秒杀环境下,订单服务从库存中心拿库存数据,如果库存总数大于0,则进行库存扣减,并创建订单服务。订单服务负责创建订单,库存服务负责扣减库存。代码如下:

@Service
public class RedisServiceImpl implements RedisService{
    @Resource
    public RedisTemplate redisTemplate;
    
    @Override
    public void redisLock(){
        Integer stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
        if (stock>0){
            int resultStock = stock-1;
             redisTemplate.opsForValue().set("stock",resultStock+"");
             System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
        }else{
            System.out.println("库存不足,抢购失败!");
        }
    }
}

        当多个线程访问的时候会出现问题,不满足原子性。所以可以加单体锁,例如我们常用的Synchronized,ReentrantLock可以保证原子性安全,但是在分布式系统下,特别是集群部署的时候,这种单体锁根本锁不住!所以我们使用分布式锁,其核心逻辑是对共享资源保证互斥性,当获取锁失败的时候,获取锁的线程进行自旋,执行完毕之后删除分布式锁。Redis实现分布式锁的核心在于SETNX命令,如果键不存在,则将键设置为给定值,在这种情况下它等于SET;当键已存在时,不存在任何操作;成功时返回1,失败返回0。


//分布式锁的实现
@Override
public void redisLock2() {
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    if (lock){
        int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
        if (stock>0){
            int resultStock = stock-1;
            redisTemplate.opsForValue().set("stock",resultStock+"");
            System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
        }else{
            System.out.println("库存不足,抢购失败!");
        }
        //删除锁
        redisTemplate.delete("lock");
    }else{
        //没有得到锁,就自旋获取锁。
        redisLock2();
    }
}

        上面这段代码就是说明分布式锁的实现,其核心逻辑就是对公共资源互斥,获取到锁执行完毕之后删除锁,没有获取到锁就自旋。

Redis分布式锁可能遇到的问题

死锁:如果某个线程在执行锁逻辑过程中宕机,导致没有删除锁,锁会一直得不到释放,其他线程会无法获取锁,造成死锁的情况。解决办法:①添加过期时间 ②原子性的添加过期时间

<------添加过期时间,避免死锁问题的发生----->
@Override
public void redisLock3() {
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    redisTemplate.expire("lock",30, TimeUnit.SECONDS);
    if (lock){
        int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
        if (stock>0){
            int resultStock = stock-1;
            redisTemplate.opsForValue().set("stock",resultStock+"");
            System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
        }else{
            System.out.println("库存不足,抢购失败!");
        }
        //删除锁
        redisTemplate.delete("lock");
    }else{
        //没有得到锁,就自旋获取锁。
        redisLock2();
    }
}
<------原子性的设置过期时间,避免死锁问题的发生----->
//分布式锁的实现,添加原子性的过期时间,避免死锁问题的发生
@Override
public void redisLock4() {
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1",30,TimeUnit.SECONDS);
    //redisTemplate.expire("lock",30, TimeUnit.SECONDS);
    if (lock){
        int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
        if (stock>0){
            int resultStock = stock-1;
            redisTemplate.opsForValue().set("stock",resultStock+"");
            System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
        }else{
            System.out.println("库存不足,抢购失败!");
        }
        //删除锁
        redisTemplate.delete("lock");
    }else{
        //没有得到锁,就自旋获取锁。
        redisLock2();
    }
}

锁不住:

①如果某个线程在执行过程中,出现卡顿或者某个数据库查询很慢超过了锁的释放时间,锁会被释放,其他线程会获取到锁。解决办法:使用Redission解决     ②删除别人锁,当前线程A进入后由于超时,有其他线程B进入,此时redis中的锁是线程B的,而原来的线程A接着执行,线程A会删除掉别人的锁。解决办法:1.通过给当前线程绑定一个局部变量uuid,由于线程是和局部变量绑定之后,我们在删除之前判断一下,当前这把锁是自己的才进行删除。2.虽然方法1可行,但是存在获取uuid,比较uuid,删除锁这三个操作并不是原子性的操作,还是存在着安全性问题,所以引入lua表达式来进行处理。


使用uuid
//分布式锁的实现,使用uuid,避免被其他人删锁
@Override
public void redisLock5() {
    String uuidLock = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuidLock,30,TimeUnit.SECONDS);
    //redisTemplate.expire("lock",30, TimeUnit.SECONDS);
    if (lock){
        int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
        if (stock>0){
            int resultStock = stock-1;
            redisTemplate.opsForValue().set("stock",resultStock+"");
            System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
        }else{
            System.out.println("库存不足,抢购失败!");
        }
        //删除锁
        String redisLock = redisTemplate.opsForValue().get("lock").toString();
        if (uuidLock.equals(redisLock)){
            redisTemplate.delete("lock");
        }
    }else{
        //没有得到锁,就自旋获取锁。
        redisLock2();
    }
}
但是上面这个存在一些问题,获取uuid,比较uuid,删除锁不符合原子性!使用lua表达式:
//分布式锁的实现,使用lua表达式,避免被其他人删锁
@Override
public void redisLock6() {
    String uuidLock = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuidLock,30,TimeUnit.SECONDS);
    //redisTemplate.expire("lock",30, TimeUnit.SECONDS);
    if (lock){
        int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
        if (stock>0){
            int resultStock = stock-1;
            redisTemplate.opsForValue().set("stock",resultStock+"");
            System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
        }else{
            System.out.println("库存不足,抢购失败!");
        }
       String lua="if redis.call('get',KEYS[1]==ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<Long>(lua,Long.class), Arrays.asList("lock"),uuidLock);
    }else{
        //没有得到锁,就自旋获取锁。
        redisLock2();
    }
}

过期时间不符合业务需求:

        在开发中更多的时候是不知道程序具体执行多久的,所以对于设置锁的过期时间上不能很精确,所以会出现安全问题。解决方法:使用Redission,Redission是分布式的Redis锁的解决技术,提供了简单高效的命令供我们使用。可以很好的解决Redis分布式锁的各种问题。Redission是一个可重入锁(ReentrantLock),同时具有看门狗机制,可以对线程获取锁的情况进行很好的控制,监视线程的运行情况,进行时间续期等。使用起来也很简单:


引入依赖:
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
配置类:
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321");
        // 创建客户端
        return Redisson.create(config);
    }
}
使用:
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);

redission的特性:

        redission会默认给我们设置过期时间。

        redission能够自动续期

        redission不会已发死锁问题(引入看门狗机制,会监控锁的状态,如果某一个线程宕机了,不会在进行续期,导致死锁问题)

        redission不会删除别人的锁,会通过抛出异常的方式解决。

        redission的使用很简单。

以上就是我对于这个问题的理解。

总结


这个问题主要是考察求职者对Redis分布式锁的理解能力。在实际应用中,Redis分布式锁非常实用,也是最常用的功能,对于这部分内容如果理解不够深刻,很容易写出低效代码。希望读完这篇文章你有所收获。欢迎转发,关注微信公众号:程序员的故事,了解更多精彩面试题。


精选推荐

1.面试题:过滤器和拦截器的区别?

2.如何知道线程池中的线程任务执行完成?

3.面试题:为什么 SQL 语句不要过多的 join?

4.有没有排查过线上OOM问题,怎么处理的?

5.cpu飚高反应慢怎么排查?

6.@Resource和@Autowired的区别?

7.面试题:请说一下对象的创建过程?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值