使用Redis实现分布式锁详解

在讲分布式锁之呢,我们不妨先来说说什么是分布式系统。
在这里插入图片描述
在系统早期,用户量少,可能我们一个app的所有模块都存在与一个应用包,部署在一台机器上,这便是我们的单体应用架构。这种设计,如果用户访问量大,便很容易造成系统压力过大而导致的系统宕机,其次如果一个模块,比如支付功能bug或其他原因,便直接导致整个系统瘫痪。
为解决这个问题,可能会想到我们的集群部署。
在这里插入图片描述
集群部署配合配合负载均衡(负载算法),可以在很大程度上减少单台服务器访问压力,但是这种模式任然存在某一模块bug,导致整个应用挂死的现象,继而导致整个集群架构瘫痪。
继而分布式架构便应声而出,分布式架构有一个最大的好处便是可以防止我么的某一模块的瘫痪导致整个应用的挂死。
在这里插入图片描述
如图,就算我们的支付模块挂了,我们还是可以完成我们的登录,下单等其他操作,不会造成整个应用群宕机。

当然今天主要是讲我们的分布式锁的实现。对于如上的分布式架构,会有一个什么问题呢?见下图
在这里插入图片描述
如图,如果我们系统是分布式架构,举个简单例子现在又库存模块和订单模块,如果库存只有1,但是此时同时有两个用户下单成功,都同时进入库存去做减库存操作,便会出现并发问题。我们可以用简单的代码来模拟这个操作。
在这里插入图片描述
在这里插入图片描述
如图,新建order类,模拟我们的订单系统,stock类,模拟我们的库存操作,库存只有一个,显然只有STOCK_NUM>0的时候才可以减库存成功。
在这里插入图片描述
新增测试类,模拟用户操作,期间起两个线程,模拟两个用户同时操作,下单后进行减库存,显然出现了并发问题,两个用户都减库存成功。那么这个问题要怎么解决呢,于是便引入我们今天的主角了,分布式锁了。

public class RedisLock implements Lock {

    ThreadLocal<Jedis> jedis = new ThreadLocal<Jedis>();
    private static String LOCK_NAME = "LOCK";
    private static String REQUEST_ID = "111111";

    void init(){
        if (jedis.get() ==null)
            jedis.set(new Jedis("localhost"));
    }

    public void lock() {
        init();
        if (tryLock())
            jedis.get().set(LOCK_NAME,REQUEST_ID,"NX","PX",5000);
    }

    public boolean tryLock() {
        while(true){
            Long ret = jedis.get().setnx(LOCK_NAME,REQUEST_ID);
            if (ret==1)
                return true;
        }
    }

    public void unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.get().eval(script, Collections.singletonList(LOCK_NAME), Collections.singletonList(REQUEST_ID));
    }

    public Condition newCondition() {
        return null;
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

}

代码如上,既然是锁,当然首先需要实现我们的Lock接口,当然对于里面的方法,我们只需要实现3个方法即可:tryLock(),lock()和unlock()即可。

 public void lock() {
        init();
        if (tryLock())
            jedis.get().set(LOCK_NAME,REQUEST_ID,"NX","PX",5000);
    }

对于lock方法,很简单,都是固定套路的,先尝试拿锁,如果能拿到锁,则往下执行,否则阻塞,在设置key的时候,设置一个超时时间,防止系统死锁问题。

jedis.set(LOCK_NAME, REQUEST_ID);
jedis.expire(LOCK_NAME, 3000);

有人说是不是可以用上述写法,其实这种写法是有问题的,就不是原子操作了,如果在
jedis.set(LOCK_NAME, REQUEST_ID);执行完之后,系统挂死了,超时时间设置不了,后续解锁操作当然也没法执行了,则系统便一直死锁了。

考虑并发问题,对于jedis连接,采用ThreadLocal去构造。

ThreadLocal<Jedis> jedis = new ThreadLocal<Jedis>();
void init(){
    if (jedis.get() ==null)
        jedis.set(new Jedis("localhost"));
}

对于tryLock(),我们可以使用一个while(true)循环,去一直尝试拿锁,如果拿到,则返回结果标识

 public boolean tryLock() {
        while(true){
            Long ret = jedis.get().setnx(LOCK_NAME,REQUEST_ID);
            if (ret==1)
                return true;
        }
    }

可能有人会问,为什么这里不用 jedis.get().get(LOCK_NAME)去判断,这里可以想一下,如果我们两个线程同时执行到tryLock(),但是没有执行lock(),redis里面没有对应的key,这时会出现两个tryLock().get(LOCK_NAME)的值都是false,然后导致两个线程都可以拿到锁的现象,导致线程安全问题,但是用setnx就不会。会先去判断key是否存在然后去设置,所有这时候肯定不会出现相关问题。

对于unlock()操作

  public void unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.get().eval(script, Collections.singletonList(LOCK_NAME), Collections.singletonList(REQUEST_ID));
    }

当然对于unlock()操作,也是一样的,为了保证原子性操作,采用脚本方式去执行操作,保证原子性。当然这里的原子性还是为了防止宕机操作,如果执行到unlock(),del操作之前系统挂死了,则这个key就不会被删除了,便会导致系统死锁问题。

 String value = jedis.get(LOCK_NAME);
        if (REQUEST_ID.equals(value)) {
           jedis.del(LOCK_NAME);
       }

上述这种写法也是有问题的,以后写redis分布式锁的时候还需注意。
在这里插入图片描述

至此,我们一个redis的分布式锁就写好了,一直执行也不会有线程安全问题了。

Redis分布式锁是一种基于Redis的分布式系统中实现互斥访问的机制。它可以确保在分布式环境下,同一时刻只有一个客户端能够持有锁,避免多个客户端同时修改共享资源导致的数据不一致性问题。 以下是Redis分布式锁的基本使用步骤: 1. 客户端尝试获取锁:客户端通过执行Redis的SET命令,尝试将一个特定的键值对设置为锁,并设置一个过期时间。例如,可以使用SETNX命令来设置键值对,只有在键不存在的情况下才会设置成功。 2. 锁的超时时间:为了避免某个客户端获取锁后异常退出或崩溃导致锁无法释放,需要为锁设置一个适当的超时时间。可以使用EXPIRE命令为锁设置过期时间,确保在一定时间后锁会自动释放。 3. 锁持有判断:在尝试获取锁之前,可以先检查锁是否已经被其他客户端持有。可以使用GET命令来获取锁的当前状态,如果返回值为空或者与当前客户端标识相同,则表示锁未被持有,可以获取锁。 4. 锁的释放:当客户端完成对共享资源的访问后,需要及时释放锁,以便其他客户端可以获取锁并继续操作。可以使用DEL命令将锁对应的键值对从Redis中删除,释放锁。 需要注意的是,在使用Redis分布式锁时,需要保证操作的原子性,避免竞争条件的发生。可以使用Lua脚本来将多个命令封装为一个原子操作,以确保获取锁和释放锁的过程是原子的。 此外,在使用Redis分布式锁时,还需要考虑以下几点: - 防止死锁:需要合理设置锁的超时时间,避免锁被持有后无法释放。 - 重入性:如果某个客户端已经持有锁,是否允许它再次获取锁,需要根据具体业务场景来确定。 - 锁粒度:锁的粒度应该尽量小,只锁定必要的共享资源,以提高并发性能。 综上所述,Redis分布式锁是一种常用的实现分布式互斥访问的机制,在分布式系统中起到了重要的作用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值