淘宝二面试常见问题的答案来了,你看了吗?(二面及答案)

本文深入探讨了淘宝二面中常问的技术问题,包括JVM内存结构1.7与1.8的区别,Redis的高性能原因,如内存存储、IO多路复用和数据结构;分析了缓存击穿、雪崩现象及其应对策略;解释了Kafka如何确保幂等性和消息顺序性;阐述了SpringBoot starter的原理;讨论了分库分表的设计方案,包括垂直与水平拆分、读写分离以及Sharding-JDBC的运作方式;介绍了分布式事务的一致性模型(强一致性、弱一致性、最终一致性);此外,还探讨了搜索引擎Elasticsearch的基础概念;最后,总结了保证高并发系统性能的手段,如降级和限流策略,并比较了令牌桶与漏斗算法的差异。
摘要由CSDN通过智能技术生成

 

1、JVM内存结构1.7和1.8的区别

JVM内存分配

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

1.7和1.8区别

其实,移除永久代的工作从JDK 1.7就开始了。JDK 1.7中,存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)、类的静态变量转移到了Java heap。
 

JDK1.8和JDK1.7的jvm内存最大的区别是, 在1.8中方法区是由元空间(元数据区)来实现的,常量池移到堆中.
1.8不存在方法区,将方法区的实现给去掉了,而是在本地内存中,新加入元数据区(元空间).
元空间: 存储.class 信息, 类的信息,方法的定义,静态变量等.而常量池放到堆里存储

 

 

2、 redis为什么快(内存、IO多路复用、高效的数据结构)

1.1、可以使用redis-benchmark对Redis的性能进行评估,命令行提供了普通/流水线方式、不同压力评估特定命令的性能的功能。

1.2、redis性能卓越,作为key-value系统最大负载数量级为10W/s, set和get耗时数量级为10ms和5ms。使用流水线的方式可以提升redis操作的性能。

Redis是一个单线程应用,所说的单线程指的是Redis使用单个线程处理客户端的请求。
虽然Redis是单线程的应用,但是即便不通过部署多个Redis实例和集群的方式提升系统吞吐, 从官网给出的数据可以看出,Redis处理速度非常快。

Redis性能非常高的原因主要有以下几点:

  • 内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销
  • 单线程实现:Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销
  • 非阻塞IO:Redis使用多路复用IO技术,在poll,epool,kqueue选择最优IO实现
  • 优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能

多路复用IO

在《unix网络编程 卷I》中详细讲解了unix服务器中的5种IO模型。

一个IO操作一般分为两个步骤:

  1. 等待数据从网络到达, 数据到达后加载到内核空间缓冲区
  2. 数据从内核空间缓冲区复制到用户空间缓冲区

按照两个步骤是否阻塞线程,分为阻塞/非阻塞, 同步/异步。

阻塞IO

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

 

非阻塞IO

Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

 

IO多路复用

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

 信号驱动IO

 

异步IO 

Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:

Reids的IO处理

总的来说Redis使用一种封装多种(select,epoll, kqueue等)实现的Reactor设计模式多路复用IO处理客户端的请求。

 

 Reactor设计模式常常用来实现事件驱动。除此之外, Redis还封装了不同平台多路复用IO的不同的库。处理过程如下:

 因为 Redis 需要在多个平台上运行,同时为了最大化执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块。

Redis 会优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。

但是如果当前编译环境没有上述函数,就会选择 select 作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差 O(n),并且只能同时服务 1024 个文件描述符,所以一般并不会以 select 作为第一方案使用。

丰富高效的数据结构

Redis提供了丰富的数据结构,并且不同场景下提供不同实现。

Redis作为key-value系统,不同类型的key对应不同的操作或者操作对应不同的实现,相同的key也会有不同的实现。Redis对key进行操作时,会进行类型检查,调用不同的实现。

为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:

redisObject 对象。
基于 redisObject 对象的类型检查。
基于 redisObject 对象的显式多态函数。
对 redisObject 进行分配、共享和销毁的机制。

3、缓存击穿、缓存雪崩

可查看我的这篇文章,里面有详细介绍:

springMVC、spring、控制反转、依赖注入、MyBatis、springBoot、springSecurity、Java多线程、Redis(缓冲击穿,穿透、雪崩、热点数据集中失效)icon-default.png?t=LA92https://blog.csdn.net/mzl_sx/article/details/117016319?spm=1001.2014.3001.5502

4、kafka怎么保证幂等性

保证生产者的消息可靠性

从本质上来说,生产者与Broker之间是通过网络进行通讯的,因此保障生产者的消息可靠性就必须要保证网络可靠性,这里Kafka使用acks=all可以设置Broker收到消息并同步到所有从节点后给生产者一个确认消息。如果生产者没有收到确认消息就会多次重复向Broker发送消息,保证在生产者与Broker之间的消息可靠性。

保证Broker的消息可靠性

在Broker收到了生产者的消息后,也有可能会丢失消息,最常见的情况是消息到达某个Broker后服务器就宕机了。这里需要补充说明一下Kafka的高可用性,直观的看,Kafka一般可被分成多个Broker节点,而为了增加Kafka的吞吐量,一个topic通常被分为多个partition,每个partition分布在不同的Broker上。如果一个partition丢失就会导致topic内容的部分丢失,因此partition往往需要多个副本,以此来保证高可用。

保证消费者的消息可靠性

这里比较容易发生消息丢失的情况是,消费者从Broker把消息拉过来,如果这个时候还没有消费完,消费者就挂了并且消费者自动提交了offset,那么此时就丢失了一条消息。所以解决办法就是关闭自动提交offset,等真正消费成功之后再手动提交offset。

幂等性

保证消息的幂等性,其实就是保证消息不会被重复消费。幂等性的保证需要根据具体业务具体分析,比如向MySQL插入一条订单信息,可以根据订单id查询数据库中是否已经存在该信息进行去重。如果是类似于Redis的设置key值,Redis天然支持消息的幂等性,所以这种情况下是不需要关心消息的幂等性的。总之,对于幂等性的保证完全可以根据业务需求进行具体分析。

有序性

消息队列在某些场景下需要严格保证消息被消费的顺序,想象一个场景,你看到你的男朋友在朋友圈晒别的女生的照片,于是你在下面评论了一句:“渣男”,然后把他的微信删了。这时候如果这两条消息的消费顺序是反过来的,也就是先删除微信,然后进行评论,那么是无法评论的。

前面说到可靠性的时候我们提到了Kafka的topic是由多个partition组成的,那么我们可以用一种最极端的方式保证消息有序性,一个topic只设置一个partition。这里的问题就是如果一个topic只对应一个partition,那么这个吞吐量肯定就大幅下降了,这就违背了Kafka的设计初衷。

还有一种方法是比较推荐的,由于不同partition之间是不能保证有序性的,因此保证消息在同一个partition中是保证消息有序性的关键,除了前面说的那种极端解决方案,其实还可以在发送消息时,指定一个partition,或者指定一个key,因为同一个key的消息可以保证只发送到同一个partition,这里的key一般可以用类似userid的属性来表示。在上面的场景来看就是妹子的userid先是进行了评论操作,又进行了删除好友的操作,这两个操作由于是同一个key值,因此被发往同一个partition中。

5、springboot的starter

starter能够抛弃以前繁杂的配置,将其统一集成进starter,使用的时候只需要在maven中引入对应的starter依赖即可,Spring Boot就能自动扫描到要加载的信息并启动相应的默认配置。

starter让我们摆脱了各种依赖库的处理,以及各种配置信息的烦恼。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。Spring Boot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

Spring Boot starter原理

从总体上来看,无非就是将Jar包作为项目的依赖引入工程。而现在之所以增加了难度,是因为我们引入的是Spring Boot Starter,所以我们需要去了解Spring Boot对Spring Boot Starter的Jar包是如何加载的?下面我简单说一下。

SpringBoot 在启动时会去依赖的 starter 包中寻找 /META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包,这类似于 Java 的 SPI 机制。

细节上可以使用@Conditional 系列注解实现更加精确的配置加载Bean的条件。

JavaSPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

自定义starter的条件

如果想自定义Starter,首选需要实现自动化配置,而要实现自动化配置需要满足以下两个条件:

  1. 能够自动配置项目所需要的配置信息,也就是自动加载依赖环境;
  2. 能够根据项目提供的信息自动生成Bean,并且注册到Bean管理容器中;

概括:

  1. Spring Boot在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包,
  2. 然后读取spring.factories文件获取配置的自动配置类AutoConfiguration`,
  3. 然后将自动配置类下满足条件(@ConditionalOnXxx)的@Bean放入到Spring容器中(Spring Context)
  4. 这样使用者就可以直接用来注入,因为该类已经在容器中了。

6、分库分表怎么设计

一. 分表

对于访问极为频繁且数据量巨大的单表来说,我们首先要做的就是减少单表的记录条数,以便减少数据查询所需要的时间,提高数据库的吞吐,这就是所谓的分表!

二. 分库

场景:分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库master

服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。

因此,我们必须换一种思路,对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库

分表的方案

当单表达到几千万的时候,单表数据量太大,会极大影响 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,单表到几百万的时候,性能就会相对差一些了,就得分表了。

根据数值取模

采用Id取模的方式来进行分表。比如那customer表举例,将customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,余数为2的放到第三个库,余数为3的放到第三个库。这样同一个用户的数据会分散到不同的表中,如果查询条件带有cusno字段,则可明确定位到相应表去查询。

优点:

将一个数据表的数据分成多个表后,数据相对比较均匀,减轻来高并发访问带来的数据库压力。

缺点:

后期如果扩容时,需要迁移旧的数据重新计算。

跨分表查询复杂性增加。比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。

根据数值范围

为了解决后期集群扩容需要迁移旧数据的问题,可以使用按日期或者ID来进行分表。例如:按日期将不同月甚至是日的数据分散到不同的表中;将cusno为1~9999的记录分到第一个表,10000~20000的分到第二个表,以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。

优缺点

优点:

单表大小可控

天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移

使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

缺点:

热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询。

分库

就是你一个库一般最多支撑到并发 2000,随着查询量的增加单台数据库服务器已经没办法支撑,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大,太大会导致单台DB的存储空间不够。那么你就可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

垂直拆分

垂直拆分意思就是处理数据库的列,列和对应的业务有关系,意思就是就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。

联想到微服务,做法与大系统拆分为多个小系统类似,按业务分类进行独立划分,每个微服务使用单独的一个数据库。比如最初就一个数据库为db_shop,db_shop库包含user,product表,随着公司业务的发展,技术团队人员也得到了扩张,划分为不同的技术小组,不同的小组负责不同的业务模块。例如A小组负责用户模块,B小组负责产品模块,拆分为db_user库和db_product库

需要解决的问题:跨数据库的事务、jion查询等问题。

水平拆分

按照规则划分,一般水平分库是在垂直分库之后的。比如每天处理的订单数量是海量的,可以按照一定的规则水平划分。比如某张表太大,单个数据库存储不下或访问性能有压力,把一张表拆成多张,每张表存放部分记录,保存在不同的数据库里,水平分库需要对系统做大的改造。

1)Scale up,升级数据库所在的物理机,提升内存/存储/IO性能,但这种升级费用昂贵,并且只能满足短期需要。

2)Scale out,把订单库拆分为多个库,分散到多台机器进行存储和访问,这种做法支持水平扩展,可以满足长远需要。

订单库主要包括订单主表/订单明细表(记录商品明细)/订单扩展表,水平分库即把这3张表的记录分到多个数据库中,订单水平分库效果如下图所示:

 

分库策略:和水平分表类似

分库维度确定后,如何把记录分到各个库里呢?一般有两种方式:

根据数值范围,比如用户Id为1-9999的记录分到第一个库,10000-20000的分到第二个库,以此类推。

根据数值取模,比如用户Id mod n,余数为0的记录放到第一个库,余数为1的放到第二个库,以此类推。

需要解决的问题:数据路由、组装。

读写分离

对于时效性不高的数据,可以通过读写分离缓解数据库压力。

需要解决的问题:在业务上区分哪些业务上是允许一定时间延迟的,以及数据同步问题。

垂直分库-->水平分库-->读写分离

7、分库分表之后java应用要怎么处理

需要解决如下问题

2.1原有事务

由于分库分表之后,新表在另外一个数据库中,如何保证主库和分库的事务性是必须要解决的问题。

解决办法:通过在主库中创建一个流水表,把操作数据库的逻辑映射为一条流水记录。当整个大事务执行完毕后(流水被插入到流水表),然后通过其他方式来执行这段流水,保证终一致性。

2.2流水

所谓流水,可以理解为一条事务消息

通过在数据库中创建一张流水表,使用一条流水记录代表一个业务处理逻辑,因此,一个流水一定是能终正确执行的.因此,当把一段业务代码提取流水中必须要考虑到:

流水延迟处理性。流水不是实时处理的,而是用过流水执行器来异步执行的。因此,如果在原有逻辑中,需要特别注意后续流程对该流水是不是有实时依赖性(例如后续业务逻辑中会使用流水结果来做一些计算等)。

流水处理无序性。保证即使后生成的流水先执行,也不能出现问题。

流水终成功性。对每条插入的流水,该条流水一定要保证能执行成功

因此,提取流水的时候:

流水处理越简单越好

流失处理依赖越少越好

提取的流水在该业务逻辑中无实时性依赖

2.4流水处理完成

因为流水表是放在原数据库中,而流水处理完成后是操作分库,如果分库操作完成去更新老表流水消息,那么又是夸库事务,如何保证流水状态的更新和分库也是在一个事务的?

解决办法是:在分库中创建一个流水表,当流失处理完成以后,不是去更新老表状态,而是插入分库流水表中、

这样做的好处:

一般会对流水做索引,那么如果流水重复多次执行的时候,插入分库流水表的时候肯定由于索引检测不通过,整个事务就会回滚(当然也可以在处理流水事前应该再做一下幂等性判断)

这样通过判断主库流水是否在分库中就能判断一条流水是否执行完毕

三、流水处理器基本框架

流水处理器其实不包含任何业务相关的处理逻辑,核心功能就是:

通知业务接入方何时处理什么样的流水

检验流水执行的成功

注:流水执行器并不知道该流水表示什么逻辑,具体需要业务系统去识别后去执行相对应业务逻辑。

3.1流水执行任务

流水处理调度任务就是通过扫描待处理的流水,然后通知业务系统该执行哪一条流水。

3.2流水校验任务

流水校验任务就是要比较主库和分库中的流水记录,对执行未成功的流水通知业务系统进行重新处理,如果多次重试失败则发出告警。

8、sharding-jdbc原理

Sharding-JDBC简介

Sharding-JDBC是当当应用框架ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据库分库分表访问。Sharding-JDBC是继dubbox和elastic-job之后,ddframe系列开源的第3个项目。

Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:

  • 可适用于任何基于Java的ORM框架,如JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC。
  • 可基于任何第三方的数据库连接池,如DBCP、C3P0、 BoneCP、Druid等。
  • 理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle、SQLServer等数据库的计划。

Sharding-JDBC定位为轻量Java框架,使用客户端直连数据库,以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。

Sharding-JDBC分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。

SQL解析功能完善,支持聚合、分组、排序、limit、or等查询,并支持Binding Table以及笛卡尔积表查询。

9、分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。例如在大型电商系统中,下单接口通常会扣减库存、减去优惠、生成订单 id, 而订单服务与库存、优惠、订单 id 都是不同的服务,下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

强一致性、弱一致性、最终一致性

强一致性

任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。

弱一致性

数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

最终一致性

不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。

CAP 原则

CAP 原则又称 CAP 定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性(C):

在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):

在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

分区容错性(P):

以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。

CAP 原则的精髓就是要么 AP,要么 CP,要么 AC,但是不存在 CAP。如果在某个分布式系统中数据无副本, 那么系统必然满足强一致性条件, 因为只有独一数据,不会出现数据不一致的情况,此时 C 和 P 两要素具备,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了 CP 系统,但是 CAP 不可同时满足。

10、会不会搜索引擎(es)

Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式的全文搜索引擎,基于restful web接口。Elasticsearch是用Java语言开发的,基于Apache协议的开源项目,是目前最受欢迎的企业搜索引擎。Elasticsearch广泛运用于云计算中,能够达到实时搜索,具有稳定,可靠,快速的特点。

相关概念

  • Near Realtime(近实时):Elasticsearch是一个近乎实时的搜索平台,这意味着从索引文档到可搜索文档之间只有一个轻微的延迟(通常是一秒钟)。
  • Cluster(集群):群集是一个或多个节点的集合,它们一起保存整个数据,并提供跨所有节点的联合索引和搜索功能。每个群集都有自己的唯一群集名称,节点通过名称加入群集。
  • Node(节点):节点是指属于集群的单个Elasticsearch实例,存储数据并参与集群的索引和搜索功能。可以将节点配置为按集群名称加入特定集群,默认情况下,每个节点都设置为加入一个名为elasticsearch的群集。
  • Index(索引):索引是一些具有相似特征的文档集合,类似于MySql中数据库的概念。
  • Type(类型):类型是索引的逻辑类别分区,通常,为具有一组公共字段的文档类型,类似MySql中表的概念。注意:在Elasticsearch 6.0.0及更高的版本中,一个索引只能包含一个类型。
  • Document(文档):文档是可被索引的基本信息单位,以JSON形式表示,类似于MySql中行记录的概念。
  • Shards(分片):当索引存储大量数据时,可能会超出单个节点的硬件限制,为了解决这个问题,Elasticsearch提供了将索引细分为分片的概念。分片机制赋予了索引水平扩容的能力、并允许跨分片分发和并行化操作,从而提高性能和吞吐量。
  • Replicas(副本):在可能出现故障的网络环境中,需要有一个故障切换机制,Elasticsearch提供了将索引的分片复制为一个或多个副本的功能,副本在某些节点失效的情况下提供高可用性。
  • 可参考如下这篇:Elasticsearch快速入门,掌握这些刚刚好! - 掘金

11、还有哪些保证高并发的手段

  • 高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。本专栏将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这 4 个方面重点介绍。
  • 一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,我将用一篇文章来专门讲解如何设计秒杀减库存方案。
  • 高可用。 虽然我介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。专栏的最后,我将带你思考可以从哪些环节来设计兜底方案。

设计高并发系统时应该注意的5个架构原则

高并发系统本质上就是一个满足大并发、高性能和高可用的分布式系统

设计原则

数据操作要尽量少

  1. 请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据
  2. 返回的数据能少就少, 减少后端序列化的时间
  3. 数据库操作能少就少 , 这个就不说了

请求数要尽量少

这个就很直观了 , 请求数少并发量就少

调用链路要尽量短

高并发系统 1. 要降低系统依赖 , 防止因为依赖造成的各种问题 , 提高可用性 2. 降低流量入侵 , 大流量尽量隔绝在外面

不要有单点

系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控 , 其次流量不能分发像redis这种会有热点数据问题

12、降级和限流怎么做

限流

限流场景我们经常遇到,有时候地铁里就被保安人员给我限流了,双十一抢购也被爸爸限流了。坐地铁之所以能限流是因为我们都要安检,有这个统一的地铁入口;浏览网站被限流是因为访问有统一的域名入口。

当我们需要根据路由规则进行限流,只要把握好网关就很方便的实现限流了,以下方案均可行

  • 布点在类似于 WAF(Web 应用防火墙)中,具体见 阿里云WAF手册
  • 如果不想花钱,也可以安装 nginx 限流插件来做类似的工作,自己部署,总之是在 web 应用前面布点拦截操作
  • 也可以在 web 应用里面结合路由组件开发一个限流组件,只要代码有统一入口,就可以方便控制

降级

生活中我也是消费降级的小伙,原来天猫,后来淘宝,现在拼多多和淘宝特价版。消费降级真香,话说回来,重点是又不是不能用。Web 项目中降级的案例,比如微博 feed 流中,用户基本信息压力比较大,而用户的勋章也在该接口中对前端输出,服务降级的重点是又不是不能用

如果是按照现在微服务的理念,勋章查询可能是一个独立的服务,所以降级对应的颗粒度可以是服务降级。既然是独立的服务单元,请求的拦截就又回到了和限流一样的场景;
如果不是微服务架构,接口依赖的用户的勋章输出只是一个独立的函数或者方法,如何进行拦截呢?

方案1. 紧急发布

如果后端服务是 PHP 的脚本语言,我们可以快速的单独发布需要修改的文件,达到快速降级的目的。
如果后端服务是 JAVA 需要编译的,对于这种简单场景的修改,也是支持热部署,单独发布一个 class 文件,也不需要重启也 OK,比如 arthas 就提供类似的功能。

如果发布系统不支持热部署,也不支持单文件发布,只支持发布软件包的方式,那么快速降级就需要 15 ~ 30分钟(业务复杂一点的 Java 应用)才能部署完成,这是互联网应用不能接受的。

方案2. 限流降级中间件

在 Java 生态目前比较成熟,知名的产品有 hystrix,我用的比较多的是 sentinel。支持从路由、方法去做单机的 qps 去限流,只需要在sentinel管控台做配置变更,然后发布推送到各个机器,机器则以最后收到的限流规则单机闭环操作,中间不再需要和中间件服务进行交互。

13、令牌桶和漏斗的区别

漏桶算法和令牌桶算法是接口限流设计中常用的两种算法,网上关于这两个算法的介绍文章有很多,但不同的人有不同的理解,导致很多技术人员在学习的时候,会陷入迷茫的状态,比如说:

1)如果要让自己的系统不被打垮,用令牌桶。如果保证别人的系统不被打垮,用漏桶算法;参考链接

2)在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,所以它适合于具有突发特性的流量。参考链接

我在架构实战营中总结两种算法的技术本质和优缺点分别如下:

 

首先,漏桶和令牌桶的区别是保护自己还是保护别人吗?

很显然不是,令牌桶保护自己和保护下游都可以,而不是说保护自己用令牌桶,保护别人用漏桶。

原因很简单,令牌桶就是一个速率控制,你可以用来控制自己的处理速度,也可以控制请求别人的处理速度,都可以起到保护作用;

其实漏桶也可以既保护自己又保护下游,因为请求太多的时候把请求先缓存到漏桶里面了,漏桶放不下就丢弃新的请求,这也是保护机制。

既然如此,两者的区别是什么呢?

其实就是我在课件上写的:漏桶的本质是总量控制,令牌桶的本质是速率控制。至于这个控制,你可以用来保护自己,也可以用来保护别人。

有的技术人员对于“保护别人”可能不太理解,这个主要应用于访问第三方,最典型的例子就是双十一的支付宝,支付的时候要访问银行的接口,银行的接口实际上处理高并发的能力并不强(例如澳门某银行对外提供的移动支付接口,峰值 TPS 是 30 次/s),这个时候支付宝自己处理性能再高都没用,而且越高越容易把下游的银行给压垮,所以支付宝不得不针对下游接口请求做限流。

网上的文章都说令牌桶更适合“突发流量”,为何你这里说漏桶更适合“瞬时高并发”?

其实,令牌桶的“突发流量”跟我们通常理解的“业务高并发”并不一样。令牌桶的算法原本是用于网络设备控制传输速度的,而且它控制的目的是保证一段时间内的平均速率控制,之所以说令牌桶适合突发流量,是指在网络传输的时候,可以允许某段时间内(一般就几秒)超过平均传输速率,这在网络环境下常见的情况就是“网络抖动”,但这个短时间的突发流量是不会导致雪崩效应,网络设备也能够处理得过来。对应到令牌桶应用到业务处理的场景,就要求即使有突发流量来了,系统自己或者下游系统要真的能够处理的过来,否则令牌桶允许突发流量进来,结果系统或者下游处理不了,那还是会被压垮。

而我说漏桶算法更适合“突发流量”,是指秒杀、抢购、整点打卡签到、微博热点事件这种业务高并发场景,它不是由于“XX 抖动”引起的,而是由业务场景引起的,并且持续的事件可能是几分钟甚至几十分钟,这种业务场景为了用户体验和业务尽量少受损,优先采取的不是丢弃大量请求,而是缓存请求,避免系统出现雪崩效应。因此我们会看到,漏桶和令牌桶都有保护作用,但漏桶的保护是尽量缓存请求(缓存不下才丢),令牌桶的保护主要是丢弃请求(即使系统还能处理,只要超过指定的速率就丢弃,除非此时动态提高速率)。

所以如果在秒杀、抢购、整点打卡签到、微博热点事件这些业务场景用令牌桶的话,会出现大量用户访问出错,因为请求被直接丢弃了;而用漏桶的话,处理可能只是会慢一些,用户体验会更好一些,所以我认为漏桶更适合“突发流量”。

其实,漏桶算法限流还是会丢请求,如果你觉得漏桶算法应对突发流量都还不够好,那就要更进一步,采取排队的方式来实现了,排队的方案其实就是一个更复杂的“漏桶”实现,例如下图中的“消息队列”,本质上就是一个“漏桶”:

 

14、代理模式和装饰器模式的区别

代理模式和装饰者模式是两种常见的设计模式。代理模式是为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰者来包裹真实的对象。

因为这两种模式比较相似,所以把它们放在一起做个比较与总结。

代理模式

代理模式包含代理对象和被代理对象,类图如下:

 代理对象 Proxy 和被代理对象 RealSubject 都继承了 Subject 接口。客户端调用 Proxy 的方法,而 Proxy 则把具体操作委托给 RealSubject 执行。下面是代码实现:

interface Subject {
    void doAction();
}

class RealSubject implements Subject {

    @Override
    public void doAction() {
        System.out.println("RealSubject#doAction");
    }
}

class Proxy implements Subject {
    private Subject subject;

    public Proxy(Subject subject) {
        this.subject = subject;
    }

    @Override
    public void doAction() {
        subject.doAction();
    }
}

public class Client {
    public static void main(String[] args) {
        Subject realSubject = new RealSubject();
        Subject proxy = new Proxy(realSubject);
        proxy.doAction();
    }
}

输出:RealSubject#doAction

在 Client 中,首先创建了一个 realSubject 对象,然后创建一个代理对象 proxy 并且把 realSubject
对象通过构造器传入进去。最后调用代理对象的 doAction,实际执行的是 realSubject 的对应方法。这里通过构造函数的参数将被代理对象传入到代理中,也可以通过其它方式,如提供一个 setSubject 方法。

上面的代理模式,代理对象和被代理对象需要实现相同的接口,所以如果要代理其它接口的对象需要写一个新的代理类。Java 提供了动态代理的功能,可以简化我们的代码。

动态代理

动态代理可以在运行期生成所需要的代理对象,看下面的代码:

class DynamicProxy implements InvocationHandler {
    // 被代理对象的引用
    private Object obj;

    public DynamicProxy(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(obj, args);
        return result;
    }
}

类 DynamicProxy 实现了 InvocationHandler 接口,这个接口中有一个 invoke 方法,被代理的对象的任何方法都是在 invoke 中调用。下面是 Client 的代码:

public class TestDelegate {
    public static void main(String[] args) {
        Subject realSubject = new RealSubject();

        DynamicProxy dynamicProxy = new DynamicProxy(realSubject);
        ClassLoader loader = realSubject.getClass().getClassLoader();
        Subject proxy = (Subject) Proxy.newProxyInstance(loader, new Class[]{Subject.class}, dynamicProxy);

        proxy.doAction();
    }
}

输出:RealSubject#doAction

首先依然是先创建一个需要被代理的对象 realSubject,然后把它传入到 DynamicProxy 的构造函数中。这个 dynamicProxy 还不是我们需要的代理,毕竟它没有实现 Subject 接口。下面通过 Proxy.newProxyInstance 创建了一个 Subject 对象,也就是最终的代理对象。

通过动态代理,创建一个实现了 InvocationHandler 接口的 DynamicProxy 类,通过这个类可以在运行期为各种对象创建对应的代理,比静态代理方便了很多。

装饰者模式

装饰者模式是为了给已有的对象增加一些逻辑,但是不改变已有对象的代码,下面是类图:

 

从上图可以看出,装饰者 Decorator与需要被装饰的对象 ContcreteComponent 实现了相同的接口。具体怎么装饰则由 Decorator 的子类 ConcreteDecorator 决定。

Java 中使用装饰者模式的一个典型的例子是 I/O 对象的创建,比如创建一个 BufferedInputStream 时:

InputStream in = ...
InputStream input = new BufferedInputStream(in);

BufferedInputStream 继承于 FilterInputStream,这个 FilterInputStream 相当于装饰者模式中的 Decorator,它继承了 InputStream 接口。BufferedInputStream 则是一个具体的装饰类,其它还有 DataInputStream 以及 ByteArrayInputStream 等。而传给 BufferedInputstream 的对象 in 则是需要被装饰者。装饰者对被装饰者进行了功能的扩展,但是又不需要修改被装饰者的相应代码,符合“开闭原则”,即对于修改是封闭的,对于扩展则是开放的。

如果是为了给某个类提供更多的功能,继承是一种方案。但是,如果我们的功能有很多种组合,那么为每种组合编写一个继承的类可能需要创建太多的子类。而装饰者模式则可以解决这个问题,只需要为每个功能编写一个装饰类,在运行时组合不同的对象即可实现所需的功能组合。

代理模式和装饰者模式有着很多的应用,这两者具有一定的相似性,都是通过一个新的对象封装原有的对象。二者之间的差异在于代理模式是为了实现对象的控制,可能被代理的对象难以直接获得或者是不想暴露给客户端,而装饰者模式是继承的一种替代方案,在避免创建过多子类的情况下为被装饰者提供更多的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小坏蛋至尊宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值