热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

在Redis分布式锁上,栽的8个跟头

在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中。但不是说用了redis分

在分布式系统中,由于 redis 分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中。

但不是说用了 redis 分布式锁,就可以高枕无忧了,如果没有用好或者用对,也会引来一些意想不到的问题。

今天我们就一起聊聊 redis 分布式锁的一些坑,给有需要的朋友一个参考:

非原子操作


使用 redis 的分布式锁,我们首先想到的可能是 setNx 命令。

if (jedis.setnx(lockKey, val) == 1) {jedis.expire(lockKey, timeout);
}

容易,三下五除二,我们就可以把代码写好。这段代码确实可以加锁成功,但你有没有发现什么问题?

加锁操作和后面的设置超时时间是分开的,并非原子操作。假如加锁成功,但是设置超时时间失败了,该 lockKey 就变成永不失效。

假如在高并发场景中,有大量的 lockKey 加锁成功了,但不会失效,有可能直接导致 redis 内存空间不足。

那么,有没有保证原子性的加锁命令呢?答案是:有,请看下面。

忘了释放锁

上面说到使用 setNx 命令加锁操作和设置超时时间是分开的,并非原子操作。

而在 redis 中还有 set 命令,该命令可以指定多个参数。

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {return true;
}
return false;

其中:

  • lockKey:锁的标识

  • requestId:请求 id

  • NX:只在键不存在时,才对键进行设置操作。

  • PX:设置键的过期时间为 millisecond 毫秒。

  • expireTime:过期时间

set 命令是原子操作,加锁和设置超时时间,一个命令就能轻松搞定。nice!

使用 set 命令加锁,表面上看起来没有问题。但如果仔细想想,加锁之后,每次都要达到了超时时间才释放锁,会不会有点不合理?加锁后,如果不及时释放锁,会有很多问题。

分布式锁更合理的用法是:

  • 手动加锁

  • 业务操作

  • 手动释放锁

  • 如果手动释放锁失败了,则达到超时时间,redis 会自动释放锁。

大致流程图如下:

那么问题来了,如何释放锁呢?伪代码如下:

try{String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {return true;}return false;
} finally {unlock(lockKey);

需要捕获业务代码的异常,然后在 finally 中释放锁。换句话说就是:无论代码执行成功或失败了,都需要释放锁。

此时,有些朋友可能会问:假如刚好在释放锁的时候,系统被重启了,或者网络断线了,或者机房断点了,不也会导致释放锁失败?

这是一个好问题,因为这种小概率问题确实存在。但还记得前面我们给锁设置过超时时间吗?

即使出现异常情况造成释放锁失败,但到了我们设定的超时时间,锁还是会被 redis 自动释放。但只在 finally 中释放锁,就够了吗?

释放了别人的锁

做人要厚道,先回答上面的问题:只在 finally 中释放锁,当然是不够的,因为释放锁的姿势,还是不对。

哪里不对?

答:在多线程场景中,可能会出现释放了别人的锁的情况。

有些朋友可能会反驳:假设在多线程场景中,线程 A 获取到了锁,但如果线程 A 没有释放锁,此时,线程 B 是获取不到锁的,何来释放了别人锁之说?

答:假如线程 A 和线程B,都使用 lockKey 加锁。线程 A 加锁成功了,但是由于业务功能耗时时间很长,超过了设置的超时时间。这时候,redis 会自动释放 lockKey 锁。

此时,线程 B 就能给 lockKey 加锁成功了,接下来执行它的业务操作。恰好这个时候,线程 A 执行完了业务功能,接下来,在 finally 方法中释放了锁 lockKey。这不就出问题了,线程 B 的锁,被线程 A 释放了。

我想这个时候,线程 B 肯定哭晕在厕所里,并且嘴里还振振有词。那么,如何解决这个问题呢?

不知道你们注意到没?在使用 set 命令加锁时,除了使用 lockKey 锁标识,还多设置了一个参数:requestId,为什么要需要记录 requestId 呢?

答:requestId 是在释放锁的时候用的。

伪代码如下:

if (jedis.get(lockKey).equals(requestId)) {jedis.del(lockKey);return true;
}
return false;

在释放锁的时候,先获取到该锁的值(之前设置值就是 requestId),然后判断跟之前设置的值是否相同,如果相同才允许删除锁,返回成功。如果不同,则直接返回失败。

换句话说就是:自己只能释放自己加的锁,不允许释放别人加的锁。

这里为什么要用 requestId,用 userId 不行吗?

答:如果用 userId 的话,对于请求来说并不唯一,多个不同的请求,可能使用同一个 userId。而 requestId 是全局唯一的,不存在加锁和释放锁乱掉的情况。

此外,使用 lua 脚本,也能解决释放了别人的锁的问题:

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

lua 脚本能保证查询锁是否存在和删除锁是原子操作,用它来释放锁效果更好一些。

说到 lua 脚本,其实加锁操作也建议使用 lua 脚本:

if (redis.call('exists', KEYS[1]) == 0) thenredis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; 
end
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; 
end; 
return redis.call('pttl', KEYS[1]);

这是 redisson 框架的加锁代码,写的不错,大家可以借鉴一下。有趣,下面还有哪些好玩的东西?

大量失败请求

上面的加锁方法看起来好像没有问题,但如果你仔细想想,如果有 1 万的请求同时去竞争那把锁,可能只有一个请求是成功的,其余的 9999 个请求都会失败。

在秒杀场景下,会有什么问题?

答:每 1 万个请求,有 1 个成功。再 1 万个请求,有 1 个成功。如此下去,直到库存不足。这就变成均匀分布的秒杀了,跟我们想象中的不一样。

如何解决这个问题呢?

此外,还有一种场景:比如,有两个线程同时上传文件到 sftp,上传文件前先要创建目录。

假设两个线程需要创建的目录名都是当天的日期,比如:20210920,如果不做任何控制,直接并发的创建目录,第二个线程必然会失败。

这时候有些朋友可能会说:这还不容易,加一个 redis 分布式锁就能解决问题了,此外再判断一下,如果目录已经存在就不创建,只有目录不存在才需要创建。

伪代码如下:

try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(!exists(path)) {mkdir(path);}return true;}
} finally{unlock(lockKey,requestId);
}  
return false;

一切看似美好,但经不起仔细推敲。来自灵魂的一问:第二个请求如果加锁失败了,接下来,是返回失败,还是返回成功呢?

主要流程图如下:

显然第二个请求,肯定是不能返回失败的,如果返回失败了,这个问题还是没有被解决。如果文件还没有上传成功,直接返回成功会有更大的问题。头疼,到底该如何解决呢?

答:使用自旋锁。

try {Long start = System.currentTimeMillis();while(true) {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(!exists(path)) {mkdir(path);}return true;}long time = System.currentTimeMillis() - start;if (time>=timeout) {return false;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}
} finally{unlock(lockKey,requestId);
}  
return false;

在规定的时间,比如 500 毫秒内,自旋不断尝试加锁(说白了,就是在死循环中,不断尝试加锁),如果成功则直接返回。

如果失败,则休眠 50 毫秒,再发起新一轮的尝试。如果到了超时时间,还未加锁成功,则直接返回失败。好吧,学到一招了,还有吗?

锁重入问题

我们都知道 redis 分布式锁是互斥的。假如我们对某个 key 加锁了,如果该 key 对应的锁还没失效,再用相同 key 去加锁,大概率会失败。

没错,大部分场景是没问题的。为什么说是大部分场景呢?

因为还有这样的场景:假设在某个请求中,需要获取一颗满足条件的菜单树或者分类树。我们以菜单为例,这就需要在接口中从根节点开始,递归遍历出所有满足条件的子节点,然后组装成一颗菜单树。

需要注意的是菜单不是一成不变的,在后台系统中运营同学可以动态添加、修改和删除菜单。为了保证在并发的情况下,每次都可能获取最新的数据,这里可以加 redis 分布式锁。

加 redis 分布式锁的思路是对的。但接下来问题来了,在递归方法中递归遍历多次,每次都是加的同一把锁。

递归第一层当然是可以加锁成功的,但递归第二层、第三层...第 N 层,不就会加锁失败了?

递归方法中加锁的伪代码如下:

private int expireTime &#61; 1000;public void fun(int level,String lockKey,String requestId){try{String result &#61; jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if(level<&#61;10){this.fun(&#43;&#43;level,lockKey,requestId);} else {return;}}return;} finally {unlock(lockKey,requestId);}
}

如果你直接这么用&#xff0c;看起来好像没有问题。但最终执行程序之后发现&#xff0c;等待你的结果只有一个&#xff1a;出现异常。

因为从根节点开始&#xff0c;第一层递归加锁成功&#xff0c;还没释放锁&#xff0c;就直接进入第二层递归。因为锁名为 lockKey&#xff0c;并且值为 requestId 的锁已经存在&#xff0c;所以第二层递归大概率会加锁失败&#xff0c;然后返回到第一层。第一层接下来正常释放锁&#xff0c;然后整个递归方法直接返回了。

这下子&#xff0c;大家知道出现什么问题了吧&#xff1f;没错&#xff0c;递归方法其实只执行了第一层递归就返回了&#xff0c;其他层递归由于加锁失败&#xff0c;根本没法执行。

那么这个问题该如何解决呢&#xff1f;

答&#xff1a;使用可重入锁。

我们以 redisson 框架为例&#xff0c;它的内部实现了可重入锁的功能。古时候有句话说得好&#xff1a;为人不识陈近南&#xff0c;便称英雄也枉然。

我说&#xff1a;分布式锁不识 redisson&#xff0c;便称好锁也枉然。哈哈哈&#xff0c;只是自娱自乐一下。由此可见&#xff0c;redisson 在 redis 分布式锁中的江湖地位很高。

伪代码如下&#xff1a;

private int expireTime &#61; 1000;public void run(String lockKey) {RLock lock &#61; redisson.getLock(lockKey);this.fun(lock,1);
}public void fun(RLock lock,int level){try{lock.lock(5, TimeUnit.SECONDS);if(level<&#61;10){this.fun(lock,&#43;&#43;level);} else {return;}} finally {lock.unlock();}
}

上面的代码也许并不完美&#xff0c;这里只是给了一个大致的思路&#xff0c;如果大家有这方面需求的话&#xff0c;以上代码仅供参考。接下来&#xff0c;聊聊 redisson 可重入锁的实现原理。

加锁主要是通过以下脚本实现的&#xff1a;

if (redis.call(&#39;exists&#39;, KEYS[1]) &#61;&#61; 0) 
then  redis.call(&#39;hset&#39;, KEYS[1], ARGV[2], 1);        redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]); return nil; 
end;
if (redis.call(&#39;hexists&#39;, KEYS[1], ARGV[2]) &#61;&#61; 1) 
then  redis.call(&#39;hincrby&#39;, KEYS[1], ARGV[2], 1); redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]); return nil; 
end;
return redis.call(&#39;pttl&#39;, KEYS[1]);

其中&#xff1a;

  • KEYS[1]&#xff1a;锁名

  • ARGV[1]&#xff1a;过期时间

  • ARGV[2]&#xff1a;uuid &#43; ":" &#43; threadId&#xff0c;可认为是 requestId

先判断如果锁名不存在&#xff0c;则加锁。接下来&#xff0c;判断如果锁名和 requestId 值都存在&#xff0c;则使用 hincrby 命令给该锁名和 requestId 值计数&#xff0c;每次都加 1。

注意一下&#xff0c;这里就是重入锁的关键&#xff0c;锁重入一次值就加 1。如果锁名存在&#xff0c;但值不是 requestId&#xff0c;则返回过期时间。

释放锁主要是通过以下脚本实现的&#xff1a;

if (redis.call(&#39;hexists&#39;, KEYS[1], ARGV[3]) &#61;&#61; 0) 
then return nil
end
local counter &#61; redis.call(&#39;hincrby&#39;, KEYS[1], ARGV[3], -1);
if (counter > 0) 
then redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[2]); return 0; else redis.call(&#39;del&#39;, KEYS[1]); redis.call(&#39;publish&#39;, KEYS[2], ARGV[1]); return 1; 
end; 
return nil

先判断如果锁名和 requestId 值不存在&#xff0c;则直接返回。如果锁名和 requestId 值存在&#xff0c;则重入锁减 1。

如果减 1 后&#xff0c;重入锁的 value 值还大于 0&#xff0c;说明还有引用&#xff0c;则重试设置过期时间。如果减 1 后&#xff0c;重入锁的 value 值还等于 0&#xff0c;则可以删除锁&#xff0c;然后发消息通知等待线程抢锁。

再次强调一下&#xff0c;如果你们系统可以容忍数据暂时不一致&#xff0c;有些场景不加锁也行&#xff0c;我在这里只是举个例子&#xff0c;本节内容并不适用于所有场景。

锁竞争问题

如果有大量需要写入数据的业务场景&#xff0c;使用普通的 redis 分布式锁是没有问题的。但如果有些业务场景&#xff0c;写入的操作比较少&#xff0c;反而有大量读取的操作。这样直接使用普通的 redis 分布式锁&#xff0c;会不会有点浪费性能&#xff1f;

我们都知道&#xff0c;锁的粒度越粗&#xff0c;多个线程抢锁时竞争就越激烈&#xff0c;造成多个线程锁等待的时间也就越长&#xff0c;性能也就越差。

所以&#xff0c;提升 redis 分布式锁性能的第一步&#xff0c;就是要把锁的粒度变细。

| 读写锁

众所周知&#xff0c;加锁的目的是为了保证&#xff0c;在并发环境中读写数据的安全性&#xff0c;即不会出现数据错误或者不一致的情况。

但在绝大多数实际业务场景中&#xff0c;一般是读数据的频率远远大于写数据。而线程间的并发读操作是并不涉及并发安全问题&#xff0c;我们没有必要给读操作加互斥锁&#xff0c;只要保证读写、写写并发操作上锁是互斥的就行&#xff0c;这样可以提升系统的性能。

我们以 redisson 框架为例&#xff0c;它内部已经实现了读写锁的功能。

读锁的伪代码如下&#xff1a;

RReadWriteLock readWriteLock &#61; redisson.getReadWriteLock("readWriteLock");
RLock rLock &#61; readWriteLock.readLock();
try {rLock.lock();//业务操作
} catch (Exception e) {log.error(e);
} finally {rLock.unlock();
}

写锁的伪代码如下&#xff1a;

RReadWriteLock readWriteLock &#61; redisson.getReadWriteLock("readWriteLock");
RLock rLock &#61; readWriteLock.writeLock();
try {rLock.lock();//业务操作
} catch (InterruptedException e) {log.error(e);
} finally {rLock.unlock();
}

将读锁和写锁分开&#xff0c;最大的好处是提升读操作的性能&#xff0c;因为读和读之间是共享的&#xff0c;不存在互斥性。

而我们的实际业务场景中&#xff0c;绝大多数数据操作都是读操作。所以&#xff0c;如果提升了读操作的性能&#xff0c;也就会提升整个锁的性能。

下面总结一个读写锁的特点&#xff1a;

  • 读与读是共享的&#xff0c;不互斥

  • 读与写互斥

  • 写与写互斥

| 锁分段

此外&#xff0c;为了减小锁的粒度&#xff0c;比较常见的做法是将大锁&#xff1a;分段。

在 java 中 ConcurrentHashMap&#xff0c;就是将数据分为 16 段&#xff0c;每一段都有单独的锁&#xff0c;并且处于不同锁段的数据互不干扰&#xff0c;以此来提升锁的性能。

放在实际业务场景中&#xff0c;我们可以这样做&#xff1a;比如在秒杀扣库存的场景中&#xff0c;现在的库存中有 2000 个商品&#xff0c;用户可以秒杀。为了防止出现超卖的情况&#xff0c;通常情况下&#xff0c;可以对库存加锁。如果有 1W 的用户竞争同一把锁&#xff0c;显然系统吞吐量会非常低。

为了提升系统性能&#xff0c;我们可以将库存分段&#xff0c;比如&#xff1a;分为 100 段&#xff0c;这样每段就有 20 个商品可以参与秒杀。

在秒杀的过程中&#xff0c;先把用户 id 获取 hash 值&#xff0c;然后除以 100 取模。模为 1 的用户访问第 1 段库存&#xff0c;模为 2 的用户访问第 2 段库存&#xff0c;模为 3 的用户访问第 3 段库存&#xff0c;后面以此类推&#xff0c;到最后模为 100 的用户访问第 100 段库存。

如此一来&#xff0c;在多线程环境中&#xff0c;可以大大的减少锁的冲突。以前多个线程只能同时竞争 1 把锁&#xff0c;尤其在秒杀的场景中&#xff0c;竞争太激烈了&#xff0c;简直可以用惨绝人寰来形容&#xff0c;其后果是导致绝大数线程在锁等待。现在多个线程同时竞争 100 把锁&#xff0c;等待的线程变少了&#xff0c;从而系统吞吐量也就提升了。

需要注意的地方是&#xff1a;将锁分段虽说可以提升系统的性能&#xff0c;但它也会让系统的复杂度提升不少。因为它需要引入额外的路由算法&#xff0c;跨段统计等功能。我们在实际业务场景中&#xff0c;需要综合考虑&#xff0c;不是说一定要将锁分段。

锁超时问题

我在前面提到过&#xff0c;如果线程 A 加锁成功了&#xff0c;但是由于业务功能耗时时间很长&#xff0c;超过了设置的超时时间&#xff0c;这时候 redis 会自动释放线程 A 加的锁。

有些朋友可能会说&#xff1a;到了超时时间&#xff0c;锁被释放了就释放了呗&#xff0c;对功能又没啥影响。

答&#xff1a;错&#xff0c;错&#xff0c;错。对功能其实有影响。

通常我们加锁的目的是&#xff1a;为了防止访问临界资源时&#xff0c;出现数据异常的情况。比如&#xff1a;线程 A 在修改数据 C 的值&#xff0c;线程 B 也在修改数据 C 的值&#xff0c;如果不做控制&#xff0c;在并发情况下&#xff0c;数据 C 的值会出问题。

为了保证某个方法&#xff0c;或者段代码的互斥性&#xff0c;即如果线程 A 执行了某段代码&#xff0c;是不允许其他线程在某一时刻同时执行的&#xff0c;我们可以用 synchronized 关键字加锁。

但这种锁有很大的局限性&#xff0c;只能保证单个节点的互斥性。如果需要在多个节点中保持互斥性&#xff0c;就需要用 redis 分布式锁。

做了这么多铺垫&#xff0c;现在回到正题。假设线程 A 加 redis 分布式锁的代码&#xff0c;包含代码 1 和代码 2 两段代码。

由于该线程要执行的业务操作非常耗时&#xff0c;程序在执行完代码 1 的时&#xff0c;已经到了设置的超时时间&#xff0c;redis 自动释放了锁。而代码 2 还没来得及执行。

此时&#xff0c;代码 2 相当于裸奔的状态&#xff0c;无法保证互斥性。假如它里面访问了临界资源&#xff0c;并且其他线程也访问了该资源&#xff0c;可能就会出现数据异常的情况。&#xff08;PS&#xff1a;我说的访问临界资源&#xff0c;不单单指读取&#xff0c;还包含写入&#xff09;

那么&#xff0c;如何解决这个问题呢&#xff1f;

答&#xff1a;如果达到了超时时间&#xff0c;但业务代码还没执行完&#xff0c;需要给锁自动续期。

我们可以使用 TimerTask 类&#xff0c;来实现自动续期的功能&#xff1a;

Timer timer &#61; new Timer(); 
timer.schedule(new TimerTask() {&#64;Overridepublic void run(Timeout timeout) throws Exception {//自动续期逻辑}
}, 10000, TimeUnit.MILLISECONDS);

获取锁之后&#xff0c;自动开启一个定时任务&#xff0c;每隔 10 秒钟&#xff0c;自动刷新一次过期时间。这种机制在 redisson 框架中&#xff0c;有个比较霸气的名字&#xff1a;watch dog&#xff0c;即传说中的看门狗。

当然自动续期功能&#xff0c;我们还是优先推荐使用 lua 脚本实现&#xff0c;比如&#xff1a;

if (redis.call(&#39;hexists&#39;, KEYS[1], ARGV[2]) &#61;&#61; 1) then redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]);return 1; 
end;
return 0;

需要注意的地方是&#xff1a;在实现自动续期功能时&#xff0c;还需要设置一个总的过期时间&#xff0c;可以跟 redisson 保持一致&#xff0c;设置成 30 秒。如果业务代码到了这个总的过期时间&#xff0c;还没有执行完&#xff0c;就不再自动续期了。

自动续期的功能是获取锁之后开启一个定时任务&#xff0c;每隔 10 秒判断一下锁是否存在&#xff0c;如果存在&#xff0c;则刷新过期时间。如果续期 3 次&#xff0c;也就是 30 秒之后&#xff0c;业务方法还是没有执行完&#xff0c;就不再续期了。

主从复制的问题

上面花了这么多篇幅介绍的内容&#xff0c;对单个 redis 实例是没有问题的。

but&#xff0c;如果 redis 存在多个实例。比如&#xff1a;做了主从&#xff0c;或者使用了哨兵模式&#xff0c;基于 redis 的分布式锁的功能&#xff0c;就会出现问题。

具体是什么问题&#xff1f;假设 redis 现在用的主从模式&#xff0c;1 个 master 节点&#xff0c;3 个 slave 节点。master 节点负责写数据&#xff0c;slave 节点负责读数据。

本来是和谐共处&#xff0c;相安无事的。redis 加锁操作&#xff0c;都在 master 上进行&#xff0c;加锁成功后&#xff0c;再异步同步给所有的 slave。

突然有一天&#xff0c;master 节点由于某些不可逆的原因&#xff0c;挂掉了。这样需要找一个 slave 升级为新的 master 节点&#xff0c;假如 slave1 被选举出来了。

如果有个锁 A 比较悲催&#xff0c;刚加锁成功 master 就挂了&#xff0c;还没来得及同步到 slave1。

这样会导致新 master 节点中的锁 A 丢失了。后面&#xff0c;如果有新的线程&#xff0c;使用锁 A 加锁&#xff0c;依然可以成功&#xff0c;分布式锁失效了。

那么&#xff0c;如何解决这个问题呢&#xff1f;

答&#xff1a;redisson 框架为了解决这个问题&#xff0c;提供了一个专门的类&#xff1a;RedissonRedLock&#xff0c;使用了 Redlock 算法。

RedissonRedLock 解决问题的思路如下&#xff1a;

  • 需要搭建几套相互独立的 redis 环境&#xff0c;假如我们在这里搭建了 5 套。

  • 每套环境都有一个 redisson node 节点。

  • 多个 redisson node 节点组成了 RedissonRedLock。

  • 环境包含&#xff1a;单机、主从、哨兵和集群模式&#xff0c;可以是一种或者多种混合。

在这里我们以主从为例&#xff0c;架构图如下&#xff1a;

RedissonRedLock 加锁过程如下&#xff1a;

  • 获取所有的 redisson node 节点信息&#xff0c;循环向所有的 redisson node 节点加锁&#xff0c;假设节点数为 N&#xff0c;例子中 N 等于 5。

  • 如果在 N 个节点当中&#xff0c;有 N/2&#43;1 个节点加锁成功了&#xff0c;那么整个 RedissonRedLock 加锁是成功的。

  • 如果在 N 个节点当中&#xff0c;小于 N/2&#43;1 个节点加锁成功&#xff0c;那么整个 RedissonRedLock 加锁是失败的。

  • 如果中途发现各个节点加锁的总耗时&#xff0c;大于等于设置的最大等待时间&#xff0c;则直接返回失败。

从上面可以看出&#xff0c;使用 Redlock 算法&#xff0c;确实能解决多实例场景中&#xff0c;假如 master 节点挂了&#xff0c;导致分布式锁失效的问题。

但也引出了一些新问题&#xff0c;比如&#xff1a;

  • 需要额外搭建多套环境&#xff0c;申请更多的资源&#xff0c;需要评估一下成本和性价比。

  • 如果有 N 个 redisson node 节点&#xff0c;需要加锁 N 次&#xff0c;最少也需要加锁 N/2&#43;1 次&#xff0c;才知道 redlock 加锁是否成功。显然&#xff0c;增加了额外的时间成本&#xff0c;有点得不偿失。

由此可见&#xff0c;在实际业务场景&#xff0c;尤其是高并发业务中&#xff0c;RedissonRedLock 其实使用的并不多。在分布式环境中&#xff0c;CAP 是绕不过去的。

CAP 指的是在一个分布式系统中&#xff1a;

  • 一致性&#xff08;Consistency&#xff09;

  • 可用性&#xff08;Availability&#xff09;

  • 分区容错性&#xff08;Partition tolerance&#xff09;

这三个要素最多只能同时实现两点&#xff0c;不可能三者兼顾。

如果你的实际业务场景&#xff0c;更需要的是保证数据一致性。那么请使用 CP 类型的分布式锁&#xff0c;比如&#xff1a;zookeeper&#xff0c;它是基于磁盘的&#xff0c;性能可能没那么好&#xff0c;但数据一般不会丢。

如果你的实际业务场景&#xff0c;更需要的是保证数据高可用性。那么请使用 AP 类型的分布式锁&#xff0c;比如&#xff1a;redis&#xff0c;它是基于内存的&#xff0c;性能比较好&#xff0c;但有丢失数据的风险。

其实&#xff0c;在我们绝大多数分布式业务场景中&#xff0c;使用 redis 分布式锁就够了&#xff0c;真的别太较真。因为数据不一致问题&#xff0c;可以通过最终一致性方案解决。但如果系统不可用了&#xff0c;对用户来说是暴击一万点伤害。

 

 


推荐阅读
  • 图像因存在错误而无法显示 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 前段时间做一个项目,需求是对每个视频添加预览图,这个问题最终选择方案是:用canvas.toDataYRL();来做转换获取视频的一个截图,添加到页面中,达到自动添加预览图的目的。 ... [详细]
  • Python中的PyInputPlus模块原文:https ... [详细]
  • 基于分布式锁的防止重复请求解决方案
    一、前言关于重复请求,指的是我们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求如果是幂等的(每次请求的结果都相同,如查 ... [详细]
  • python+selenium十:基于原生selenium的二次封装fromseleniumimportwebdriverfromselenium.webdriv ... [详细]
author-avatar
胃热额外_522
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有