Java最新Java分布式锁,java链表面试题

最后

这份清华大牛整理的进大厂必备的redis视频、面试题和技术文档

祝大家早日进入大厂,拿到满意的薪资和职级~~~加油!!

感谢大家的支持!!

image.png

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。

private static final int defaultExpire = 60;//默认过期时间

public static boolean lock(String key) {

return lock2(key, defaultExpire);

}

3、执行完业务代码后,可以通过 delete 命令删除 key。

public static void unLock1(String key) {

redis.del(key);

}

这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。

比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。

针对该方法完整代码如下:

package com.talent.api.utils;

import redis.clients.jedis.Jedis;

//redis分布式锁

public final class RedisLockUtil {

private static final int defaultExpire = 60;//默认过期时间

private static final String REMOTEURL = “192.168.1.2”;//redis远程连接

private static Jedis redis = new Jedis(REMOTEURL);//设置远程连接

private RedisLockUtil() {

//

}

/**

  • 加锁

  • @param key redis key

  • @param expire 过期时间,单位秒

  • @return true:加锁成功,false,加锁失败

*/

public static boolean lock(String key, int expire) {

long status = redis.setnx(key, “1”);

if (status == 1) {

redis.expire(key, expire);

return true;

}

return false;

}

public static boolean lock(String key) {

return lock2(key, defaultExpire);

}

public static void unLock1(String key) {

redis.del(key);

}

6.1 基于 REDIS 的 SETNX()、GET()、GETSET()方法做分布式锁


这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化。

1.getset():这个命令主要有两个参数 getset(key,newValue)。

该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:

  • getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1

  • getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2

  • 依次类推!

使用步骤:

  • setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2。

在这里插入图片描述

  • get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。

在这里插入图片描述

  • 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。

  • 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

在这里插入图片描述

  • 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

public static void unLock2(String key) {

long oldExpireTime = Long.parseLong(redis.get(key));

if (oldExpireTime > System.currentTimeMillis()) {

redis.del(key);

}

}

完整代码如下:

package com.talent.api.utils;

import redis.clients.jedis.Jedis;

//redis分布式锁

public final class RedisLockUtil {

private static final int defaultExpire = 60;//默认过期时间

private static final String REMOTEURL = “192.168.1.2”;//redis远程连接

private static Jedis redis = new Jedis(REMOTEURL);//设置远程连接

private RedisLockUtil() {

//

}

/**

  • 加锁

  • @param key redis key

  • @param expire 过期时间,单位秒

  • @return true:加锁成功,false,加锁失败

*/

public static boolean lock(String key, int expire) {

long status = redis.setnx(key, “1”);

if (status == 1) {

redis.expire(key, expire);

return true;

}

return false;

}

public static boolean lock(String key) {

return lock2(key, defaultExpire);

}

/**

  • 加锁

  • @param key redis key

  • @param expire 过期时间,单位秒

  • @return true:加锁成功,false,加锁失败

*/

public static boolean lock2(String key, int expire) {

long value = System.currentTimeMillis() + expire;

long status = redis.setnx(key, String.valueOf(value));

if (status == 1) {

return true;

}

long oldExpireTime = Long.parseLong(redis.get(key));

if (oldExpireTime < System.currentTimeMillis()) {

//超时

long newExpireTime = System.currentTimeMillis() + expire;

long currentExpireTime = Long.parseLong(redis.getSet(key, String.valueOf(newExpireTime)));

if (currentExpireTime == oldExpireTime) {

return true;

}

}

return false;

}

public static void unLock1(String key) {

redis.del(key);

}

public static void unLock2(String key) {

long oldExpireTime = Long.parseLong(redis.get(key));

if (oldExpireTime > System.currentTimeMillis()) {

redis.del(key);

}

}

}

接下来我们通过一个领红包的场景来实现加锁和释放锁

public void drawRedPacket(long userId) {

RedisLockUtil redisLockUtil = new RedisLockUtil();

String key = “draw.redpacket.userid:” + userId;//每个用户均使用自己的id作为锁的标识

boolean lock = RedisLockUtil.lock2(key, 60);//默认60失效

if (lock) {

try {

//执行领红包操作

System.out.println(“红包领取成功”);

} finally {

//释放锁

redisLockUtil.unLock2(key);

}

} else {

new RuntimeException(“重复领取奖励”);

}

}

7.基于 REDLOCK 做分布式锁

===============================================================================

多节点redis实现的分布式锁算法(RedLock):有效防止单点故障

Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。

算法的步骤如下:

假设有5个完全独立的redis主服务器

  • 1.获取当前时间戳

  • 2.client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。

比如:TTL(Time To Live 中文为:生存时间值,该字段指定IP包被路由器丢弃之前允许通过的最大网段数量,简单理解就是过期时间)为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁

  • 3.client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功

  • 4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);

  • 5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁

算法示意图如下:

在这里插入图片描述

使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。

7.1 RedLock算法是否是异步算法?


可以看成是同步算法;因为 即使进程间(多个电脑间)没有同步时钟,但是每个进程时间流速大致相同;并且时钟漂移相对于TTL叫小,可以忽略,所以可以看成同步算法;(不够严谨,算法上要算上时钟漂移,因为如果两个电脑在地球两端,则时钟漂移非常大)

7.2 RedLock失败重试


当client不能获取锁时,应该在随机时间后重试获取锁;并且最好在同一时刻并发的把set命令发送给所有redis实例;而且对于已经获取锁的client在完成任务后要及时释放锁,这是为了节省时间;

7.3 RedLock释放锁


由于释放锁时会判断这个锁的value是不是自己设置的,如果是才删除;所以在释放锁时非常简单,只要向所有实例都发出释放锁的命令,不用考虑能否成功释放锁;

7.4 RedLock注意点


1.先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移;

2.对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况, 从而使锁失效

3.一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效

7.5 RedLock性能及崩溃恢复的相关解决方法


1.如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;

2.如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒-次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;

3.有效解决既保证锁完全有效性及性能高效及即使断电情况的方法是redis同步到磁盘方式保持默认的每秒,在redis无论因为什么原因停掉后要等待TTL时间后再重启(学名:延迟重启) ;缺点是 在TTL时间内服务相当于暂停状态;

8.基于 REDISSON 做分布式锁

================================================================================

8.1 添加maven依赖


org.redisson

redisson

3.8.2

8.2 用法:


package com.example.lock.utils;

import org.redisson.Redisson;

import org.redisson.api.RLock;

import org.redisson.api.RedissonClient;

import org.redisson.config.Config;

public class RedSession {

public void test() {

Config config = new Config();

config.useClusterServers()

.setScanInterval(2000) // cluster state scan interval in milliseconds

.addNodeAddress(“redis://127.0.0.1:7000”, “redis://127.0.0.1:7001”)

.addNodeAddress(“redis://127.0.0.1:7002”);

RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock(“anyLock”);

lock.lock();

try {

System.out.println(“线程已经加锁”);

} finally {

lock.unlock();

}

}

}

通过上面这段代码,我们看一下Redisson是如何基于Redis实现分布式锁的

8.3 原理解释


加锁

Redisson中提供的加锁的方法有很多,但大致类似,此处只看lock()方法

在这里插入图片描述

在这里插入图片描述

可以看到,调用getLock()方法后实际返回一个RedissonLock对象,在RedissonLock对象的lock()方法主要调用tryAcquire()方法

在这里插入图片描述

由于leaseTime == -1,于是走tryLockInnerAsync()方法,这个方法才是关键

首先,看一下evalWriteAsync方法的定义

<T, R> RFuture evalWriteAsync(String key, Codec codec, RedisCommand evalCommandType, String script, List keys, Object … params);

最后两个参数分别是keys和params

evalWriteAsync具体如何调用的呢?

commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,

"if (redis.call(‘exists’, KEYS[1]) == 0) then " +

"redis.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) then " +

"redis.call(‘hincrby’, KEYS[1], ARGV[2], 1); " +

"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +

"return nil; " +

"end; " +

“return redis.call(‘pttl’, KEYS[1]);”,

Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

结合上面的参数声明,我们可以知道,这里KEYS[1]就是getName(),ARGV[2]是getLockName(threadId)

假设前面获取锁时传的name是“abc”,假设调用的线程ID是Thread-1,假设成员变量UUID类型的id是6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c

那么KEYS[1]=abc,ARGV[2]=6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1

因此,这段代码想表达什么呢?

1、判断有没有一个叫“abc”的key

2、如果没有,则在其下设置一个字段为“6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1”,值为“1”的键值对 ,并设置它的过期时间

3、如果存在,则进一步判断“6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1”是否存在,若存在,则其值加1,并重新设置过期时间

4、返回“abc”的生存时间(毫秒)

这里用的数据结构是hash,hash的结构是: key 字段1 值1 字段2 值2 。。。

用在锁这个场景下,key就表示锁的名称,也可以理解为临界资源,字段就表示当前获得锁的线程

所有竞争这把锁的线程都要判断在这个key下有没有自己线程的字段,如果没有则不能获得锁,如果有,则相当于重入,字段值加1(次数)

算法原理如下图所示:

在这里插入图片描述

解锁

protected RFuture unlockInnerAsync(long threadId) {

return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,

"if (redis.call(‘exists’, KEYS[1]) == 0) then " +

"redis.call(‘publish’, KEYS[2], ARGV[1]); " +

"return 1; " +

“end;” +

"if (redis.call(‘hexists’, KEYS[1], ARGV[3]) == 0) then " +

“return nil;” +

"end; " +

"local counter = redis.call(‘hincrby’, KEYS[1], ARGV[3], -1); " +

"if (counter > 0) then " +

"redis.call(‘pexpire’, KEYS[1], ARGV[2]); " +

"return 0; " +

"else " +

"redis.call(‘del’, KEYS[1]); " +

"redis.call(‘publish’, KEYS[2], ARGV[1]); " +

"return 1; "+

"end; " +

“return nil;”,

Arrays.asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}

我们还是假设name=abc,假设线程ID是Thread-1

同理,我们可以知道

KEYS[1]是getName(),即KEYS[1]=abc

KEYS[2]是getChannelName(),即KEYS[2]=redisson_lock__channel:{abc}

ARGV[1]是LockPubSub.unlockMessage,即ARGV[1]=0

ARGV[2]是生存时间

ARGV[3]是getLockName(threadId),即ARGV[3]=6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1

因此,上面脚本的意思是:

1、判断是否存在一个叫“abc”的key

2、如果不存在,向Channel中广播一条消息,广播的内容是0,并返回1

3、如果存在,进一步判断字段6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1是否存在

4、若字段不存在,返回空,若字段存在,则字段值减1

5、若减完以后,字段值仍大于0,则返回0

6、减完后,若字段值小于或等于0,则广播一条消息,广播内容是0,并返回1;

可以猜测,广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了

在这里插入图片描述

等待

上面的加锁,解锁均是 可以获取到锁资源的情况,那么当无法立即获取锁资源时,就需要等待

@Override

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {

long threadId = Thread.currentThread().getId();

Long ttl = tryAcquire(leaseTime, unit, threadId);

// lock acquired

if (ttl == null) {

return;

}

// 订阅

RFuture future = subscribe(threadId);

commandExecutor.syncSubscription(future);

try {

while (true) {

ttl = tryAcquire(leaseTime, unit, threadId);

// lock acquired

if (ttl == null) {

break;

}

// waiting for message

if (ttl >= 0) {

getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);

} else {

getEntry(threadId).getLatch().acquire();

}

}

} finally {

unsubscribe(future, threadId);

}

// get(lockAsync(leaseTime, unit));

}

protected static final LockPubSub PUBSUB = new LockPubSub();

protected RFuture subscribe(long threadId) {

return PUBSUB.subscribe(getEntryName(), getChannelName(), commandExecutor.getConnectionManager().getSubscribeService());

}

protected void unsubscribe(RFuture future, long threadId) {

PUBSUB.unsubscribe(future.getNow(), getEntryName(), getChannelName(), commandExecutor.getConnectionManager().getSubscribeService());

}

这里会订阅Channel,当资源可用时可以及时知道,并抢占,防止无效的轮询而浪费资源

当资源可用用的时候,循环去尝试获取锁,由于多个线程同时去竞争资源,所以这里用了信号量,对于同一个资源只允许一个线程获得锁,其它的线程阻塞

9.基于 ZooKeeper 做分布式锁

=================================================================================

9.1 为什么要选择zookeeper


1、基于mysql实现分布式锁

基于分布式锁的实现,首先肯定是想单独分离出一台mysql数据库,所有服务要想操作文件(共享资源),那么必须先在mysql数据库中插入一个标志,插入标志的服务就持有了锁,并对文件进行操作,操作完成后,主动删除标志进行锁释放,其与服务会一直查询数据库,看是否标志有被占用,直到没有标志占用时自己才能写入标志获取锁。

但是这样有这么一个问题,如果服务(jvm1)宕机或者卡顿了,会一直持有锁未释放,这样就造成了死锁,因此就需要有一个监视锁进程时刻监视锁的状态,如果超过一定时间未释放就要进行主动清理锁标记,然后供其与服务继续获取锁。

如果监视锁字段进程和jvm1同时挂掉,依旧不能解决死锁问题,于是又增加一个监视锁字段进程,这样一个进程挂掉,还有另一个监视锁字段进程可以对锁进行管理。这样又诞生一个新的问题,两个监视进程必须进行同步,否则对于过期的情况管理存在不一致问题。

因此存在以下问题,并且方案变得很复杂:

1、监视锁字段进程对于锁的监视时间周期过短,仍旧会造成多售(jvm1还没处理完其持有的锁就被主动销毁,造成多个服务同时持有锁进行操作)。

2、监视锁字段进程对于锁的监视时间周期过长,会造成整个服务卡顿过长,吞吐低下。

3、监视锁字段进程间的同步问题。

4、当一个jvm持有锁的时候,其余服务会一直访问数据库查看锁,会造成其余jvm的资源浪费。

在这里插入图片描述

2、基于Redis实现分布式锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点,Redis就是其中一种。由于Redis可以设置字段的有效期,因此可以实现自动释放超期的锁,不需要多个监视锁字段进程进行锁守护,可以依旧存在上述mysql实现中除了3以外1、2、4中的问题。

3、基于Zookeeper实现分布式锁

基于以上两种实现方式,有了基于zookeeper实现分布式锁的方案。由于zookeeper有以下特点:

1️⃣维护了一个有层次的数据节点,类似文件系统。

2️⃣有以下数据节点:临时节点、持久节点、临时有序节点(分布式锁实现基于的数据节点)、持久有序节点。

3️⃣zookeeper可以和client客户端通过心跳的机制保持长连接,如果客户端链接zookeeper创建了一个临时节点,那么这个客户端与zookeeper断开连接后会自动删除。

4️⃣zookeeper的节点上可以注册上用户事件(自定义),如果节点数据删除等事件都可以触发自定义事件。

5️⃣zookeeper保持了统一视图,各服务对于状态信息获取满足一致性。

Zookeeper的每一个节点,都是一个天然的顺序发号器。

在每一个节点下面创建子节点时,只要选择的创建类型是有序(EPHEMERAL_SEQUENTIAL 临时有序或者PERSISTENT_SEQUENTIAL 永久有序)类型,那么,新的子节点后面,会加上一个次序编号。这个次序编号,是上一个生成的次序编号加一

比如,创建一个用于发号的节点“/test/lock”,然后以他为父亲节点,可以在这个父节点下面创建相同前缀的子节点,假定相同的前缀为“/test/lock/seq-”,在创建子节点时,同时指明是有序类型。如果是第一个创建的子节点,那么生成的子节点为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,等等。

在这里插入图片描述

9.2 如何使用zookeeper实现分布锁


大致思想为:每个客户端对某个方法加锁时,在 Zookeeper 上与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。

判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

排它锁

排他锁,又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁。

排他锁核心是保证当前有且仅有一个事务获得锁,并且锁释放之后,所有正在等待获取锁的事务都能够被通知到。

Zookeeper 的强一致性特性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。可以利用Zookeeper这个特性,实现排他锁。

1️⃣定义锁:通过Zookeeper上的数据节点来表示一个锁

2️⃣获取锁:客户端通过调用 create 方法创建表示锁的临时节点,可以认为创建成功的客户端获得了锁,同时可以让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况

3️⃣释放锁:以下两种情况都可以让锁释放

当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除

正常执行完业务逻辑,客户端主动删除自己创建的临时节点

基于Zookeeper实现排他锁流程:

在这里插入图片描述

共享锁

共享锁,又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。

共享锁与排他锁的区别在于,加了排他锁之后,数据对象只对当前事务可见,而加了共享锁之后,数据对象对所有事务都可见。

1️⃣定义锁:通过Zookeeper上的数据节点来表示一个锁,是一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点

2️⃣获取锁:客户端通过调用 create 方法创建表示锁的临时顺序节点,如果是读请求,则创建 /lockpath/[hostname]-R-序号 节点,如果是写请求则创建 /lockpath/[hostname]-W-序号节点

3️⃣判断读写顺序:大概分为4个步骤

  • 1)创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听

  • 2)确定自己的节点序号在所有子节点中的顺序

  • 3.1)对于读请求:1. 如果没有比自己序号更小的子节点,或者比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑 2. 如果有比自己序号小的子节点有写请求,那么等待

  • 3.2)对于写请求,如果自己不是序号最小的节点,那么等待

  • 4)接收到Watcher通知后,重复步骤1)

4️⃣释放锁:与排他锁逻辑一致

在这里插入图片描述

在这里插入图片描述

羊群效应

在实现共享锁的 “判断读写顺序” 的第1个步骤是:创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听。这样的话,任何一次客户端移除共享锁之后,Zookeeper将会发送子节点变更的Watcher通知给所有机器,系统中将有大量的 “Watcher通知” 和 “子节点列表获取” 这个操作重复执行,然后所有节点再判断自己是否是序号最小的节点(写请求)或者判断比自己序号小的子节点是否都是读请求(读请求),从而继续等待下一次通知。

然而,这些重复操作很多都是 “无用的”,实际上每个锁竞争者只需要关注序号比自己小的那个节点是否存在即可。

当集群规模比较大时,这些 “无用的” 操作不仅会对Zookeeper造成巨大的性能影响和网络冲击,更为严重的是,如果同一时间有多个客户端释放了共享锁,Zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知–这就是所谓的 “羊群效应”。

改进分布锁的实现

改进后的分布式锁实现:

  • 1.客户端调用 create 方法创建一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点。

  • 2.客户端调用 getChildren 方法获取所有已经创建的子节点列表(这里不注册任何Watcher)。

  • 3.如果无法获取任何共享锁,那么调用 exist 来对比自己小的那个节点注册Watcher

读请求:向比自己序号小的最后一个写请求节点注册Watcher监听

写请求:向比自己序号小的最后一个节点注册Watcher监听

  • 4.等待Watcher监听,继续进入步骤2

Zookeeper羊群效应改进前后Watcher监听图:

在这里插入图片描述

ZOOKEEPER 锁相关基础知识

  • zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。

  • zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。

Java高频面试专题合集解析:

阿里Java岗面试百题:Spring 缓存 JVM 微服务 数据库 RabbitMQ等

当然在这还有更多整理总结的Java进阶学习笔记和面试题未展示,其中囊括了Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构资料和完整的Java架构学习进阶导图!

阿里Java岗面试百题:Spring 缓存 JVM 微服务 数据库 RabbitMQ等

更多Java架构进阶资料展示

阿里Java岗面试百题:Spring 缓存 JVM 微服务 数据库 RabbitMQ等

阿里Java岗面试百题:Spring 缓存 JVM 微服务 数据库 RabbitMQ等

阿里Java岗面试百题:Spring 缓存 JVM 微服务 数据库 RabbitMQ等

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

Children 方法获取所有已经创建的子节点列表(这里不注册任何Watcher)。

  • 3.如果无法获取任何共享锁,那么调用 exist 来对比自己小的那个节点注册Watcher

读请求:向比自己序号小的最后一个写请求节点注册Watcher监听

写请求:向比自己序号小的最后一个节点注册Watcher监听

  • 4.等待Watcher监听,继续进入步骤2

Zookeeper羊群效应改进前后Watcher监听图:

在这里插入图片描述

ZOOKEEPER 锁相关基础知识

  • zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。

  • zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。

Java高频面试专题合集解析:

[外链图片转存中…(img-RLw4dijv-1715375686651)]

当然在这还有更多整理总结的Java进阶学习笔记和面试题未展示,其中囊括了Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构资料和完整的Java架构学习进阶导图!

[外链图片转存中…(img-3QxQ9FTU-1715375686651)]

更多Java架构进阶资料展示

[外链图片转存中…(img-bhf097By-1715375686652)]

[外链图片转存中…(img-GuxfAtzZ-1715375686652)]

[外链图片转存中…(img-wG88FzkL-1715375686652)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值