Eureka中读写锁的奇思妙想,学废了吗?

很抱歉 好久没有更新文章了,最近的一篇原创还是在去年十月份,这个号确实荒废了好久,感激那些没有把我取消关注的小伙伴。

有读者朋友经常私信问我: ”你号卖了?“ ”文章咋不更新了?“

不更新主要的原因就是自己太懒了,也不知道要写些什么东西。最近一年还是在零散的学些东西,每次准备提笔写文章都半途而废了,到了最后就干脆不写了。

废话不多说了,还是看文章吧,分享的内容是我自己思考的一些东西,并没有标准答案,希望大家看的时候都能够有自己的见解,有问题可以第一时间联系到我 一起探讨。

跟着我,要么学会,要么学废!

要学废什么?

本文只想唠唠EurekaServer中关于读写锁的一些使用小技巧。

对于我们正常逻辑思维来说,读锁就是在读的时候加锁,写锁就是在写的时候加锁,这似乎没有什么技巧?

img

好像什么也学不废了?Oh No ~~~ 读写锁只是通俗的叫法,为何限定读锁只能加在读操作,写锁只能加在写操作呢?

细细品下方面那句话,接下来一起看看网飞的程序员是怎么玩的吧。

读写锁回顾

JDK中常说的读写锁是ReentrantReadWriteLock,我们平时工作中使用ReentrantLock会多一些,这两把锁都是师出同门,它们都是实现了AbstractQueuedSynchronizer中的相关逻辑

ReentrantLock将AQS中的state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:

img

大家也可以看下我之前写过的一篇详解AQS的文章:我画了35张图就是为了让你深入 AQS

这里就不再赘述读写锁底层的实现原理了,原理都在上面文章中。我们在这里可以把读写锁理解为和ReentrantLock一样的锁,只是带了读写操作的区分。

读与读之间不互斥,读与写、写与写之间是互斥的,这样做的目的是能够提升读写操作的性能。比如我们的业务是读多写少,那么使用读写锁,大多数情况都是可以并发访问的,不需要通过每次加锁来影响系统性能。

EurekaServer如何玩读写锁的?

前面铺垫了很多,希望大家能够知道读写锁这个东西。读写锁的使用很简单,JDK中都有现成的API供我们调用。往往一些牛叉的框架也都是使用这些JDK底层的API 构建起来的,接着我们就看EurekaServer是如何玩的吧。

PS:对于SpringCloud底层源码感兴趣的可以看我之前写的一套源码解读博客:https://www.cnblogs.com/wang-meng/p/12147889.html (密码:222 不要告诉别人哟o( ̄▽ ̄)d)

EurekaServer为何需要加锁?

我们知道EurekaServer作为一个注册中心,里面是保存EurekaClient注册表信息的,为了能够感知其他注册实例的存在,每个EurekaClient都会定时去注册中心拉取增量的注册表信息,然而这个增量拉取很有门道的,在增量获取的时候必须要加写锁来保证获取的数据准确性,这里先不详细展开,后续会一点点讲解

我们先看几个常见场景:

服务A启动的时候需要向注册中心发送regist请求,注册表会将服务A写入自己的花名册中

服务B发送下线请求,告知注册中心 我要下线了,请把我从注册表中请求,此时注册表会把服务B从花名册中抹掉

服务C在运行过程中也需要定时拉取注册表的最新数据,然后将数据同步到本地,这样本地就可以通过服务名去发现其他服务了

image-20210626213448048

这里加读写锁的玄机就藏在ServiceC获取注册表增量信息里面,我们先看EurekaServer读写锁中的相关代码:

public abstract class AbstractInstanceRegistry implements InstanceRegistry { private static final Logger logger = LoggerFactory.getLogger(AbstractInstanceRegistry.class); // registry就是注册表,存储注册信息的集合 private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>(); // 存放最近修改的实例信息 private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>(); // 今天的主角,读写锁 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final Lock read = readWriteLock.readLock(); private final Lock write = readWriteLock.writeLock(); }

上面有三个关键的地方需要注意:

注册表:ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry

最近修改的实例信息队列:ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue

读写锁:ReentranteadWriteLock readWriteLock

EurekaServer读写锁使用场景?

上面交代了大致背景,接下来就看看读写锁在这里是如何使用的。我们先来梳理下读写锁在这里使用的几个场景:

image-20210626220037428

接着也看下代码中readLock和writeLock 的使用链条,这里说明下 evict操作底层走的也是cancel逻辑让服务下线,所以调用链条中并没有显示evict的相关引用

readLock:

image-20210626220226325

writeLock:

image-20210626220209955

这里再回过头去回味上面的那句话:不要限定读锁只能加在读操作,写锁只能加在写操作,现在应该能明白这句话的含义了吧?

Eureka中确实是这么做的,读操作加写锁,写操作加读锁,一顿反向操作猛如虎

image-20210626233604197

再来一张图完整总结读写锁的详细使用场景:

image-20210627134136535

深层次思考

再去深究下上面提到的读写互斥操作,我们这里需要理解清楚`EurekaClient获取注册表信息操作是如何实现的:

(关于注册表获取的原理也可以参考下我之前的博文:https://www.cnblogs.com/wang-meng/p/12118203.html)

EurekaClient获取全量注册表信息实现方式:

image-20210626223437998

这里是EurekaClient第一次全量获取注册表的实现原理,从注册中心拉取到注册表后,EurekaClient会将注册表信息保存在本地的list中。

这里也要提下EurekaServer中的两层缓存机制,我们每次从注册中心拉取注册表时都是直接走的缓存,缓存使用的是谷歌提供的GuavaCahe

EurekaClient获取增量注册表实现方式:

image-20210626230424652

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzfsjf.html