Redis分布式锁实现以及避免死锁

一、什么是分布式锁?

要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

二、分布式锁的使用场景。

线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。

有这样一个情境,线程A和线程B都共享某个变量X。

如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。

如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

三、分布式锁的实现(Redis)

分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器,存储锁信息,这时候我们很容易就想到了Redis。首先我们要搭建一个Redis服务器,用Redis服务器来存储锁信息。

在实现的时候要注意的几个关键点:

1、锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;

2、同一时刻只能有一个线程获取到锁。

几个要用到的redis命令:

setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。

get(key):获得key对应的value值,若不存在则返回nil。

getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。

expire(key, seconds):设置key-value的有效期为seconds秒。

四、死锁问题

基于setnx、getset命令的分布式锁实现过程中如果细节不反复测试,很容易出现死锁问题:
考虑一种情况,如果进程获得锁后,断开了与 Redis 的连接(可能是进程挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其他进程都会处于一直等待的状态,即出现“死锁”。上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:
P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)
P2和P3进程发现锁 lock.foo 已超时
P2执行 DEL lock.foo命令
P2执行 SETNX lock.foo命令,并返回1,即P2获得锁
P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)
P3执行 SETNX lock.foo命令,并返回1,即P3获得锁
P2和P3同时获得了锁
从上面的情况可以得知,在检测到锁超时后,进程不能直接简单地执行 DEL 删除键的操作以获得锁。为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法。
我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。接下来的情况:
进程P4执行 SETNX lock.foo 以尝试获取锁。
由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败。
P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测。
如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值(比如当前时间为6.06,lock.foo设置的时间戳为System.currentTimeMillis()+5s=6.05,6.06>6.05,6.05以前的时间都在有效的过期时间内,说明已经超过了过期时间),P4会执行以下操作。
GETSET lock.foo {key,System.currentTimeMillis()+timeout},return回P1的P1OldTime。
由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否等于getset返回的旧值,如果相等可以判断进程是否已获得锁。如果不相等说明在P4进去之前,可能P5已经先进去修改时间了,导致P4的GETSET返回的旧时间其实是P5设置的时间,自然而然代表P4返回的旧值(其实是P5设置的时间)不等于P1设置的时间,意味着锁已经被P5抢走了,那就继续等待释放锁。这个判断主要用于多个线程进去造成都抢锁成功的问题。

五、代码实现

public class LockUtils{

private LockUtils(){
}

public static boolean lock(String lockName){//lockName可以为共享变量名,也可以为方法名,主要是用于模拟锁信息
    System.out.println(Thread.currentThread() + "开始尝试加锁!");
    Long result = RedisPoolUtil.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
    //setnx这个操作成功并且返回1,表示这个锁没有被别人获取,如果锁在别人手上则会返回0
    if (result != null && result.intValue() == 1){
        System.out.println(Thread.currentThread() + "加锁成功!");
        RedisPoolUtil.expire(lockName, 5);
        System.out.println(Thread.currentThread() + "执行业务逻辑!");
        RedisPoolUtil.del(lockName);
        return true;
    } else { 
    //这里的走向主要判断锁还在被别的线程持有,而且持有的时间已经超过所设置的过期时间。
    //否则表示这把锁已经被人拿走了,因为这里的走向代表返回的不是1
    	//通过key查询value(这个value代表上一个线程持锁线程的时间戳)	
        String lockValueA = RedisPoolUtil.get(lockName);
        //当前时间大于所设置时间戳,表示锁的时间已经过期了
        if (System.currentTimeMillis() >= (lockValueA != null && Long.parseLong(lockValueA)) ){
        //如果锁已经过期了说明锁失效了,就可以重新获取锁,通过getset重新设置时间戳,并且返回上一个持锁线程的时间戳lockValueB
            String lockValueB = RedisPoolUtil.getSet(lockName, String.valueOf(System.currentTimeMillis() + 5000));
            //如果lockValueB !=lockValueA,代表lockValue在getset之前有一条线程已经拿走锁了,然后重新设置了时间戳,所以导致现在这条线程拿到的时间戳和lockValueA对不上。
            if (lockValueB == null || lockValueB.equals(lockValueA)){
                System.out.println(Thread.currentThread() + "加锁成功!");
                RedisPoolUtil.expire(lockName, 5);
                System.out.println(Thread.currentThread() + "执行业务逻辑!");
                RedisPoolUtil.del(lockName);
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

}

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现redis分布式锁的方法如下: 1. 定义一个RedisLock类,该类有如下属性: - redis连接池对象 - 锁的key值 - 锁的value值 - 锁的过期时间 2. 在RedisLock类中定义加锁方法lock()和解锁方法unlock(),具体实现如下: - lock()方法实现: a. 从redis连接池中获取一个redis连接对象,使用setnx命令设置key和value,如果返回值为1,表示加锁成功,返回true;如果返回值为0,表示锁已经被其他线程占用,返回false。 b. 如果加锁失败,需要判断锁是否已经超时,如果没有超时,需要等待一段时间后重新尝试加锁;如果超时,则可以直接加锁。 - unlock()方法实现: a. 从redis连接池中获取一个redis连接对象,使用del命令删除key,释放锁。 b. 如果删除失败,则表示锁已经被其他线程占用,或者已经超时,不需要进行任何操作。 3. 避免死锁的方法: 为了避免死锁,需要在加锁时设置一个过期时间,当锁过期时,自动释放锁,避免出现死锁。 4. 实现redlock算法: 在集群环境下,需要使用redlock算法来实现分布式锁。redlock算法是由redis官方提出的一种分布式锁算法,可以避免redis节点故障、网络分区等问题导致的锁失效。 具体实现如下: a. 获取当前时间戳t1。 b. 依次尝试在N个redis节点上加锁,每个节点的key值不同,value值为当前线程ID,并设置一个过期时间。 c. 获取当前时间戳t2。 d. 如果在大多数redis节点上加锁成功,并且加锁时间小于锁的过期时间,则认为加锁成功。 e. 如果加锁失败,则依次在之前加锁成功的节点上解锁。 f. 如果加锁成功,执行业务逻辑。 g. 在锁过期之前,执行完业务逻辑后,在之前加锁成功的节点上解锁。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值