redis分布式锁的各种坑

本文深入探讨Redis实现分布式锁的多种方式,从单机环境到分布式环境,逐步解决并发操作带来的线程安全问题。通过代码示例,展示了如何使用setnx、设置过期时间、锁续命等技术,最终引入Redission简化分布式锁的实现。

用redis做分布式锁,看似是一个非常简单的问题,但是其中却蕴含着很多坑,我从单机方式开始把redis分布式锁的所有坑都踩一踩,豁然开朗。文字描述较少,直接上代码,相信大家一看就懂。尤其是点评把每一个情况的坑描述出来了。后一种情况会把上一种情况的坑给解决掉。

第一种情况:单机方式

我们对一个库存进行扣减的一个简单操作

public class RedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public String descStock(){
    	//获取库存量
        int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if(stockNum>0){
        	//库存减一
            stockNum--;
            //重新设置库存
            redisTemplate.opsForValue().set("stock",stockNum+"");
            System.out.println("库存扣减成功,剩余"+stockNum);
        } else {
            System.out.println("库存扣减失败");
        }
        return "SUCCESS";
    }
}

点评:

在单机环境下,这个代码有什么问题呢?如果并发高的情况下,stockNum是一个共享变量,很明显会带来线程安全问题

第二种情况:单机+同步锁

对于第一种情况,我们的解决办法也很简单,只需要加上synchronized代码块或者Lock锁即可

 public String descStock() {
        synchronized (this) {
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--;

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }
        }
        return "SUCCESS";
    }

点评:

对于这种情况,如果在分布式集群模式下是否能实现共享变量的安全问题呢?明显不能,因为synchronized和Lock都是线程级别的锁,不能在进程之间锁住数据的安全性。这里我们用redis做分布式锁来解决(除了redis,还有zookeeper和数据库来实现分布式锁)

第三种情况:分布式环境+redis实现分布式锁–1:加分布式锁

这里用到了setnx命令:如果key存在着set失败,如果不存在则set成功。设置成功,意味着当前线程拿到锁,设置失败意味着当前线程没有拿到锁。

    public String descStock() {
        String lockKey = "lockKey";
        //类似jedis.setnx(lockKey,"wxgLock");
        //获取分布式锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock");
        if(!result){
            return "被锁住了,请稍后重试:";
        }
        //throw new Exception(); ---step A
        int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (stockNum > 0) {
            stockNum--;

            redisTemplate.opsForValue().set("stock", stockNum + "");
            System.out.println("库存扣减成功,剩余" + stockNum);
        } else {
            System.out.println("库存扣减失败");
        }
        return "SUCCESS";
    }

点评:

问题。锁没有释放,导致其他线程永远拿不到锁,导致死锁。

第四种情况:分布式环境+redis实现分布式锁–2:释放分布式锁

public String descStock() {
        String lockKey = "lockKey";
         //获取分布式锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock");//类似jedis.setnx(lockKey,"wxgLock");
        try {
            if(!result){
                return "被锁住了,请稍后重试:";
            }
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--; // step A

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }    
        }finally {
        	//释放锁
            redisTemplate.delete(lockKey);
        }
        
        return "SUCCESS";
    }

点评:

注意这里把释放锁放到了finally中是因为如果业务代码异常的话也可以释放锁。难道就没有问题了吗?

问题:当代码执行STEP A 中的时候,如果是服务器宕机了,或者kill -9 了。那么依然会导致锁无法释放,其他服务依然拿不到锁,导致死锁。怎么办?

第五种情况:分布式环境+redis实现分布式锁–3:设置过期时间


    public String descStock() {
        String lockKey = "lockKey";
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock");//类似jedis.setnx(lockKey,"wxgLock");
        redisTemplate.expire(lockKey,30, TimeUnit.SECONDS); //设置过期时间30s -- step a
        try {
            if(!result){
                return "被锁住了,请稍后重试:";
            }
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--;

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }
        }finally {
            redisTemplate.delete(lockKey);
        }

        return "SUCCESS";
    }

点评:

这个代码就没有问题吗?如果在设置时间前,也就是step a前就崩了,那么虽然设置成功了,但是,没有设置过期时间成功,依然会有第五种情况的问题,本质是两步操作么有实现原子性。死锁

第六种情况:分布式环境+redis实现分布式锁–4:设置过期时间


    public String descStock() {
        String lockKey = "lockKey";
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock",10,TimeUnit.SECONDS);
        try {
            if(!result){
                return "被锁住了,请稍后重试:";
            }
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--;

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }
        }finally {
            redisTemplate.delete(lockKey);
        }

        return "SUCCESS";
    }

点评:

一步操作保证原子性。但是真的没问题了吗?在并发不高的情况下这个分布式锁问题不大,但是在高并发下出现如下问题:

在这里插入图片描述

也就是说自己创建的锁被其他线程给删除掉了!怎么办?可以引入一个唯一值,如thread ID或者uuid等

 public String descStock() {
        String lockKey = "lockKey";
        String tid = Thread.currentThread().getId()+"";
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, tid,10,TimeUnit.SECONDS);//类似jedis.setnx(lockKey,"wxgLock");
        try {
            if(!result){
                return "被锁住了,请稍后重试:";
            }
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--;

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }
        }finally {
            if(tid.equals(redisTemplate.opsForValue().get(lockKey))){
                redisTemplate.delete(lockKey);
            }
        }

        return "SUCCESS";
    }

点评:

这样就真的没问题了吗?一脸蒙蔽。还有一个问题:我们设置了10s的过期时间真的够吗?如果业务代码的一个sql慢查询超过了10s。那么还是会导致代码没有执行完,锁已经自动释放了,其他线程可以获取锁(多个线程同时获得了锁),导致线程安全问题。那么设置几秒合适呢?好像没有合适的值吧。那怎么办?----可以用一个定时器来实现:锁的续命

第七种情况:分布式环境+redis实现分布式锁–5:锁的续命

锁的续命就是每隔一段时间判断当前线程的锁是否存在,如果存在则重置过期时间(给锁续了一条命!)
直接上代码:单开一个线程(可以用线程池优化),实现锁的续命

 public String descStock() {
        String lockKey = "lockKey";
        String tid = Thread.currentThread().getId()+"";
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, tid,10,TimeUnit.SECONDS);//类似jedis.setnx(lockKey,"wxgLock");
        
        
        //锁续命的逻辑:可能不严谨,但是思路应该就这样
        new Thread(()->{
            Timer timer =new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    String tid = redisTemplate.opsForValue().get(lockKey);
                    if(id!=null&&tid.equals(id)){
                        //锁续命
                        redisTemplate.opsForValue().setIfAbsent(lockKey, tid,10,TimeUnit.SECONDS);
                    } else {
                   		 timer.cancel(); 
                    }
                   
                }
            },2000,3000);//延迟2s执行,每隔3s执行一次续命逻辑
        });
        
        
        try {
            if(!result){
                return "被锁住了,请稍后重试:";
            }
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--;

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }
        }finally {
            if(tid.equals(redisTemplate.opsForValue().get(lockKey))){
                redisTemplate.delete(lockKey);
            }
        }

        return "SUCCESS";
    }

点评:

这样的话redis分布式锁就差不多了。但是操作到第七种的情况下已经变得复杂了,也不是所有人能考虑到高并发下的这些所有情况,所以我们可以借用Redission来实现分布式锁。

第八种情况:分布式环境+redis实现分布式锁–6:redission

public String descStock() {
        String lockKey = "lockKey";
        RLock lock = redisson.getLock(lockKey); 
        try {
            lock.lock();
            int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                stockNum--;

                redisTemplate.opsForValue().set("stock", stockNum + "");
                System.out.println("库存扣减成功,剩余" + stockNum);
            } else {
                System.out.println("库存扣减失败");
            }
        }finally {
           lock.unlock();
        }

        return "SUCCESS";
    }

点评:

lock.lock(); lock.unlock();两行代码解决,方便!且默认已经解决了以上的所有问题。

redission内部是用lua脚本实现的,本来想上源码分析的,但是码字实在太累了,有空再补上吧。撤了撤了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值