基于redis的分布式锁

前言

多线程下的数据一致性问题一直都是热点问题,既要考虑到数据的一致,又要考虑实现的效率,在分布式情况下,这又要成为一种新的难题。
分布式锁和我们java基础中学习到的synchronized略有不同,在synchronized中我们的锁是个对象,如果一个线程拿到了该锁,别的线程就只
能等待了。比如购物下单这种业务场景,下单的系统部署在不同的服务实例上,单纯使用synchronized或者lock 已经无法满足对库存一致性的
判断,所以分布式锁由此诞生,其本质  加锁/释放锁的操作 不再当前服务实现了。

如今常见的锁方案如下:
基于数据库实现分布式锁
基于缓存,实现分布式锁,如redis
基于Zookeeper实现分布式锁

由于实践中使用的是基于redis 的分布式锁, 所以今天主要说说redis 的分布式说

redis分布式锁的实现

SETNX
使用redis的SETNX实现分布式锁,多个进程执行以下Redis命令:
SETNX
是将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
返回1,说明该进程获得锁,
返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。

基础版1.0

先来一个最简单版本

 public void lock() {
    while(true){
        String result = jedis.set(lockKey, "value", NOT_EXIST);
        if(OK.equals(result)){
            System.out.println(Thread.currentThread().getId()+"加锁成功!");
            break;
        }
    }
  }

而unlock方法就是调用DEL命令将键删除。

但是这样会存在一个问题
假如服务器A在加锁成功后 服务器宕机或者 网络断了 等等。。。 总之断开与redis 的连接,那么服务器BCDEF永远都无法获取锁了

带着这个问题 升级到1.1版本

基础版1.1设置锁的过期时间

public void lock() {
    while(true){
        String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);
        if(OK.equals(result)){
            System.out.println(Thread.currentThread().getId()+"加锁成功!");
            break;
        }
    }
}


注:要保证设置过期时间和设置锁具有原子性,防止设置过期时间时前服务器断开连接

但是这样还是会存在一个问题
步骤如下

服务器A获取锁成功,过期时间10秒。
服务器A在某个操作上阻塞了30秒。
10秒时间到了,锁自动释放了。
服务器B获取到了对应同一个资源的锁。
服务器A从阻塞中恢复过来,释放掉了服务器B持有的锁。

基础版1.2 设置带Value的锁

在1.1的基础上 解决自己的锁自己释放 , 即自己的坑自己填
直接上代码

	public void lock() {
	    while(true){
	        String result = jedis.set(lockKey, requestId, NOT_EXIST,SECONDS,30);
	        if(OK.equals(result)){
	            System.out.println(Thread.currentThread().getId()+"加锁成功!");
	            break;
	        }
	    }
	}

requestId 就是每个线程自己生成一个唯一标识, 再释放锁的时候 判断这个锁是否是自己加的

	public void unlock() {
	    String requestId = jedis.get(lockKey);
	    if (requestId .equals(requestId )){
	        jedis.del(lockKey);
	    }
	}

看着好像没什么问题,但是, 这里又一个但是,获取值、判断和删除锁这是三步操作,在多线程的情况下,就会出现问题
所以要确保解锁过程是原子型操作。 噔噔噔 上神器 <redis的lua脚本>

入门版 ->lua 初窥

先说说什么是lua 脚本?

​ Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。 Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。 Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。

上面巴拉巴拉说了一堆总结起来就是 体积很小,运行速度很快,并且每次的执行都是作为一个原子事务来执行的。重点,重点,重点,速度快,每次执行都是一个原子事务。 如果能把上面 的取值 判断 删除三步放在一个执行事务中,不就解决问题了吗???

问题1:解决 自己的锁自己释放问题

lua 的作用就是为了解决 在解锁中获取值、判断、删除这三步操作 合成一步执行,在执行lua脚本的时候把requestId 传进去, 作为ARGV[1] , KEYS[1]为锁的key,不多逼逼,上代码

    //一个脚本 既有取值 又有if else 判断  还有具体操作,  神器啊

	if redis.call("get",KEYS[1]) == ARGV[1] then
	    return redis.call("del",KEYS[1])
	else
	    return 0
	end

问题2:解决业务操作时间大于自动释放时间

业务操作的时间长短的确具有不确定性,但是按照正常来说,一个业务,开始前先获取锁,成功拿到锁后,执行业务操作,执行完成删除锁。假设想象每个请求都需要执行完成,不在乎它的时间长短,那么在服务步不宕机的情况下,锁必由自己来释放,如果可以启动一个线程,每隔一段时间不断查询这个key是否过期,如果没有,那么就延长一定时间。在验证key 是否过期的时候也必须 考虑多线程访问,所以还是需要使用到lua脚本 获取值 ,判断, 延期,看着是不是和上面的很像,上代码比较比较

	if redis.call("get",KEYS[1]) == ARGV[1] then
		return redis.call('expire',KEYS[1],ARGV[2])
	else
		 return 0
	end

那么现在只需要启动一个线程 隔断时间查询某个key 都否过期 ,执行上面这段脚本,就可以完美的解决这个问题了。

刷新key的过期时间
	  
	private class ExpirationRenewal implements Runnable{
	        @Override
	        public void run() {
	            while (isOpenExpirationRenewal){
	                System.out.println("执行延迟失效时间中...");

                String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 end";
                jedis.eval(checkAndExpireScript, 1, lockKey, requestId , "30");

                //休眠10秒
                sleepBySencond(10);
            }
        }
    }

完美开源解决方案

刚才说了手写一个redis分布式事务锁需要考虑的茫茫多的东西,那么有已经开源已经写好的吗?说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的。

redisson,下面是它的官方wiki ,大家有兴趣可以看看 redisssion 官网wiki

下面给大家看一段简单的使用代码片段,先直观的感受一下:

RLock lock = redisson.getLock("myLock");
// Most familiar locking method
lock.lock();

lock.unlock();
RLock lock = redisson.getLock("myLock");

lock.lock();
 
lock.unlock();

是不是特别清爽,没有自己写的茫茫多的判断,

此外,人家还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。

redission实现分布式锁原理

随便从网上找的一张图
(1)加锁机制
咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。这里注意,仅仅只是选择一台机器!这点很关键!

紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:

在这里插入图片描述
是不是很眼熟,这就是上面提到的lua脚本

参数解析

KEYS[1] 代表的是你加锁的那个key,比如说:RLock lock = redisson.getLock(“myLock”);这里你自己设置了加锁的那个锁key就是“myLock”。

ARGV[1] 代表的就是锁key的默认生存时间,默认30秒。

ARGV[2] 代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1 (本质其实就是uuid+线程id)

这个lua和咱们自己手写的好像还不太一样,咱们一行行的来看看

第一个if

第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。

使用到了一个hash的结构 ,加锁命令如下:

hset myLock   8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

加完后的数据结构

myLock:{
    "8743c9c0-0795-4907-87fd-6c719a6b4586:1": 1
}

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
到此就第一个if 执行完成

好了,ok,加锁完成了。

第二个if

在先说第二个if 前,先看一种情况

RLock lock = redisson.getLock("myLock");
lock.lock();
lock.lock();

lock.unlock();
lock.unlock();

在这种情况下 第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行

incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

通过这个命令,对客户端1的加锁次数,累加1。

此时myLock数据结构变为下面这样:

myLock:{
    "8743c9c0-0795-4907-87fd-6c719a6b4586:1": 2
}

大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

这种情况就是所谓的可重入锁,表现在同一个线程可以多次获得锁,而不同线程依然不可多次获得锁

还有一个return哦

也就是说前面两个if 都不满足的情况下,就会走向第三个return, 想象以下:
不满足第一个if,也就说已经有其他客户端加锁成功,
不满足第二个if,也就说不是这个客户端加的锁
也就说已经有其他客户端加锁成功,这个时候就会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。
此时当前会进入一个while循环,不停的尝试加锁。

看门狗

刚才咱们自己实现分布式锁的时候好像还启动一个线程监控自己的key是否实现延期,
其实redission 也有,watchdog机制
一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

unlock

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

刚才解析第二个if 我们发现hash 对应的value 就是加锁数量

其实每次unlock ,都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值