Eureka读时加写锁,写时加读锁,到底是故意为之还是一个bug?

news2024/11/18 3:44:22

在对于读写锁的认识当中,我们都认为读时加读锁,写时加写锁来保证读写和写写互斥,从而达到读写安全的目的。但是就在我翻Eureka源码的时候,发现Eureka在使用读写锁时竟然是在读时加写锁,写时加读锁,这波操作属实震惊到了我,Eureka到底是故意为之还是一个bug?于是我就花了点时间研究了一下Eureka的这波操作。

Eureka服务注册实现类

众所周知,Eureka作为一个服务注册中心,肯定会涉及到服务实例的注册和发现,从而肯定会有服务实例写操作和读操作,这是每个注册中心最基本也是最核心的功能。

AbstractInstanceRegistry

如上图,AbstractInstanceRegistry是注册中心的服务注册核心实现类,这里面保存了服务实例的数据,封装了对于服务实例注册、下线、读取等核心方法。

这里讲解一下这个类比较重要的成员变量

服务注册表

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

注册表就是存储的服务实例的信息。Eureka是使用ConcurrentHashMap来进行保存的。键值是服务的名称,值为服务的每个具体的实例id和实例数据的映射,所以也是一个Map数据结构。InstanceInfo就是每个服务实例的数据的封装对象。

服务的上线、下线、读取其实就是从注册表中读写数据。

最近变动的实例队列

private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<>();

recentlyChangedQueue保存了最近变动的服务实例的信息。如果有服务实例的变动发生,就会将这个服务实例封装到RecentlyChangedItem中,存到recentlyChangedQueue中。

什么叫服务实例发生了变动。举个例子,比如说,有个服务实例来注册了,这个新添加的实例就是变动的实例。

所以服务注册这个操作就会有两步操作,首先会往注册表中添加这个实例的信息,其次会给这个实例标记为新添加的,然后封装到RecentlyChangedItem中,存到recentlyChangedQueue中。

新增

同样的,服务实例状态的修改、删除(服务实例下线)不仅会操作注册表,同样也会进行标记,封装成一个RecentlyChangedItem并添加到recentlyChangedQueue中。

修改

下线

所以从这分析也可以看出,注册表的写操作同时也会往recentlyChangedQueue中写一条数据,这句话很重要。

后面本文提到的注册表的写操作都包含对recentlyChangedQueue的写操作。

读写锁

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();

读写锁就不用说了,JDK提供的实现。

读写锁的加锁场景

上面说完了AbstractInstanceRegistry比较重要的成员变量,其中就有一个读写锁,也是本文的主题,所以接下来看看哪些操作加读锁,哪些操作加写锁。

加读锁的场景

1、服务注册

register

服务注册就是在注册表中添加一个服务实例的信息,加读锁。

2、服务下线

cancel和internalCancel

服务下线就是在注册表删除这个服务实例的信息,服务下线的方法最后是调用internalCancel实现的,而internalCancel是加的读锁,所以服务实例下线的时候加了读锁。

3、服务驱逐

什么叫服务驱逐,很简单,就是服务端会定时检查每个服务实例是否有向服务端发送心跳,如果服务端超过一定时间没有接收到服务实例的心跳信息,那么就会认为这个服务实例不可用,就会自动将这个服务实例从注册表删除,这就是叫服务驱逐。

服务驱逐是通过evict方法实现的,这个方法最终也是调用服务下线internalCancel方法来实现驱逐的。

所以服务驱逐,其实也是加读锁的,因为最后是调用internalCancel方法来实现的,而internalCancel方法就是加的读锁。

4、更新服务状态

服务实例的状态变动了,进行更新操作,也是加的读锁

5、删除服务状态

将服务的状态删了,也是加的读锁。

这里都是对于注册表的写操作,所以进行这些操作的同时也会往recentlyChangedQueue中写一条数据,只不过方法太长,代码太多,这里就没有截出来。

加写锁的场景

获取增量的服务实例的信息。

getApplicationDeltasFromMultipleRegions

所谓的增量信息,就是返回最近有变动的服务实例,而recentlyChangedQueue刚刚好保存了最近的服务实例的信息,所以这个方法的实现就是遍历recentlyChangedQueue,取出最近有变动的实例,返回。所以保存最近变动的实例,其实是为了增量拉取做准备的。

加锁总结

这里我总结一下读锁和写锁的加锁场景:

  • 加读锁: 服务注册、服务下线、服务驱逐、服务状态的更新和删除
  • 加写锁:获取增量的服务实例的信息

读写锁的加锁疑问

上一节讲了Eureka中加读锁和写锁的场景,有细心的小伙伴可能会有疑问,加读锁的场景主要涉及到服务注册表的增删操作,也就是写操作;而加写锁的场景是一个读的操作。

这不是很奇怪么,不按套路出牌啊,别人都是写时加写锁,读时加读锁,Eureka刚好反过来,属实是真的会玩。

写的时候加的读锁,那么就说明可以同时写,那会不会有线程安全问题呢?

答案是不会有安全问题。

我们以一个服务注册为例。一个服务注册,涉及到注册表的写操作和recentlyChangedQueue的写操作。

注册表本身就是一个ConcurrentHashMap,线程安全的map,注册表的值的Map数据结构,其实也是一个ConcurrentHashMap,如图。

通过源码可以发现,其实也是放入的值也是一个ConcurrentHashMap,所以注册表本身就是线程安全的,所以对于注册表的写操作,本身就是安全的。

再来看一下对于recentlyChangedQueue,它本身就是一个ConcurrentLinkedQueue,并发安全的队列,也是线程安全的。

所以单独对注册表和recentlyChangedQueue的操作,其实是线程安全的。

到这里更加迷糊了,本身就是线程安全的,为什么要加锁呢,而且对于写操作,还加的是读锁,这就导致可以有很多线程同时去写,对于写来说,相当加锁加了个寂寞。

带着疑惑,接着往下看。

Eureka服务实例的拉取方式和hash对比机制

拉取方式

Eureka作为一个注册中心,客户端肯定需要知道服务端道理存了哪些服务实例吧,所以就涉及到了服务的发现,从而涉及到了客户端跟服务端数据的交互方式,pull还是push。

那么Eureka到底是pull还是push模式呢?这里我就不再卖关子了,其实是一种pull模式,也就是说客户端会定期从服务端拉取服务实例的数据。并且Eureka提供了两种拉取方式,全量和增量。

1、全量

全量其实很好理解,就是拉取注册表所有的数据。

全量一般发生在客户端启动之后第一次获取注册表的信息的时候,就会全量拉取注册表。还有一种场景也会全量拉取,后面会说。

2、增量

增量,前面在说加写锁的时候提到了,就是获取最近发生变化的实例的信息,也就是recentlyChangedQueue里面的数据。

增量相比于全量拉取的好处就是可以减少资源的浪费,假如全量拉取的时候数据压根就没有变动,那么白白浪费网络资源;但是如果是增量的话,数据没有变动,那么就没有增量信息,就不会有资源的浪费。

在客户端第一次启动的全量拉取之后,定时任务每次拉取的就是增量数据。

增量拉取的hash对比机制

如果是增量拉取,客户端在拉取到增量数据之后会多干两件事:

  • 会将增量信息跟本地缓存的服务实例进行合并
  • 判断合并后的服务的数据跟服务端的数据是不是一样

那么如何去判定客户端的数据跟服务端的数据是不是一样呢?

Eureka是通过一种hash对比的机制来实现的。

当服务端生成增量信息的时候,同时会生成一个代表这一刻全部服务实例的hash值,设置到返回值中,代码如下

所以增量信息返回的数据有两部分,一部分是变动的实例的信息,还有就是这一刻服务端所有的实例信息生成的hash值。

当客户端拉取到增量信息并跟本地原有的老的服务实例合并完增量信息之后,客户端会用相同的方式计算出合并后服务实例的hash值,然后会跟服务端返回的hash值进行对比,如果一样,说明本次增量拉取之后,客户端缓存的服务实例跟服务端一样,如果不一样,说明两边的服务实例的数据不一样。

这就是hash对比机制,通过这个机制来判断增量拉取的时候两边的服务实例数据是不是一样。

hash对比

但是,如果发现了不一样,那么此时客户端就会重新从服务端全量拉取一次服务数据,然后将该次全量拉取的数据设置到本地的缓存中,所以前面说的还有一种全量拉取的场景就在这里,源码如下

重新全量拉取

读写锁的使用揭秘

前面说了增量拉取和hash对比机制,此时我们再回过头仔细分析一下增量信息封装的两步操作:

  • 第一步遍历recentlyChangedQueue,封装增量的实例信息
  • 第二步生成所有服务实例数据对应的hash值,设置到增量信息返回值中

为什么要加锁

假设不加锁,那么对于注册表和recentlyChangedQueue读写都可以同时进行,那么会出现这么一种情况

当获取增量信息的时候,在第一步遍历recentlyChangedQueue时有2个变动的实例,注册表总共有5个实例

当recentlyChangedQueue遍历完之后,还没有进行第二步计算hash值时,此时有服务实例来注册了,由于不加锁,那么可以同时操作注册表和recentlyChangedQueue,于是注册成功之后注册表数据就变成了6个实例,recentlyChangedQueue也会添加一条数据

但是因为recentlyChangedQueue已经遍历完了,此时不会在遍历了,那么刚注册的这个实例在此次获取增量数据时就获取不到了,但是由于计算hash值是通过这一时刻所有的实例数据来计算,那么就会把这个新的实例计算进去了。

这不完犊子了么,增量信息没有,但是全部实例数据的hash值有,那么就会导致客户端在合并增量信息之后计算的hash值跟返回的hash值不一样,就会导致再次全量拉取,白白浪费了本次增量拉取操作。

所以一定要加锁,保证在获取增量数据时,不能对注册表进行改动。

为什么加读写锁而不是synchronized锁

这个其实跟Eureka没多大关系,主要是读写锁和synchronized锁特性决定的。synchronized会使得所有的操作都是串行化,虽然也能解决问题,但是也会导致并发性能降低。

为什么写时加读锁,读时加写锁

现在我们转过来,按照正常的操作,服务注册等写操作加写锁,获取增量的时候加读锁,那么可以不可呢?

其实也是可以的,因为这样注册表写操作和获取的增量信息读操作还是互斥的,那么获取的增量信息还是对的。

那么为什么Eureka要反过来?

写(锁)写(锁)是互斥的。如果注册表写操作加了写锁,那么所有的服务注册、下线、状态更新都会串行执行,并发性能就会降低,所以对于注册表写操作加了读锁,可以提高写的性能。

但是,如果获取的增量读的操作加了写锁,那岂不是读操作都串行化了,那么读的性能不是会变低么?而且注册中心其实是一个读多写少的场景,为了提升写的性能,浪费读的性能不是得不偿失么?

哈哈,其实对于这个读操作性能低的问题,Eureka也进行了优化,那就是通过缓存来优化了这个读的性能问题,读的时候先读缓存,缓存没有才会真正调用获取增量的方法来读取增量的信息,所以最后真正走到获取增量信息的方法,请求量很低。

ResponseCacheImpl

ResponseCacheImpl内部封装了缓存的操作,因为不是本文的重点,这里就不讨论了。

总结

所以,通过上面的一步一步分析,终于知道了Eureka读写锁的加锁场景、为什么要加读写锁以及为什么写时加读锁,读时加写锁。这里我再总结一下:

为什么加读写锁

是为了保证获取增量信息的读操作和注册表的写操作互斥,避免由于并发问题导致获取到的增量信息和实际注册表的数据对不上,从而引发客户端的多余的一次全量拉取的操作。

为什么写时加读锁,读时加写锁

其实是为了提升写的性能,而读由于有缓存的原因,真正走到获取增量信息的请求很少,所以读的时候就算加写锁,对于读的性能也没有多大的影响。

从Eureka对于读写锁的使用也可以看出,一个技术什么时候用,如何使用都是根据具体的场景来判断的,不能要一概而论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/160178.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app | 从零创建一个新项目以及关于网络请求配置和分包

一、uni-app简介uni-app 是一个使用 Vue.js 开发所有前端应用的框架。开发者编写一套代码&#xff0c;可发布到 iOS、Android、H5、以及各种小程序&#xff08;微信/支付宝/百度/头条/QQ/钉钉/淘宝&#xff09;、快应用等多个平台。二、开发工具uni-app官方推荐使用HBuilderX来…

【JS 逆向百例】X-Bogus 逆向分析,JSVMP 纯算法还原

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;不提供完整代码&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 本文章未…

72小时灵感冲刺,创意就该这么玩 | LigaAI Hackathon特别策划

2023 年 1 月 9 日至 12 日&#xff0c;LigaAI 团队全员出逃&#xff1a;放下迭代&#xff0c;暂缓需求&#xff0c;处处充斥着「可以摸鱼➕卷死他们」的矛盾又欢乐的气息。职场和远程的伙伴们无一不在热烈讨论、积极组队、抢占会议室、搜刮零食饮料…… 是什么让矜持内敛的技…

微信小程序的启动和渲染过程(加组件分类和组件的基本使用以及API分类)

小程序的启动过程 把小程序的代码包下载到本地解析app.json全局配置文件执行app.js小程序入口文件,调用App()创建小程序实例渲染小程序首页小程序启动完成 小程序页面渲染的过程 加载解析页面的.json配置文件加载页面的.wxml模板和.wxss样式执行页面的.js文件,调用page(创建…

网站建设的具体流程

有网站制作需求的朋友想了解一下网站建设的具体流程&#xff0c;防止不良的网站制作公司的业务人员用虚假的工作量欺骗自己报出高价&#xff0c;米贸搜详细给你整理一下一个网站的建设有哪些流程:1.在线或当面沟通网站制作需求&#xff0c;详细沟通网站的功能需求、设计风格以及…

功率放大模块的作用是什么(功率放大器模块的应用范围)

功率放大模块是一种能够把开关电源、数字功放集成到一起的放大模块。很多人对于功率放大模块的作用是什么以及功率放大模的应用范围都不清楚&#xff0c;下面就来详细的为大家介绍下什么是功率放大模块以及功率放大模块的基础知识。 一、什么是功率放大模块 功率放大模块是一种…

谷歌AR应用挑战赛上那些富有创意的AR项目集锦

前不久&#xff0c;谷歌面向全球100多个国家和地区的开发者们推出ARCore Geospatial API挑战赛&#xff0c;获奖的AR应用可得到1000美元到1.2万美元的奖金&#xff0c;支持多种不同的内容类别&#xff0c;比如AR导航、AR游戏、AR娱乐等等。据悉&#xff0c;Geospatial API是谷歌…

贵阳某小区一次HC小区管理系统自研道闸故障解决记录

一次HC小区管理系统自研道闸故障解决记录&#xff0c;方便其他小伙伴出现问题的时候提供解决思路 早上九点钟 客户说道闸用不了&#xff0c;这个客户不是物业&#xff0c;而是科技公司&#xff0c;他们给物业安装的道闸。 问题描述是&#xff0c;mqtt 也重启了&#xff0c;物…

Allegro如何打开锁定走线线宽的功能操作指导

Allegro如何打开锁定走线线宽的功能操作指导 在做PCB设计的时候,有时需要用到锁定线宽的功能,让走线一直保持固定的线宽不变化,例如下图 任何网络的走线在任何地方都是固定20mil的线宽 具体操作如下 选择菜单的Setup选择User Preferences

解决IE页面打不开及白屏问题

IE浏览器控制台报错如下&#xff1a; 解决办法&#xff1a; ① public文件下新建TextEncoder.js /* eslint-disable */ ; var textEncoder (function(window) {if (undefined ! window.TextEncoder) { return }function _TextEncoder() {// --DO NOTHING}_TextEncoder.prot…

steam搬砖项目详细介绍

一、项目介绍 其实&#xff0c;Steam就是一个全球的游戏平台&#xff0c;搬砖主要是搬的一款火遍全球的游戏CSGO的装备和饰品。CS听说过吧&#xff0c;这款游戏就是CS的一个系列。&#xff08;通俗易懂的理解就是&#xff0c;从国外steam游戏平台购买装备&#xff0c;再挂到国内…

分销商城小程序开发

武汉微驱动科技有限公司 目前电商行业正在蓬勃发展&#xff0c;越来越多的商家想要利用各种社交关系&#xff0c;用社交软件扩大经营规模&#xff0c;小程序分销是最近很火爆的流行趋势&#xff0c;不过有不少商家还不清楚“什么是小程序分销模式&#xff1f;小程序分销商城是什…

gerapy部署项目报错:ModuleNotFoundError: No module named ...

使用gerapy部署我的项目&#xff0c;rebuild的时候是成功的&#xff0c;但是deploy的时候失败了&#xff0c;报错&#xff1a; Client 1 Failed to Deploy 没有显示具体的错误&#xff0c;只能到gerapy的部署目录找日志。 根据 ll 命令&#xff0c;找到了最新的日志文件&…

AutoSAR Crypto_UtilizationOfCryptoServices-AUTOSAR加密服务使用

1 Stack Architecture The Crypto Service Manager (CSM) CSM控制一个或者多个Client对一个或者多个同步或异步加密服务。它提供了优先级队列来管理专用CRYPTO不能直接处理的作业 CSM的功能如下&#xff1a; ● HASH计算&#xff1b; ● 消息认证码的生成和校验&#xff1b; ●…

Centos6源码安装Haproxy进行四层代理

一.背景 公司使用专线与第三方公司进行系统交互&#xff0c;给定了我们业务IP的使用范围&#xff0c;防火墙策略只开放业务IP范围之内的IP地址才能访问&#xff0c;如果源IP不在业务IP范围之内&#xff0c;那么通过互联IP过去是访问不了的。我们的做法是为了不影响现有业务&…

完整复现YOLOv8:包括训练、测试、评估、预测阶段【本文源码已开源,地址在文章末尾】

训练过程展示: 目录 1、复现过程1.1、配置开发环境1.2、demo预测实现过程2 、项目实现方法与代码(包括训练、测试、评估、预测阶段)2.1、训练、测试、评估、预测代码适配2.2、同时开始训练、测试、评估、预测2.3、训练完之后进行预测2.4、训练、评估、混淆矩阵、召回曲线等…

Linux 基础常用命令 整理

Shell Shell 这个单词的原意是“外壳”&#xff0c;跟 kernel&#xff08;内核&#xff09;相对应&#xff0c;比喻内核外面的一层&#xff0c;即用户跟内核交互的对话界面。 Shell 是一个程序&#xff0c;提供一个与用户对话的环境。这个环境只有一个命令提示符&#xff0c;让…

解读小红书:零食行业用户洞察报告

随着消费持续升级、零食正作为休闲生活的一部分开始走近更多用户。以日趋多元化的消费需求为导向&#xff0c;零食行业用户又透露出哪些消费特征呢&#xff1f; 本期为大家解读小红书官方发布的《「灵感补给站」小红书2023年零食行业用户洞察报告》&#xff0c;基于上千名用户定…

从状态机的角度async和await的实现原理

一. 深度剖析准备&#xff1a;先给VS安装一个插件ILSpy,这样更容易反编译代码进行查看,另外要注意反编译async和await的时候,要把C#代码版本改为4.0哦。1.什么是状态机(1).含义&#xff1a;通常我们所说的状态机(State Machine)指的是有限状态自动机的简称&#xff0c;是现实事…

6.5、文件传送协议FTP

将某台计算机中的文件通过网络传送到可能相距很远的另一台计算机中&#xff0c;是一项基本的网络应用&#xff0c;即文件传送。 文件传送协议FTP\color{red}文件传送协议\texttt{FTP}文件传送协议FTP (File Transfer Protocol)是因特网上使用得最广泛的文件传送协议。 FTP 提供…