14_Redis 分布式锁

是什么

  • 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
  • 进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
  • 分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问

使用条件

  • 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)
  • 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)
  • 同步访问(即有很多个进程同时访问同一个共享资源。)

应用场景

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

  • 如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
  • 如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

分布式锁可以基于很多种方式实现,比如zookeeper、redis…

不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识


Redis实现分布锁

原理

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

redis的SETNX命令可以方便的实现分布式锁。

1)setNX(SET if Not eXists)

SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在

若给定的 key 已经存在,则 SETNX 不做任何动作

  • 设置成功,返回 1 。
  • 设置失败,返回 0 。

在这里插入图片描述


所以我们使用执行下面的命令SETNX可以用作加锁原语(locking primitive)。

比如说,要对关键字(key)foo 加锁,客户端可以尝试以下方式:

SETNX lock.foo <current Unix time + lock timeout + 1>
  • 如果 SETNX返回 1 ,说明客户端已经获得了锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。 之后客户端可以通过 DEL lock.foo 来释放锁。
  • 如果 SETNX返回 0 ,说明 key 已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。

2)getSET

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

GETSET key value

将给定 key 的值设为 value ,并返回 key 的旧值(old value)。

当 key 存在但不是字符串类型时,返回一个错误。

返回值:

返回给定 key 的旧值[之前的值]。

当 key 没有旧值时,也即是, key 不存在时,返回 nil 。

192.168.77.130:7002> getset lock "lock_2"
"lock_1"  -->返回旧的value
192.168.77.130:7002> get lock
"lock_2"

回答面试的核心点

1、同一时刻只能有一个进程获取到锁。setnx

2、释放锁:锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;(最简单的方式就是del, 如果在删除之前死锁了。)

在这里插入图片描述


返回0时的正确做法:

C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0 C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。 反之,如果已超时,C3通过下面的操作来尝试获得锁: GETSET lock.foo <current Unix time + lock timeout + 1> 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。

留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略

不计。


为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。


示例:

public static boolean lock(String lockName) {
    Jedis jedis = RedisPool.getJedis(); //lockName
    // 可以为共享变量名,也可以为方法名,主要是用于模拟锁信息
    System.out.println(Thread.currentThread() + "开始尝试加锁!");
    Long result = jedis.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
    if (result != null && result.intValue() == 1) {
        System.out.println(Thread.currentThread() + "加锁成功!");
        jedis.expire(lockName, 5);
        //TODO ...
        System.out.println(Thread.currentThread() + "执行业务逻辑!");
        jedis.del(lockName);
        return true;
    } else {//判断是否死锁 
        String lockValueA = jedis.get(lockName); 
        //得到锁的过期时间,判断小于当前时间,说明已超时但是没释放锁,通过下面的操作来尝试获得锁。下面 逻辑防止死锁[已经过期但是没有释放锁的情况]
        if (lockValueA != null && Long.parseLong(lockValueA) < System.currentTimeMillis()) {
            String lockValueB = jedis.getSet(lockName, String.valueOf(System.currentTimeMillis() + 5000)); 
            //这里返回的值是旧值,如果有的话。之前没有值就返回null,设置的是新超时。
            if (lockValueB == null || lockValueB.equals(lockValueA)) {
                System.out.println(Thread.currentThread() + "加锁成功!");
                jedis.expire(lockName, 5);
                System.out.println(Thread.currentThread() + "执行业务逻辑!");
                jedis.del(lockName);
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值