首页 > 编程语言 >10分钟带你看完Java架构设计演变

10分钟带你看完Java架构设计演变

时间:2024-07-04 17:54:06浏览次数:18  
标签:10 缓存 Java 架构设计 数据库 发送 MQ 消息 key

原文:https://mp.weixin.qq.com/s/LAY8JEn4FJaL06lJtRt4ag

关于Java架构方面的面试经常都会被问到,“千万、上亿级别的流量应该我们应该怎么处理”,我之前面试的时候也被问过几次,还被问过以下问题:

现在面对业务急剧增长你会怎么处理?
业务量突然增长100倍、1000倍怎么处理?
怎么来处理高并发的?
怎么设计一个高并发系统?
高并发系统都有什么特点?
... ...
诸如此类,问法很多。

这类问题对于大多数人来说可能较为棘手,因为看似复杂且难以入手。然而,我们可以通过一个常规的思路来回答,即围绕如何合理设计系统以支持高并发业务场景展开讨论。一旦你能够想到这一点,接下来我们就可以从硬件和软件层面探讨如何支撑高并发。本质上,这个问题旨在综合考察你对各个细节的处理能力以及是否有相关经验。

在面对超高并发的情况下,首先需要确保硬件层面的机器具备足够的承载能力。其次,在架构设计方面,应采用微服务的拆分策略,以提高系统的可伸缩性和灵活性。在代码层面,需要合理运用各种缓存、削峰和解耦等技术手段,以优化系统的性能和稳定性。同时,在数据库层面,应实施读写分离和分库分表的策略,以提高数据库的吞吐量和响应速度。此外,为了确保系统的稳定性,必须建立完善的监控机制,并采取熔断、限流和降级等措施,以便及时发现和处理潜在的问题。通过以上措施,可以初步构建一个高性能、高可用的系统设计。

微服务架构演变
在互联网的早期阶段,单体架构足以满足日常业务需求。所有业务服务都集成在一个项目中,部署在一台物理机器上。交易系统、会员信息、库存、商品等各个业务模块紧密耦合在一起。然而,一旦流量激增,单体架构的问题便显露无疑:一旦机器出现故障,整个业务将无法正常运行。

图片

因此,分布式集群架构应运而生。当单个服务器无法承受压力时,最简单有效的方法是进行水平扩展和横向扩容。通过负载均衡技术,将流量分配到不同的服务器上,从而暂时解决单点故障导致服务不可用的问题。

图片

随着业务的不断发展,在一个项目中维护所有业务场景的开发和代码维护变得越来越困难。即使是一个简单的需求变更,也需要发布整个服务,导致代码合并冲突频繁发生,同时线上故障的风险也不断增加。为了解决这些问题,微服务的架构模式应运而生。

图片

通过将每个独立的业务拆分为独立的部署单元,可以降低开发和维护的成本,并提高集群的可承受压力。此外,不再需要对一个微小的更改点进行全局性的改动。从高并发的角度来看,这些优点都可以归因于通过服务拆分和集群物理机器的扩展来提高整体系统的抗压能力。然而,随着拆分而来的问题也需要在高并发系统中解决。

远程RPC服务
微服务的拆分带来了显著的好处和便利性,但同时需要关注各个微服务之间的通信。传统的HTTP通信方式对性能造成了巨大的浪费,因此需要引入类似Dubbo的RPC框架,采用基于TCP长连接的方式,以提高整个集群的通信效率。

图片

假设客户端的初始QPS为12000,通过负载均衡策略将其分散到每台服务器上,每台服务器的QPS为4000。当将HTTP接口改为RPC接口后,接口的响应时间缩短,从而提升了单机和整体的QPS。此外,RPC框架通常自带负载均衡和熔断降级机制,以更好地维护系统的高可用性。接下来,我们将探讨Dubbo作为国内普遍选择的一些基本原理。Dubbo工作原理:

服务启动的时候,provider和consumer根据配置信息,连接到注册中心register,分别向注册中心注册和订阅服务;

register根据服务订阅关系,返回provider信息到consumer,同时consumer会把provider信息缓存到本地。如果信息有变更,consumer会收到来自register的推送;

consumer生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor记录接口的调用次数和时间信息;

拿到代理对象之后,consumer通过代理对象发起接口调用;

provider收到请求后对数据进行反序列化,然后通过代理调用具体的接口。

图片

Dubbo负载均衡策略
1.加权随机:假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上就可以了。

2.最小活跃数:每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。

3.一致性hash:通过hash算法,把provider的invoke和随机节点生成hash,并将这个 hash 投射到 [0, 2^32 - 1] 的圆环上,查询的时候根据key进行md5然后进行hash,得到第一个节点的值大于等于当前hash的invoker。

图片

4.加权轮询:比如服务器 A、B、C 权重比为 5:2:1,那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。

集群容错
Failover Cluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为1也就是只调用一次。

Failback Cluster快速失败:在调用失败,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每隔5秒对失败的调用进行重试

Failfast Cluster失败自动恢复:只会调用一次,失败后立刻抛出异常

Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果

Forking Cluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回了结果,就会立刻返回结果

Cluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常。

消息队列
消息队列(MQ)在微服务架构中扮演着重要的角色,其主要功能包括削峰填谷和解耦。通过依赖消息队列,将同步操作转变为异步方式,可以有效降低微服务之间的耦合度。

对于一些不需要同步执行的接口,我们可以采用引入消息队列的方式来实现异步执行,从而提高接口的响应时间。例如,在交易完成后需要扣除库存并给会员发放积分的场景中,发放积分的动作本质上属于履约服务,对实时性的要求并不高。我们只需确保最终一致性,即履约成功即可。对于这类具有相似性质的请求,可以通过MQ进行异步处理,从而提高系统的抗压能力。

图片

对于消息队列而言,怎么在使用的时候保证消息的可靠性、不丢失?

消息可靠性
消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。

生产者丢失

生产者丢失消息的潜在风险主要源于程序在发送失败时未进行重试处理,或者在发送过程中网络短暂中断导致MQ未能接收到消息。由于同步发送方式通常不会出现此类问题,我们将重点讨论异步发送场景。
异步发送可分为两种模式:带回调的异步发送和不带回调的异步发送。在不带回调的模式中,生产者在发送完消息后不关心结果,这可能导致消息丢失。为解决这一问题,我们可以通过结合异步发送、回调通知以及本地消息表的方式来实现。以下以订单创建为例进行说明。

1.下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。

2.下单成功,直接返回客户端成功,异步发送MQ消息

3.MQ回调通知消息发送结果,对应更新数据库MQ发送状态

4.JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试

5.在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。

图片

一般而言,对于大部分场景来说异步回调的形式就可以了,只有那种需要完全保证不能丢失消息的场景我们做一套完整的解决方案。

MQ丢失

在生产者确保消息成功发送至MQ,但MQ在收到消息后尚未将其持久化到磁盘时发生宕机,且未能及时将消息同步给从节点,这种情况下可能导致消息丢失。以RocketMQ为例:
RocketMQ支持同步刷盘和异步刷盘两种模式,其中默认为异步刷盘。这种模式可能导致消息在未被持久化到硬盘前就丢失。为提高消息可靠性,可以设置为同步刷盘模式,这样即使MQ出现故障,恢复时仍可从磁盘中恢复丢失的消息。

比如Kafka也可以通过配置做到:

acks=all 只有参与复制的所有节点全部收到消息,才返回生产者成功。这样的话除非所有的节点都挂了,消息才会丢失。
replication.factor=N,设置大于1的数,这会要求每个partion至少有2个副本
min.insync.replicas=N,设置大于1的数,这会要求leader至少感知到一个follower还保持着连接
retries=N,设置一个非常大的值,让生产者发送失败一直重试
虽然我们可以通过配置的方式来达到MQ本身高可用的目的,但是都对性能有损耗,怎样配置需要根据业务做出权衡。

消费者丢失

在消费者刚收到消息时,若服务器发生故障,MQ会认为消费者已成功消费并停止重复发送。这种情况下,可能导致消息丢失。以RocketMQ为例:
RocketMQ默认要求消费者回复确认(ack),而Kafka则需要手动配置关闭自动offset功能。若消费者未返回确认,根据不同的MQ类型,重发机制的发送间隔和次数可能有所不同。若重试次数超过限制,消息将进入死信队列,需手动处理。(Kafka没有这些)。

图片

消息的最终一致性
事务消息可以达到分布式事务的最终一致性,事务消息就是MQ提供的类似XA的分布式事务能力。

半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。

实现原理如下:

1.生产者先发送一条半事务消息到MQ
2.MQ收到消息后返回ack确认
3.生产者开始执行本地事务
4.如果事务执行成功发送commit到MQ,失败发送rollback
5.如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
6.生产者查询事务执行最终状态
7.根据查询事务状态再次提交二次确认
最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

图片

数据库
在高并发系统中,数据库作为核心支撑组件承载着所有流量的查询和写入操作。为了降低数据库压力并提升其性能,实现高并发能力的基础是采用读写分离和分库分表的策略。

以系统整体视角来看,流量呈现漏斗状分布。例如,日活跃用户(DAU)为30万,而实际每天访问提单页面的用户仅为5万次请求每秒(QPS),最终成功下单支付的用户仅有2万QPS。在这种情况下,系统的读操作需求大于写操作需求。因此,通过实施读写分离策略,可以有效减轻数据库的压力。

图片

读写分离相当于采用数据库集群的方式,以减轻单个节点的压力。随着数据量的急剧增长,传统的单库单表存储方式已无法满足业务发展需求。因此,需要对数据库进行分库分表处理。对于微服务而言,垂直分库已经得到应用,而大部分工作集中在分表方案上。

水平分表
首先,根据业务场景确定分表字段(sharding_key),例如在日订单量达到1000万的情况下,大部分场景来自C端用户。因此,我们可以使用user_id作为sharding_key。数据查询支持最近3个月的订单,超过3个月的订单将进行归档处理。那么,3个月的数据量将达到9亿,可以分成1024张表,每张表的数据量大约为100万。
以用户ID为100为例,我们首先对其进行哈希运算(hash(100)),然后对1024取模,从而确定该用户数据应存储在哪个表中。

分表后的ID唯一性
因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑:

1.设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。

2.分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种

3.分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。

主从同步原理
1.master提交完事务后,写入binlog
2.slave连接到master,获取binlog
3.master创建dump线程,推送binglog到slave
4.slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
5.slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
6.slave记录自己的binglog
图片

由于MySQL默认采用异步复制方式,主库在将日志发送给从库后并不关心从库是否已成功处理。这种机制可能导致一个问题:当主库出现故障,而从库的处理失败时,从库升级为主库后会丢失部分日志信息。因此,这种情况引发了两个关键概念。

全同步复制

主库在将日志写入binlog后,会强制同步这些日志到从库。只有当所有从库都成功执行完这一过程后,主库才会向客户端返回响应。然而,这种方式显然会对系统性能产生显著影响。

半同步复制

与全同步复制不同,半同步复制遵循以下逻辑:从库在成功写入日志后向主库发送确认(ACK),主库收到至少一个从库的确认后,认为写操作已完成。

缓存组件
缓存作为高性能的代表,在某些特殊业务中可能承担超过80%的热点流量。对于一些高并发场景,如秒杀活动,其并发查询每秒(QPS)可能达到数十万级别。在这种情况下,引入缓存预热可以显著减轻对数据库的压力。例如,20万的QPS对于单机数据库来说可能是不可承受的,但对于像Redis这样的缓存系统来说则完全不成问题。

图片

以秒杀系统为例,活动预热商品信息可以提前缓存并提供查询服务,活动库存数据也可以提前缓存。下单流程完全通过缓存扣减实现,秒杀结束后再异步写入数据库,从而显著减轻了数据库的压力。然而,引入缓存后还需考虑缓存击穿、雪崩和热点等一系列问题。

热key问题
所谓的热key问题指的是,在高并发场景下,大量请求突然访问Redis上某个特定的key,导致流量过于集中,超过了物理网卡的上限,进而引发该Redis服务器的宕机和雪崩效应。

图片

针对热key的解决方案:

提前把热key打散到不同的服务器,降低压力

加入二级缓存,提前加载热key数据到内存中,如果redis宕机,走内存查询

缓存击穿
缓存击穿是指在高并发情况下,某一个key的缓存失效,导致所有请求都直接访问数据库,造成数据库压力过大。这种情况与热key问题类似,但区别在于缓存击穿是由于缓存过期导致的请求全部打到数据库上。

解决方案:

  1. 采用加锁更新策略,当请求查询A时,如果缓存中不存在对应的数据,则对A这个key进行加锁。同时,从数据库中查询数据,并将数据写入缓存中。最后将数据返回给用户。这样,后续的请求就可以直接从缓存中获取数据了。

2.将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象。

图片

缓存穿透
缓存穿透是指查询不存在缓存中的数据,每次请求都会打到DB,就像缓存不存在一样。

图片

针对该问题,可以采用布隆过滤器进行优化。布隆过滤器的工作原理是在数据存入时,通过哈希函数将数据映射到位数组中的K个点,并将这些点的值设为1。当用户再次查询A时,如果A在布隆过滤器中的值为0,则直接返回,避免了缓存击穿请求对数据库的影响。
然而,使用布隆过滤器可能会存在误判的问题,因为其本质上是一个数组,有可能存在多个值映射到同一个位置。但是,只要合理设置位数组的长度,误判的概率就会降低。因此,需要根据具体情况进行权衡和调整。

图片

缓存雪崩
当大规模缓存失效时,例如缓存服务宕机,将导致大量请求直接访问数据库,从而可能引发系统崩溃的现象被称为雪崩。与击穿和热key问题不同,雪崩是指大规模的缓存同时过期失效。

图片

针对雪崩几个解决方案:

针对不同key设置不同的过期时间,避免同时过期

限流,如果redis宕机,可以限流,避免同时刻大量请求打崩DB

二级缓存,同热key的方案。

稳定性
图片

熔断

比如营销服务挂了或者接口大量超时的异常情况,不能影响下单的主链路,涉及到积分的扣减一些操作可以在事后做补救。

限流

对突发如大促秒杀类的高并发,如果一些接口不做限流处理,可能直接就把服务打挂了,针对每个接口的压测性能的评估做出合适的限流尤为重要。

降级

熔断之后实际上可以说就是降级的一种,以熔断的举例来说营销接口熔断之后降级方案就是短时间内不再调用营销的服务,等到营销恢复之后再调用。

预案

一般来说,就算是有统一配置中心,在业务的高峰期也是不允许做出任何的变更的,但是通过配置合理的预案可以在紧急的时候做一些修改。

核对

针对各种分布式系统产生的分布式事务一致性或者受到攻击导致的数据异常,非常需要核对平台来做最后的兜底的数据验证。比如下游支付系统和订单系统的金额做核对是否正确,如果收到中间人攻击落库的数据是否保证正确性。

总结
实际上,设计高并发系统并非难事。基于已知的知识点,我们可以从物理硬件层面到软件架构和代码层面进行优化,使用各种中间件来提高系统的抗压能力。然而,这个问题本身会引发一系列其他问题。例如,微服务的拆分可能导致分布式事务问题;HTTP和RPC框架的使用可能带来通信效率、路由和容错问题;消息队列(MQ)的引入可能导致消息丢失、积压、事务消息和顺序消息的问题;缓存的使用可能引发一致性、雪崩和击穿问题;数据库的读写分离和分库分表可能导致主从同步延迟、分布式ID和事务一致性问题。为了解决这些问题,我们需要不断采取各种措施,如熔断、限流、降级、离线核对和预案处理等,以防止和追溯这些问题。

标签:10,缓存,Java,架构设计,数据库,发送,MQ,消息,key
From: https://www.cnblogs.com/huft/p/18284339

相关文章

  • “Java多线程编程:从Thread到Runnable再到Callable的深入探索“
    1什么是进程?通俗地解释为:计算机中正在执行的一个程序实例。进程它是系统分配资源的基本单位。想象一下,你的电脑就像是一个大工厂,而每一个进程就像是这个工厂里的一条生产线或者一个工作小组,它们各自独立地运行着不同的任务,但同时又受到整个工厂(即操作系统)的管理和调度。......
  • java编译时出现错误[ERROR] 不再支持源选项 5。请使用 6 或更高版本。[ERROR] 不再支
    当我引入一个新项目在控制台输入命令mvn  clean install -U,报错出现原因是我们下载了多个java版本,我的电脑上就有1.8和11两个版本,此时只需在引入的pom文件中指定具体的版本即可<maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</mave......
  • Java实现简单的冒泡排序
    Java实现简单的冒泡排序核心思想:把相邻的两个数字两两比较,当一个数字大于右侧相邻的数字时,交换他们的位置,当一个数字和他右侧的数字小于或等于的时候,不交换。(小到大排序)例如有数组{3,1,5,7,4,2}第一次排序{3,1,5,7,4,2}//开始{1,3,5,7,4,2}//1和3互换{1,3,5,7,4,2......
  • 深入探索Java IO与NIO:差异与高性能网络编程的应用
    深入探索JavaIO与NIO:差异与高性能网络编程的应用一、引言在Java中,I/O(Input/Output)操作是应用程序与外部世界交互的基本方式。Java标准库提供了多种I/O模型,其中最常用的有传统的I/O(即阻塞I/O)和新引入的NIO(Non-blockingI/O,非阻塞I/O)。随着网络应用的日益复杂和性能要求的......
  • 速度是conda的10倍以上,mamba的4倍,Pixi是何方神圣呢?真有这么快吗?
    原文链接:速度是conda的10倍以上,mamba的4倍,Pixi是何方神圣呢?真有这么快吗?本期教程写在前面今天中午看到通哥分享的教程,conda转圈圈,为何不试试pixi,Pixi是第一次了解。但是,通过他们的介绍,确实牛X,速度是conda的10倍以上,mamba的4倍。对于自己而言,自从使用了mamba以后,基本不......
  • 每天10个js面试题(一)
    1.js基本数据类型?JavaScript共有八种数据类型,分别是Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。其中Symbol和BigInt是ES6中新增的数据类型2.let、const、var的区别?let和const有暂时性死区,var没有let和const声明的变量具有块级作用域,var没有......
  • 基于Java+Vue的采购管理系统:采购过程合规高效(整套代码)
         前言:采购管理系统是一个综合性的管理平台,旨在提高采购过程的效率、透明度,并优化供应商管理。以下是对各个模块的详细解释:一、供应商准入供应商注册:供应商通过在线平台进行注册,填写基本信息和资质文件。资质审核:系统对供应商提交的资质文件进行自动或人工审核,确......
  • GBU2510-ASEMI储能专用整流桥GBU2510
    编辑:llGBU2510-ASEMI储能专用整流桥GBU2510型号:GBU2510品牌:ASEMI封装:GBU-4正向电流(Id):25A反向耐压(VRRM):1000V正向浪涌电流:200A正向电压(VF):1.10V引脚数量:4芯片个数:4芯片尺寸:88MIL功率(Pd):中小功率设备工作温度:-55°C~150°C类型:整流扁桥、插件整流桥应用领域工业电源......
  • Note -「Analysis」“重聚是你我共同的回答”(S1~S10)
    \[\textit{Litar!}\newcommand{\opn}[1]{\operatorname{#1}}\newcommand{\card}[0]{\opn{card}}\newcommand{\E}[0]{\exist}\newcommand{\A}[0]{\forall}\newcommand{\l}[0]{\left}\newcommand{\r}[0]{\right}\newcommand{\eps}[0]{\varepsilon......
  • Win10关闭断电三次修复功能,并彻底关闭windowupdate服务
    应用环境:某些环境断电较频繁,断电3次造成系统修复,系统修复又需要人为干预,从而造成生产停止,因此需要禁用该功能。1、关闭断电三次修复功能以管理者权限运行cmd.exebcdedit/setbootstatuspolicyignoreallfailuresbcdedit/setrecoveryenabledNobcdedit/set{current}boot......