目录
1 网络通信
参考链接:不再冗述
- 22.网络模型
- 22.1用户空间和内核空间
- 22.2 阻塞IO
- 22.3 非阻塞IO
- 22.4 IO多路复用
- 22.4.1 Linux中的IO多路复用
- 22.4.2 epoll中的事件通知机制
- 22.5 信号驱动IO
-
22.6 异步IO
2 分布式和微服务的区别:
分布式和微服务都是现代软件开发中常用的架构模式,它们各自具有独特的特点和优势,同时也存在一定的联系。下面我将详细解释它们之间的区别和联系。
分布式的定义、特点和优势
定义:
分布式系统是由多台计算机通过网络连接组成的系统,这些计算机协调工作,对用户表现为一个统一整体。
特点:
-
可扩展性:通过增加节点来扩展系统的处理能力。
-
容错性:部分节点失效不影响整体可用性。
-
透明性:包括访问、位置、并发等透明性,用户无需感知底层细节。
-
并发性:多节点可并行处理操作。
-
资源共享:硬件、软件、数据资源可被多用户共享。
优势:
-
高可靠性:通过冗余节点避免单点故障。
-
性能提升:分布式计算可分解任务并行处理。
-
弹性扩展:根据负载动态增减资源。
-
资源优化:整合闲置资源提高利用率。
-
成本效益:使用普通硬件集群替代昂贵大型机。
微服务的定义、特点和优势
定义:
微服务是一种软件架构风格,它将一个大型应用程序拆分为一组小型、独立、低耦合的服务,每个服务专注于完成单一特定的业务功能,并且每个服务都可以独立开发、部署和扩展。
特点:
-
服务细粒度:每个微服务通常对应着系统中的一个小的、独立的功能模块。
-
独立开发、部署与扩展:每个微服务可以独立选择最适合的技术栈进行开发、测试和部署。
-
低耦合:服务之间通过定义良好的接口进行通信,不依赖对方的内部实现细节。
-
高内聚:服务内部的功能紧密相关,便于理解和修改。
优势:
-
灵活性:开发团队可以更为灵活地开发和部署服务。
-
稳定性:由于服务粒度的减小,单个服务的复杂度大大降低,提高了系统的稳定性。
-
易维护性:每个服务代码量小、功能单一,便于维护和修改。
-
资源利用率高:通过容器化技术,提升了服务的独立性和系统的资源利用率。
分布式与微服务的联系和区别
联系:
-
都是解决大型复杂系统的手段:分布式和微服务都旨在解决大型复杂系统的开发、部署和管理问题。
-
都强调服务的独立性:分布式服务架构强调的是服务的分布式部署和通信,而微服务架构则侧重于将应用划分为一组小的、独立的服务。
-
都遵循松耦合和高内聚的设计原则:以减少服务间的依赖和提高可维护性。
区别:
-
服务粒度:分布式服务架构的服务粒度可能较大,而微服务架构的服务粒度更细。
-
部署方式:分布式服务架构可能将不同的服务部署在不同的服务器上,但不一定要求每个服务都独立部署。而微服务架构则要求每个服务都能独立开发、部署和扩展。
-
通信方式:分布式服务架构可能通过多种方式进行通信,而微服务架构通常通过轻量级通信机制(如HTTP/REST、gRPC、消息队列等)进行通信。
3 RPC,HTTP和HTTPS的区别联系
区别
- 概念与设计目的
- RPC(远程过程调用):是一种远程过程调用协议,旨在让分布式系统中的不同节点像调用本地函数一样方便地调用远程函数,主要用于实现分布式系统中服务之间的高效通信和协作。
- HTTP(超文本传输协议):是一种超文本传输协议,用于在网络上传输超文本数据,如 HTML 页面、JSON 数据等,主要用于实现客户端(如浏览器)与服务器之间的信息交互,以获取和展示网页内容等资源。
- HTTPS(超文本传输安全协议):是在 HTTP 基础上加入 SSL/TLS 加密协议的安全版本,其目的是为了保证数据在传输过程中的安全性、完整性和保密性,防止数据被窃取或篡改,常用于对安全性要求较高的场景,如网上银行、电子商务等。
- 通信方式
- RPC:采用客户端 - 服务器模式,客户端发起调用请求,服务器接收请求并返回结果。它对开发者隐藏了底层网络通信细节,更像是本地方法调用。
- HTTP:同样基于客户端 - 服务器模式,客户端发送 HTTP 请求,服务器返回 HTTP 响应。请求和响应有明确的格式和规范,包括请求方法(如 GET、POST 等)、头信息、状态码等,通信过程相对更透明。
- HTTPS:与 HTTP 通信方式类似,但在数据传输前会先进行 SSL/TLS 握手,以建立安全连接,然后在加密通道上进行数据传输。
- 数据格式
- RPC:可使用多种序列化方式传输数据,如 Protobuf、JSON、XML 等,注重数据的高效传输和反序列化,以提升调用性能。
- HTTP:主要使用 JSON、XML 等格式传输数据,也能传输 HTML、图片、视频等多种类型的数据,数据格式侧重于可读性和通用性。
- HTTPS:数据格式与 HTTP 相同,但在传输过程中会对数据进行加密处理,以密文形式传输。
- 性能和效率
- RPC:通常在性能上有优势,能针对特定应用场景优化,如采用高效序列化方式、连接复用等,适用于分布式系统内部的高性能服务调用。
- HTTP:在简单 Web 应用场景表现良好,但处理大量数据传输和高并发服务调用时,因额外头信息和复杂协议处理,性能可能不如 RPC。
- HTTPS:由于增加了加密和解密过程,会消耗额外的计算资源和时间,性能相对 HTTP 略低,但在安全性要求高的场景下,这种性能损耗是可接受的。
- 安全性
- RPC:安全性通常依赖于具体的实现和配置,如使用安全的传输协议、进行身份验证和授权等。一些 RPC 框架提供了相应的安全机制,但不是所有 RPC 调用都默认具备强安全性。
- HTTP:本身不提供加密和身份验证等安全功能,数据以明文形式传输,容易被窃取、篡改或监听,存在一定的安全风险。
- HTTPS:通过 SSL/TLS 协议对数据进行加密,同时可以对服务器进行身份验证,确保数据传输的安全性和完整性,有效防止中间人攻击等安全威胁。
联系
- 底层依赖:三者在底层都依赖网络协议来传输数据,如 TCP/IP 协议。都需要通过网络连接将请求从客户端发送到服务器,并将响应从服务器返回给客户端。
- 应用场景关联:在现代网络应用中,HTTP 和 HTTPS 常用于客户端(如浏览器)与服务器之间的通信,而 RPC 常用于服务器内部不同服务之间的通信。例如,一个 Web 应用中,前端通过 HTTPS 请求向后端服务器获取数据,后端服务器可能会使用 RPC 来调用内部的其他服务进行数据处理。
- 基于 HTTP 的 RPC 实现:部分 RPC 框架会基于 HTTP 协议来实现,如 gRPC-Web 基于 HTTP/2 协议实现 RPC,利用 HTTP 的通用性和广泛支持,结合 RPC 的优势,为 Web 应用提供高效的远程调用方案。
常见的RPC和Http框架:
RPC框架:
- gRPC:由Google开发,支持多种语言(如Java、Python、C++、Go、Node.js等)。它基于HTTP/2协议,并支持常见的众多编程语言,提供了内建的服务发现、负载均衡和认证机制。gRPC适用于高性能、高并发的微服务架构。此外,gRPC Web允许在浏览器中调用gRPC服务。
- Dubbo:由阿里巴巴开发的高性能RPC框架,支持多种协议(如Dubbo、HTTP、gRPC等),广泛应用于Java微服务架构。Dubbo提供了服务注册与发现、负载均衡、容错、监控等功能,适用于大规模分布式系统。
- Apache Thrift:Facebook的开源RPC框架,主要是一个跨语言的服务开发框架。它提供了丰富的数据类型和接口定义语言(Thrift IDL),支持多种传输协议(如TCP、HTTP、Framed Transport等)。Thrift适用于大规模分布式系统,性能高,支持多种语言和协议。
- RabbitMQ和Kafka:这些消息队列系统可以实现异步RPC调用,适合高并发、分布式系统,支持解耦和负载均衡。RabbitMQ支持AMQP协议,而Kafka支持高吞吐量的异步消息传递。
Http框架:
- Flask(Python):一个轻量级框架,非常适合小型项目或快速开发原型。它提供了简单的路由、模板渲染和请求处理。
- Express(Node.js):Node.js最流行的HTTP框架之一,灵活性强且有大量插件支持。适用于构建RESTful API和全栈应用。
- Django(Python):“电池全包”的框架,适合复杂项目。其强大的ORM系统、内置用户管理和权限控制使得开发变得便捷。
- Spring Boot(Java):构建企业级Java应用的首选,提供了丰富的功能和强大的生态系统,适用于大规模分布式应用。Spring Boot结合Spring Web是构建Java Web服务的主流选择。
- ASP.NET Core(C#):一个跨平台的框架,支持快速开发高性能的Web API和Web应用
注意:
OpenFeign底层使用的是HTTP协议进行通信,而不是RPC
4 谈谈你对负载均衡的理解
负载均衡是分布式系统和网络架构中的一个核心概念,它主要关注如何有效地分配网络或应用流量,以优化资源使用、提高系统性能和可靠性。以下是我对负载均衡的详细理解:
一、定义
负载均衡(Load Balancing)是一种将网络请求或数据流量分散到多个服务器或网络节点上的技术,以确保每个节点都能处理适量的请求,从而避免单点过载,提高整个系统的吞吐量和响应时间。
二、目的
负载均衡的主要目的是提高系统的可用性、可扩展性和性能。通过分散流量,负载均衡可以减少单个服务器的负载,防止服务器过载崩溃,同时提高系统的容错能力和弹性。此外,负载均衡还可以实现资源的优化利用,提高系统的整体效率和用户体验。
三、常见策略
-
轮转调度(Round-Robin Scheduling):
- 原理:按顺序将请求依次分配给后端服务器,循环往复。
- 优点:实现简单,无需额外计算,公平分配请求。
- 缺点:无法感知服务器负载差异,可能导致性能差的服务器过载;不支持动态调整权重。
- 适用场景:服务器配置相同且负载波动较小的场景,如静态资源服务器集群。
-
加权轮转调度(Weighted Round-Robin Scheduling):
- 原理:在轮询基础上,为每台服务器分配权重,权重高的服务器获得更多请求。
- 优点:可根据服务器性能差异灵活分配流量,支持手动配置权重,适合异构服务器环境。
- 缺点:权重需预先静态配置,无法动态适应负载变化;长时间运行可能导致低权重服务器闲置。
- 适用场景:服务器性能差异明显的场景(如CPU、内存不同),需人工干预权重的环境。
-
随机均衡调度(Random Scheduling):
- 原理:完全随机选择一个服务器处理请求。
- 优点:实现简单,适合快速部署,在大量请求下接近均匀分布。
- 缺点:无法保证流量分配的精准性,可能短时间集中访问某台服务器,导致局部负载过高。
- 适用场景:服务器性能相近且对流量分配精度要求不高的场景,如测试环境。
-
加权随机均衡调度(Weighted Random Scheduling):
- 原理:根据服务器权重随机分配请求,权重高的服务器被选中的概率更高。
- 优点:结合随机性和权重分配,灵活性较高,适合需要概率性负载均衡的场景。
- 缺点:仍依赖静态权重配置,无法实时响应服务器状态变化,流量分配不如加权轮询稳定。
- 适用场景:需要按概率分配流量且服务器性能差异较大的场景,如混合云环境。
-
最小连接调度(Least-Connection Scheduling):
- 原理:优先将请求分配给当前连接数最少的服务器。
- 优点:动态感知服务器负载,自动平衡流量,适合处理长连接或请求处理时间差异大的场景(如数据库查询)。
- 缺点:需要实时监控服务器连接数,增加系统开销;不适用于短连接或请求处理时间均匀的场景。
- 适用场景:长连接服务(如WebSocket)、处理时间差异大的后端服务(如API网关)。
-
加权最小连接调度(Weighted Least-Connection Scheduling):
- 原理:用相应的权值表示各个服务器的处理性能,具有较高权值的服务器将承受较大比例的活动连接负载。调度器可以自动问询服务器的负载情况,并动态地调整其权值。
- 优点:结合服务器性能进行动态负载分配,提高资源利用率。
- 缺点:实现相对复杂,需要动态调整权值。
- 适用场景:服务器性能差异较大且需要动态负载均衡的场景。
-
目标地址散列调度(Destination Hashing Scheduling):
- 原理:根据请求的目标IP地址,通过散列函数将其映射到一台可用且未超载的服务器。
- 优点:确保同一个客户端的请求始终被分配到同一个服务器,有利于实现会话保持。
- 缺点:可能导致负载不均衡,特别是当目标IP地址分布不均匀时。
- 适用场景:需要保持会话一致性的场景,如Web应用会话管理。
-
源地址散列调度(Source Hashing Scheduling):
- 原理:根据请求的源IP地址,通过散列函数将其映射到一台可用且未超载的服务器。
- 优点:类似于目标地址散列调度,但基于源IP地址进行映射,适用于需要基于客户端IP进行负载均衡的场景。
- 缺点:同样可能导致负载不均衡。
- 适用场景:需要基于客户端IP进行负载均衡且对会话一致性要求不高的场景。
-
基于局部性的最少链接调度(Locality-Based Least Connections Scheduling):
- 原理:找出请求的目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;否则用“最少链接”的原则选出一个可用的服务器。
- 优点:提高各台服务器的访问局部性和主存Cache命中率。
- 缺点:实现相对复杂,需要维护服务器间的连接信息。
- 适用场景:服务器性能相近且需要提高缓存命中率的场景。
-
带复制的基于局部性最少链接调度(Locality-Based Least Connections with Replication Scheduling):
- 原理:维护从一个目标IP地址到一组服务器的映射,而不是从一个目标IP地址到一台服务器的映射。按“最少链接”原则从服务器组中选出一台服务器,若服务器可用且没有超载,将请求发送到该服务器;否则按“最少链接”原则从这个集群中选出一台服务器,将该服务器加入到服务器组中,再将请求发送到该服务器。
- 优点:提高负载均衡的灵活性和容错能力。
- 缺点:增加系统的复杂性和开销。
- 适用场景:需要高可靠性和容错能力的场景。
-
响应速度均衡调度(Response Time Scheduling):
- 原理:负载均衡设备对内部各服务器发出一个探测请求,然后由对探测请求响应最快的一台服务器来响应客户端的服务请求。
- 优点:动态优化用户体验,优先分配高性能节点。
- 缺点:需持续采集响应时间数据,计算复杂度高。
- 适用场景:对响应速度要求较高的场景。
-
处理能力均衡调度(Processing Capacity Scheduling):
- 原理:负载均衡设备记录集群内部处理负荷(根据服务器CPU型号、CPU数量、内存大小及当前连接数等换算而成),将服务请求分配给负荷最轻的服务器。
- 优点:考虑到了内部服务器的处理能力及当前网络运行状况等不同情形,因此这种均衡算法相对来说更加精确。
- 缺点:附加开销较大。
- 适用场景:适合运用到第7层(应用层)负载均衡的情况。
-
DNS均衡调度(DNS Scheduling):
- 原理:分处在不同地理位置的负载均衡设备,收到同一个客户端的域名解析请求,并在同一时间内,把此域名解析成各自相对应服务器的IP地址,并返回给客户端,客户端将以最先收到的域名解析IP地址来继续请求服务,而忽略其他的IP地址响应。
- 优点:实现简单,适用于地理位置分布广泛的场景。
- 缺点:可能导致负载不均衡,特别是当不同地理位置的服务器性能差异较大时。
- 适用场景:需要基于地理位置进行负载均衡的场景。
四、实现负载均衡的技术和工具
- 硬件负载均衡器:如F5 Networks的BIG-IP等,它们通常具有高性能和稳定性,但成本较高。
- 软件负载均衡器:如Nginx、HAProxy等,它们具有灵活性高、配置简单、成本低等优点,广泛应用于各种规模的系统中。
- DNS负载均衡:通过DNS解析器将域名解析到多个IP地址上,实现流量的分散。这种策略通常用于跨地域的负载均衡,但可能无法精确控制每个服务器的负载。
五、负载均衡如何与容器技术结合
-
使用容器编排工具的负载均衡功能
- 容器编排工具如Kubernetes内置了负载均衡功能。在Kubernetes中,Service对象可以实现基本的负载均衡,将流量分发到多个Pod(容器实例)上。
- 通过配置Service的类型(如ClusterIP、NodePort、LoadBalancer),可以灵活地将服务暴露给集群内部或外部。
- Kubernetes还支持Ingress资源,用于实现更复杂的七层负载均衡,包括基于路径和域名的路由规则。
-
服务网格
- 服务网格如Istio为微服务架构提供了更高级的负载均衡能力。
- 服务网格可以在应用层面实现流量管理和负载均衡,而无需修改应用代码。
- 通过配置Istio的VirtualService和DestinationRule等资源,可以实现复杂的路由规则和负载均衡策略。
-
反向代理
- 反向代理如Nginx或HAProxy也可以用于实现负载均衡。
- 在容器化环境中,可以将反向代理部署为单独的容器或服务,并将流量分发到后端的容器实例上。
- 反向代理支持多种负载均衡算法,如轮询、加权轮询、IP哈希等。
-
云服务提供商的负载均衡服务
- 云服务提供商如AWS、Azure、GCP等提供了专门的负载均衡服务。
- 这些服务可以与容器编排工具(如Kubernetes)集成,实现跨多个可用区的负载均衡。
- 云服务提供商的负载均衡服务通常支持自动伸缩、健康检查等高级功能。
5.介绍下CAP
CAP理论的三个属性
-
一致性(Consistency):
-
定义:所有节点在同一时间具有相同的数据值,即数据的更新对所有用户可见。
-
场景示例:在银行转账场景中,当用户A向用户B转账100元时,转账操作要么完全成功,要么完全失败,系统必须保证双方账户余额的一致性。
-
重要性:对于需要强一致性的系统,如金融交易系统、电子商务的订单支付等,一致性是至关重要的。一旦出现数据不一致,将导致严重的业务问题,如重复扣款或充值失败。
-
-
可用性(Availability):
-
定义:系统中的每个请求都会在合理的时间范围内收到非错误的响应,无论该响应是操作成功还是失败。
-
场景示例:在电商网站的高峰期,如“双11”购物节期间,系统需要能够处理大量的用户请求,保证用户可以正常浏览商品、下单购买等。
-
重要性:对于高并发、高流量的业务系统,如电商、社交网络等,可用性是保障用户体验的关键。如果系统出现不可用的情况,将导致用户流失和业务损失。
-
-
分区容错性(Partition Tolerance):
-
定义:分布式系统在遇到任何网络分区故障时,仍然能够正常运行。即系统能够容忍网络分区故障,继续提供服务。
-
场景示例:在大型分布式系统中,如跨地区、跨机房部署的云计算平台,网络可能出现故障,导致部分节点之间无法通信。系统需要在这种情况下仍然能够正常运行,保证数据的存储和访问。
-
重要性:在网络环境复杂的分布式系统中,分区容错性是保证系统可靠性的基础。由于网络故障是不可避免的,系统必须具备分区容错能力,以确保数据不会因局部故障而丢失或不可用。
-
CAP理论的应用和权衡
在分布式系统设计中,由于网络分区是不可避免的,因此需要在一致性和可用性之间做出权衡。具体来说,在网络分区发生时,系统可以选择:
-
CP模型:保证一致性和分区容错性,但可能牺牲部分可用性。适用于需要强一致性的场景,如金融交易系统、支付系统等。
(ZooKeeper )
-
AP模型:保证可用性和分区容错性,但可能牺牲一致性。适用于高并发、高流量的业务系统,如电商、社交网络等。这些系统通常可以接受一定的数据不一致性,以换取更高的可用性和吞吐量。
(Kafka, Cassandra,Redis Cluster(默认AP))
值得注意的是,虽然理论上存在CA模型(即同时保证一致性和可用性),但在现实世界的分布式系统中,由于网络分区几乎是不可避免的,因此实际上很难维持CA模型的全部优点。因此,在分布式系统设计中,通常需要根据具体的业务需求和技术约束来选择合适的CAP组合。
6 分布式锁
6.1 概念
-
分布式锁通过在分布式环境下对共享资源进行加锁控制,确保同一时刻只有一个客户端能够访问该资源。就像在单进程环境中,锁用于保护临界区资源,防止多个线程同时访问导致数据不一致或其他并发问题一样,分布式锁在分布式系统中起到了类似的作用,只不过它跨越了多个节点和进程,为整个分布式系统提供了一种互斥访问的手段。
- 核心要素
-
唯一性:分布式锁在整个分布式系统中必须是唯一的,即同一把锁不能被多个客户端同时持有。这是实现互斥访问的基础,通过唯一标识来区分不同的锁,确保每个锁都能准确地控制对应的共享资源。
-
可获取与释放:客户端需要能够主动获取锁,以表明它要访问共享资源,并且在访问完成后能够及时释放锁,以便其他客户端能够获取锁并访问资源。获取和释放锁的操作应该是原子性的,以避免出现中间状态导致锁的状态不一致。
-
锁的有效期:为了防止因客户端异常或其他原因导致锁被无限期持有,分布式锁通常需要设置一个有效期。当锁的持有时间超过有效期后,系统会自动释放锁,允许其他客户端获取,从而保证资源的可用性。
-
6.2 实现方式
- 基于数据库实现
-
表锁方式:通过在数据库中创建一张锁表,包含锁的名称、持有锁的客户端信息、锁的创建时间等字段。当客户端要获取锁时,在表中插入一条记录,若插入成功则获取锁成功,否则表示锁已被占用。释放锁时,删除相应记录。这种方式实现简单,但在高并发场景下性能较差,因为每次获取和释放锁都需进行数据库读写操作,且会对整张表进行锁定,并发度低。
-
行锁方式:可以利用数据库的行级锁来实现分布式锁。假设存在一张用于存储锁信息的表,其中有一个字段表示锁的状态。当客户端尝试获取锁时,使用
SELECT ... FOR UPDATE
语句来获取特定行的锁。如果查询到锁状态为未被占用,则获取锁成功,并更新锁状态为已占用;如果查询到锁已被占用,则获取锁失败。这种方式相比表锁方式,并发度有所提高,因为它只锁定了特定的行,而不是整张表。但它仍然存在性能问题,在高并发下数据库的压力较大。
-
- 基于缓存实现
-
使用 SETNX 命令:以 Redis 为例,
SETNX
命令用于在键不存在时设置键的值。当客户端尝试获取分布式锁时,执行SETNX lock_key value
命令,其中lock_key
是锁的键,value
可以是客户端的唯一标识或其他相关信息。如果命令返回1
,表示设置成功,即获取锁成功;如果返回0
,表示键已存在,锁已被其他客户端获取。释放锁时,通过DEL lock_key
命令删除键。为防止锁被长时间占用,通常会给锁设置过期时间,可使用EXPIRE
命令或在SETNX
时通过参数直接设置。 -
Redisson 框架:Redisson 是一个在 Redis 的基础上实现的分布式锁和分布式同步器的框架。它提供了更丰富的功能和更便捷的使用方式。例如,Redisson 的可重入锁
RLock
,可以像在 Java 中使用普通锁一样方便地在分布式环境中使用。它还支持锁的自动续期,当一个客户端持有锁的时间超过了设置的有效期的一半时,Redisson 会自动延长锁的有效期,避免因业务处理时间过长导致锁提前释放。
-
- 基于分布式协调服务实现
-
基于 Zookeeper 实现:Zookeeper 通过节点的创建和观察机制来实现分布式锁。客户端在 Zookeeper 的指定节点下创建一个临时顺序节点,然后获取该节点下所有子节点的列表,并判断自己创建的节点是否是序号最小的。如果是,则获取锁成功;否则,需要监听比自己序号小的前一个节点的删除事件。当监听到前一个节点被删除时,再次判断自己是否是序号最小的节点,若是则获取锁。释放锁时,只需删除自己创建的临时节点即可。Zookeeper 的分布式锁实现具有较高的可靠性和稳定性,因为 Zookeeper 能够保证数据的一致性和顺序性,并且在节点故障时能够自动进行选举和恢复。
-
基于 etcd 实现:etcd 是一个分布式键值存储系统,它也可以用于实现分布式锁。客户端通过在 etcd 中创建一个唯一的键,并设置租约(Lease)来表示锁。当客户端获取锁时,它会创建一个带有租约的键,如果创建成功,则获取锁成功。其他客户端在尝试获取锁时,会发现键已存在,从而获取锁失败。租约可以设置一个有效期,当租约到期后,键会自动被删除,相当于释放了锁。etcd 的分布式锁实现利用了其强大的分布式一致性算法和键值存储功能,能够提供高效、可靠的锁服务。
-
6.3 应用场景
分布式锁在分布式系统中有着广泛的应用场景,包括但不限于:
-
高并发下的资源争抢:如秒杀活动中的库存扣减,通过分布式锁确保在每次扣减库存时,只有一个线程或服务实例能够对库存进行修改,从而避免并发冲突。
-
分布式任务调度:确保定时任务在整个集群中只有一个节点执行,避免多个实例同时执行相同任务导致的资源浪费。
-
数据一致性保障:控制跨服务数据修改顺序,确保数据的一致性。
-
全局唯一操作:如生成订单号或流水号,通过分布式锁确保在同一时刻,只有一个实例能够执行生成唯一ID的操作,从而保证ID的唯一性。
-
分布式配置更新:确保在更新配置时,只有一个进程或线程可以进行操作,从而避免配置更新冲突。
6.4 优缺点
优点:
-
解决分布式环境下数据一致性的问题。
-
提高系统的可用性和稳定性。
-
通过分布式锁,可以避免多个客户端同时对同一个资源进行读写操作,从而减少系统的负载和资源消耗。
缺点:
-
实现复杂度高:分布式锁需要协调多个节点之间的操作,实现起来相对复杂,需要处理各种异常情况和网络延迟等问题。
-
性能开销大:由于分布式锁涉及到多个节点的协调和通信,因此相对于单机锁来说,性能开销较大。
-
可能存在死锁问题:在分布式环境下,由于多个客户端可能同时持有锁,如果处理不当,可能会导致死锁问题的发生。
6.5 什么是幂等,如何解决幂等性问题
在分布式系统和网络协议中,幂等性是一个重要的概念,它要求对一个资源发起一次请求和多次请求的效果是相同的。以下是几种常见的解决幂等性问题的方案和代码示例:
一、使用数据库唯一主键
工作原理:
利用数据库的唯一索引或主键约束来保证数据的唯一性。当重复数据尝试插入时,数据库会抛出异常,从而阻止重复操作。
适用场景:
适用于新增类请求,确保不会因重复提交而产生冗余数据。
代码示例:
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
order_number VARCHAR(50) NOT NULL UNIQUE, -- 唯一订单号
amount DECIMAL(10,2) NOT NULL, status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class OrderService {
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public void createOrder(String orderNumber, int userId, double amount) {
String sql = "INSERT INTO orders(order_number, user_id, amount) VALUES(?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, orderNumber);
pstmt.setInt(2, userId);
pstmt.setDouble(3, amount);
pstmt.executeUpdate();
System.out.println("订单创建成功!");
} catch (SQLException e) {
if (e.getErrorCode() == 1062) {
System.out.println("订单已存在, 无法重新创建!");
}
System.out.println("数据库错误: " + e.getMessage());
}
}
public static void main(String[] args) {
OrderService service = new OrderService();
service.createOrder("ORDER123", 1, 100.0);
service.createOrder("ORDER123", 1, 100.0); // 重复提交 }
}
二、业务状态校验
工作原理:
根据业务ID的唯一性和业务处理的结果去做判断,但需要考虑原子性,否则会因并发问题导致幂等失效。解决途径包括加锁(单机或分布式锁)或采用现成方案(如Tomato)。
适用场景:
适用于更新类请求,确保业务状态的一致性。
代码示例(假设使用分布式锁):
// 使用Redisson实现分布式锁
RLock lock = redissonClient.getLock("order:" + orderId);
try { lock.lock();
// 业务逻辑处理
} finally {
lock.unlock();
}
三、数据库乐观锁
工作原理:
在更新数据时,会根据预先设置的乐观锁状态来判断是否被其他操作修改过。如果状态符合预期,则进行更新;反之,需重新进行处理,以保证数据一致性和操作的幂等性。
适用场景:
适用于并发更新场景,确保数据不会被重复修改。
代码示例:
在数据库中增加一个版本号字段(如version
),每次更新数据时检查版本号:
UPDATE orders SET status = ?, version = version + 1 WHERE order_number = ? AND version = ?;
public void updateOrderStatus(String orderNumber, String newStatus) {
String sql = "UPDATE orders SET status = ?, version = version + 1 WHERE order_number = ? AND version = ?"; // ... 省略数据库连接和查询当前版本号的代码 ...
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, newStatus);
pstmt.setString(2, orderNumber);
pstmt.setInt(3, currentVersion);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
throw new RuntimeException("更新失败,版本号不匹配");
}
} catch (SQLException e) {
// 处理SQL异常
}
}
四、防重Token令牌
工作原理:
在请求处理流程中生成一个唯一的Token,并将其与请求绑定。在后续请求中,通过检查Token的有效性来判断请求是否重复。
适用场景:
适用于需要防止重复提交的场景,如表单提交、支付请求等。
代码示例:
生成Token并存储到Redis中:
String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("token:" + orderId, token, 10, TimeUnit.MINUTES);
在请求处理时检查Token:
String requestToken = request.getParameter("token");
String storedToken = redisTemplate.opsForValue().get("token:" + orderId);
if (requestToken != null && requestToken.equals(storedToken)) {
// 处理请求
redisTemplate.delete("token:" + orderId);
// 请求处理完毕后删除Token } else {
// 请求重复或无效
}
五、下游传递唯一序列号
工作原理:
在调用下游服务时,为每个请求分配一个唯一的序列号。下游服务在接收到请求时,检查序列号是否已处理过。
适用场景:
适用于微服务架构中的服务间调用,确保请求的唯一性和幂等性。
代码示例:
生成唯一序列号并传递给下游服务:
String requestId = UUID.randomUUID().toString(); // 将requestId作为请求参数传递给下游服务
下游服务在接收到请求时检查序列号:
if (processedRequestIds.contains(requestId)) {
// 请求已处理过,直接返回结果或抛出异常
} else {
// 处理请求,并将requestId添加到已处理集合中
processedRequestIds.add(requestId);
}
6.6 你对一致性hash算法的理解
一致性哈希算法(Consistent Hashing) 是分布式系统中用于解决数据分片、负载均衡以及节点动态扩缩容问题的核心算法。它的核心思想是通过哈希环的抽象结构,将数据与节点映射到同一个环上,从而在节点变化时最小化数据的迁移量。以下是其核心要点和设计哲学:
1. 为什么需要一致性哈希?
在传统哈希算法(如 hash(key) % N
)中,数据通过取模分配到固定数量的节点上。但存在两个关键问题:
-
节点增减时数据迁移量大:当节点数量(N)变化时,几乎所有数据的哈希结果都会改变,导致大规模数据迁移。
-
负载不均:节点数量变化可能导致某些节点负载骤增或骤减。
一致性哈希的核心目标:在节点加入或离开时,仅影响相邻节点的数据,保持大部分数据的位置不变,从而降低系统波动。
2. 一致性哈希的工作原理
步骤1:构建哈希环
-
将哈希值的取值范围(如
0 ~ 2^32-1
)首尾相连形成一个环。 -
节点映射:对每个节点(如服务器IP)计算哈希值,将其映射到环上。
-
数据映射:对数据的键(Key)计算哈希值,同样映射到环上。
步骤2:数据定位
-
数据存储在环上顺时针方向第一个遇到的节点上。
-
示例:若数据哈希值为
1000
,而节点哈希值为800
和1500
,则数据归属1500
节点。
步骤3:节点动态变化
-
新增节点:仅影响新节点与前一节点之间的数据(原属于下一节点的部分数据迁移到新节点)。
-
移除节点:该节点的数据迁移到顺时针下一个节点。
3. 虚拟节点(Virtual Nodes)
一致性哈希的早期实现存在负载不均问题(节点分布稀疏时,数据可能集中在少数节点)。通过引入虚拟节点优化:
-
虚拟节点:为每个物理节点生成多个虚拟节点(如
NodeA#1
、NodeA#2
),分散在环的不同位置。 -
优势:
-
平衡负载:物理节点在环上的分布更均匀。
-
灵活扩缩容:通过调整虚拟节点数量控制节点权重(如高性能节点分配更多虚拟节点)。
-
4. 一致性哈希的优缺点
优点 | 缺点 |
---|---|
节点增减时数据迁移量小 | 实现复杂度较高(需管理虚拟节点) |
天然支持动态扩缩容 | 需要额外存储虚拟节点与物理节点映射关系 |
负载相对均衡(配合虚拟节点) | 环的哈希空间划分需合理设计 |
5. 实际应用场景
-
分布式缓存(如Redis Cluster):
-
数据分片到多个节点,节点扩容时避免全量数据迁移。
-
-
负载均衡(如Nginx、Dubbo):
-
请求按一致性哈希分配到固定后端服务,保持会话粘性(Session Affinity)。
-
-
分布式数据库(如Cassandra):
-
数据分片存储,支持动态增减节点。
-
-
CDN网络:
-
内容按哈希分配到最近的边缘节点,减少延迟。
-
6. 示例:Redis Cluster的数据分片
-
Redis Cluster采用哈希槽(Hash Slot,共16384个槽)实现变种的一致性哈希。
-
每个节点负责一部分槽,槽与数据通过
CRC16(key) % 16384
映射。 -
节点变化时,仅需迁移受影响槽的数据。
总结
一致性哈希通过哈希环和虚拟节点技术,在分布式系统中实现了:
-
高扩展性:节点动态扩缩容时数据迁移量最小。
-
负载均衡:数据均匀分布,避免热点问题。
-
容错性:节点故障时,数据自动迁移到下一节点。
核心原则:在不可靠的环境中,通过算法降低变化带来的系统性影响。