当前位置:网站首页 > Java基础 > 正文

java基础架构师



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

 

诸如此类,问法很多。

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

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

微服务架构演变

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

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

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

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

远程RPC服务 

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

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

  1. 服务启动的时候,provider和consumer根据配置信息,连接到注册中心register,分别向注册中心注册和订阅服务;
  2. register根据服务订阅关系,返回provider信息到consumer,同时consumer会把provider信息缓存到本地。如果信息有变更,consumer会收到来自register的推送;
  3. consumer生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor记录接口的调用次数和时间信息;
  4. 拿到代理对象之后,consumer通过代理对象发起接口调用;
  5. 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次请求。

集群容错

  1. Failover Cluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为1也就是只调用一次。
  2. Failback Cluster快速失败:在调用失败,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每隔5秒对失败的调用进行重试
  3. Failfast Cluster失败自动恢复:只会调用一次,失败后立刻抛出异常
  4. Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果
  5. Forking Cluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回了结果,就会立刻返回结果
  6. Cluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常。

消息队列

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

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

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

消息可靠性

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

生产者丢失

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

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

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

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

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

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

MQ丢失

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

java基础架构师
 

虽然我们可以通过配置的方式来达到MQ本身高可用的目的,但是都对性能有损耗,怎样配置需要根据业务做出权衡。

消费者丢失

消息的最终一致性

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

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

实现原理如下:

 

最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

数据库

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

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

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

水平分表

分表后的ID唯一性

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

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

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

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

主从同步原理

 

由于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,就像缓存不存在一样。

缓存雪崩

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

针对雪崩几个解决方案:

  1. 针对不同key设置不同的过期时间,避免同时过期
  2. 限流,如果redis宕机,可以限流,避免同时刻大量请求打崩DB
  3. 二级缓存,同热key的方案。

稳定性

熔断

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

限流

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

降级

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

预案

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

核对

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

总结

版权声明


相关文章:

  • java第三方基础平台2024-11-15 16:58:05
  • java基础代码实例2024-11-15 16:58:05
  • java基础架构建设2024-11-15 16:58:05
  • java基础 网络编程2024-11-15 16:58:05
  • java基础1到20的阶乘2024-11-15 16:58:05
  • java四大基础类型2024-11-15 16:58:05
  • java 基础设施2024-11-15 16:58:05
  • 熟悉java基础概念2024-11-15 16:58:05
  • java web基础 知乎2024-11-15 16:58:05
  • java基础注解配置2024-11-15 16:58:05