2024年Web前端最新慎入,高并发水这么深,你能顶的住吗?(1),2024年最新面试的时候答不上来怎么办

最后

四轮技术面+一轮hr面结束,学习到了不少,面试也是一个学习检测自己的过程,面试前大概复习了 一周的时间,把以前的代码看了一下,字节跳动比较注重算法,面试前刷了下leetcode和剑指offer, 也刷了些在牛客网上的面经。大概就说这些了,写代码去了~

祝大家都能收获大厂offer~

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

篇幅有限,仅展示部分内容

nginx_upstream_check_module 支持 TCP 心跳和 HTTP 心跳来实现健康检查。

流量控制

====

流量分发

====

流量分发就不多说了,上面已经讲了,是接入层的基本功能。

流量切换

====

我听朋友说过一个有意思的事情,他们公司将流量从一个机房切到另一个机房,结果翻车,所有工程师运维平台一片飘红,全公司集体围观,运维团队就很丢面子。

高并发,我把握不住啊

流量切换就是在某些情况下,比如机房故障、光纤被挖断、服务器故障故障情况,或者灰度发布、A/B等运维测试场景,需要将流量切到不同的机房、服务器等等。

就像我们上面提到的负载均衡典型架构,不同层级的负载负责切换不同层级的流量。

  1. DNS:切换机房入口。

  2. HttpDNS:主要 APP 场景下,在客户端分配好流量入口,绕过运营商 LocalDNS并实现更精准流量调度。

  3. LVS/HaProxy:切换故障的 Nginx 接入层。

  4. Nginx:切换故障的应用层。

另外,有些应用为了更方便切换,还可以在 Nginx 接入层做切换,通过 Nginx 进行一些流量切换,而没有通过如 LVS/HaProxy 做切换。

限流

==

限流是保证系统可用的一个重要手段,防止超负荷的流量直接打在服务上,限流算法主要有令牌桶、漏桶。

高并发,我把握不住啊

可以在很多层面做限流,例如服务层网关限流、消息队列限流、Redis限流,这些主要是业务上的限流。

这里我们主要讨论的是接入层的限流,直接在流量入口上限流。

对于 Nginx接入层限流可以使用 Nginx自带的两个模块:连接数限流模块

ngx_http_limit_conn_module 和漏桶算法实现的请求限流模块

ngx_http_limit_req_moduleo

还可以使用 OpenResty提供的 Lua限流模块 ua-resty**-**limit-traffic应对更复杂的限流场景。

limmit_conn用来对某个 key 对应的总的网络连接数进行限流,可以按照如 IP、域名维度进行限流。limit_req用来对某个 key对应的请求的平均速率进行限流,有两种用法:平滑模式(delay ) 和允许突发模式(nodelay)。

流量过滤

====

很多时候,一个网站有很多流量是爬虫流量,或者直接是恶意的流量。

可以在接入层,对请求的参数进行校验,如果参数校验不合法,则直接拒绝请求,或者把请求打到专门用来处理非法请求的服务。

最简单的是使用Nginx,实际场景可能会使用OpenResty,对爬虫 user-agent 过滤和一些恶意IP (通过统计 IP 访问量来配置阈值),将它们分流到固定分组,这种情况会存在一定程度的误杀,因为公司的公网 IP —般情况下是同一个,大家使用同一个公网出口 IP 访问网站,因此,可以考虑 IP+Cookie 的方式,在用户浏览器种植标识用户身份的唯一 Cookie。访问服务前先种植 Cookie, 访问服务时验证该 Cookie, 如果没有或者不正确,则可以考虑分流到固定分组,或者提示输入验证码后访问。

高并发,我把握不住啊

降级

==

降级也是保证高可用的一把利剑,降级的思路是“弃车保帅”,在眼看着不能保证全局可用的情况下,抛弃或者限制一些不重要的服务。

降级一般分为多个层级,例如在应用层进行降级,通过配置中心设置降级的阈值,一旦达到阈值,根据不同的降级策略进行降级。

也可以把降级开关前置到接入层,在接入层配置功能降级开发,然后根据情况行自动/人工降级。后端应用服务出问题时,通过接入层降级,可以避免无谓的流量再打到后端服务,从而给应用服务有足够的时间恢复服务。

Web层

====

经过一系列的负载均衡,用户终于请求到了web层的服务。web服务开发完成,经过部署,运行在web服务器中给用户提供服务。

集群

==

一般会根据业务模块,来划分不同的服务,一个服务部署多个实例组成集群。

高并发,我把握不住啊

为了隔离故障,可以再将集群进行分组,这样一个分组出现问题,也不会影响其它分组。像比较常问的秒杀,通常会将秒杀的服务集群和普通的服务集群进行隔离。

能做到集群化部署的三个要点是无状态、拆分、服务化。

  • 无状态:设计的应用是无状态的,那么应用比较容易进行水平扩展。

  • 拆分:设计初期可以不用拆分,但是后期访问量大的时候,就可以考虑按功能拆分系统。拆分的维度也比较灵活,根据实际情况来选择,例如根据系统维度、功能维度、读写维度、AOP 维度、模块维度等等。

  • 服务化:拆分更多的是设计,服务化是落地,服务化一般都得服务治理的问题。除了最基本的远程调用,还得考虑负载均衡、服务发现、服务隔离、服务限流、服务访问黑白名单等。甚至还有细节需要考虑,如超时时间、重试机制、服务路由、故障补偿等。

Web服务器

======

独立开发一个成熟的 Web 服务器,成本非常高,况且业界又有那么多成熟的开源 Web 服务器,所以互联网行业基本上都是 “拿来主义” ,挑选一个流行的开源服务器即可。大一点的公司,可能会在开源服务器的基础上,结合自己的业务特点做二次开发,例如淘宝的 Tengine,但一般公司基本上只需要将开源服务器摸透,优化一下参数,调整一下配置就差不多了。

服务器的选择主要和开发语言相关,例如,Java 的有 Tomcat、JBoss、Resin 等,PHP/Python 的用 Nginx。

Web服务器的性能之类的一般不会成为瓶颈,例如Java最流行的Web服务器Tomcat默认配置的最大请求数是 150,但是没有关系,集群部署就行了。

容器

==

容器是最近几年才开始火起来的,其中以 Docker 为代表,在 BAT 级别的公司已经有较多的应用。

容器化可以说给运维带来了革命性的变化。Docker 启动快,几乎不占资源,随时启动和停止,基于Docker 打造自动化运维、智能化运维逐渐成为主流方式。

容器化技术也天生适合当前流行的微服务,容器将微服务进程和应用程序隔离到更小的实例里,使用更少的资源,更快捷地部署。结合容器编排技术,可以更方便快速地搭建服务高可用集群。

高并发,我把握不住啊

服务层

===

开发框架

====

一般,互联网公司都会指定一个大的技术方向,然后使用统一的开发框架。例如,Java 相关的开发框架 SSH、SpringBoot, Ruby 的 Ruby on Rails, PHP 的 ThinkPHP, Python 的Django 等。

框架的选择,有一个总的原则:优选成熟的框架,避免盲目追逐新技术!

对于一般的螺丝工而言,所做的主要工作都是在这个开发框架之下。对于开发语言和框架的使用,一定要充分了解和利用语言和框架的特性。

以Java为例,在作者的开发中,涉及到一个加密解密的服务调用,服务提供方利用了JNI的技术——简单说就是C语言编写代码,提供api供Java调用,弥补了Java相对没那么底层的劣势,大大提高了运算的速度。

在服务开发这个日常工作的层面,可以做到这些事情来提高性能:

  • 并发处理,通过多线程将串行逻辑并行化。

  • 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。

  • 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。

  • 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法

  • 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。

  • JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。

  • 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。

可以通过这些事情来提高可用性:

  • 设置合适的超时时间、重试次数及机制,必要时要及时降级,返回兜底数据等,防止把服务提方供打崩

  • 防重设计:通过防重key、防重表等方式实现防重

  • 幂等设计:在接口层面实现幂等设计

服务中心

====

当系统数量不多的时候,系统间的调用一般都是直接通过配置文件记录在各系统内部的,但当系统数量多了以后,这种方式就存在问题了。

比如说总共有 10 个系统依赖 A 系统的 X 接口,A 系统实现了一个新接口 Y, 能够更好地提供原有 X 接口的功能,如果要让已有的 10 个系统都切换到 Y 接口,则这 10 个系统的几十上百台器的配置都要修改,然后重启,可想而知这个效率是很低的。

服务中心的实现主要采用服务名字系统。

  • 服务务名字系统 (Service Name System)

看到这个翻译,相信你会立刻联想到 DNS, 即 Domain Name System。没错,两者的性质是基本类似的。

DNS 的作用将域名解析为 IP 地址,主要原因是我们记不住太多的数字 IP, 域名就容易记住。服务名字系统是为了将 Service 名称解析为 “host + port + 接口名称” ,但是和 DNS一样,真正发起请求的还是请求方。

高并发,我把握不住啊

在微服务的架构下,实现这个功能的称之为注册中心,例如在Java语言体系下,开源的注册中心有Nacos、Ecuraka等。

配置中心

====

配置中心就是集中管理各个服务的配置。

在服务不多的时候,各个服务各自管理自己的配置,没有问题,但是当服务成百上千,再各行其政,就是一个比较头疼的事。

所以将配置中心抽象成公共的组件,集中配置多个系统,操作效率高。

在微服务架构体系下,配置中心的开源方案有SpringCloud的SpringCloud Config、阿里的Nacos等。

服务框架

====

服务拆分最直接的影响就是本地调用的服务变成了远程调用,服务消费者A需要通过注册中心去查询服务提供者B的地址,然后发起调用,这个看似简单的过程就可能会遇到下面几种情况,比如:

  • 注册中心宕机;

  • 服务提供者B有节点宕机;

  • 服务消费者A和注册中心之间的网络不通;

  • 服务提供者B和注册中心之间的网络不通;

  • 服务消费者A和服务提供者B之间的网络不通;

  • 服务提供者B有些节点性能变慢;

  • 服务提供者B短时间内出现问题。

怎么去保证服务消费者成功调用服务生产者?这就是服务治理框架要解决的问题。

在Java语言体系下,目前流行的服务治理框架有SpringCloud和Dubbo。

以SpringCloud为例:

高并发,我把握不住啊

  • Feign封装RestTemplate实现http请求方式的远程调用

  • Feign封装Ribbon实现客户端负载均衡

  • Euraka集群部署实现注册中心高可用

  • 注册中心心跳监测,更新服务可用状态

  • 集成Hystrix实现熔断机制

  • Zuul作为API 网关 ,提供路由转发、请求过滤等功能

  • Config实现分布式配置管理

  • Sluth实现调用链路跟踪

  • 集成ELK,通过Kafka队列将日志异步写入Elasticsearch,通过Kibana可视化查看

SpringCloud是一整套完整微服务解决方案,被称为“SpringCloud 全家桶”。这里只是简单地介绍一下。

Dubbo主要提供了最基础的RPC功能。

不过SpringCloud的RPC采用了HTTP协议,可能性能会差一些。

利好的是,“SpringCloud2.0”——SpringCloud Alibaba流行了起来,Dubbo也可以完美地融入SpringCloud的生态。

消息队列

====

消息队列在高性能、高扩展、高可用的架构中扮演着很重要的角色。

消息队列是用来解耦一些不需要同步调用的服务或者订阅一些自己系统关心的变化。使用消息队列可以实现服务解耦(一对多消费)、异步处理、流量削峰/缓冲等。

服务解耦

====

服务解耦可以降低服务间耦合,提高系统系统的扩展性。

例如一个订单服务,有多个下游,如果不用消息队列,那么订单服务就要调用多个下游。如果需求要再加下游,那么订单服务就得添加调用新下流的功能,这就比较烦。

引入消息队列之后,订单服务就可以直接把订单相关消息塞到消息队列中,下游系统只管订阅就行了。

高并发,我把握不住啊

异步处理

====

异步处理可以降低响应时间,提高系统性能。

随着业务的发展项目的请求链路越来越长,这样一来导致的后果就是响应时间变长,有些操作其实不用同步处理,这时候就可以考虑采用异步的方式了。

高并发,我把握不住啊

流量削峰/缓冲

=======

流量削峰/缓冲可以提高系统的可用性。

我们前面提到了接入层的限流,在服务层的限流可以通过消息队列来实现。网关的请求先放入消息队列中,后端服务尽可能去消息队列中消费请求。超时的请求可以直接返回错误,也可以在消息队列中等待。

高并发,我把握不住啊

消息队列系统基本功能的实现比较简单,但要做到高性能、高可用、消息时序性、消息事务性则比较难。业界已经有很多成熟的开源实现方案,如果要求不高,基本上拿来用即可,例如,RocketMQ、Kafka、ActiveMQ 等。

但如果业务对消息的可靠性、时序、事务性要求较高时,则要深入研究这些开源方案,提前考虑可能会遇到的问题,例如消息重复消费、消息丢失、消息堆积等等。

平台层

===

当业务规模比较小、系统复杂度不高时,运维、测试、数据分析、管理等支撑功能主要由各系统或者团队独立完成。随着业务规模越来越大,系统复杂度越来越高,子系统数量越来越多,如果继续采取各自为政的方式来实现这些支撑功能,会发现重复工作非常多。所以就会自然地把相关功能抽离出来,作为公共的服务,避免重复造轮子,减少不规范带来的沟通和协作成本。

平台层是服务化思维下的产物。将公共的一些功能拆分出来,让相关的业务服务只专注于自己的业务,这样有利于明确服务的职责,方便服务扩展。

同时一些公共的平台,也有利于各个服务之间的统筹,例如数据平台,可以对数据进行聚合,某个服务以前需要一些整合一些数据可能要调用多个上游服务,但是引入数据平台以后,只需要从数据平台取数据就可以了,可以降低服务的响应时间。

运维平台

====

运维平台核心的职责分为四大块:配置、部署、监控、应急,每个职责对应系统生命周期的一个阶段,如下图所示:

高并发,我把握不住啊

  • 部署:主要负责将系统发布到线上。例如,包管理、灰度发布管理、回滚等。

  • 监控:主要负责收集系统上线运行后的相关数据并进行监控,以便及时发现问题。

  • 应急:主要负责系统出故障后的处理。例如,停止程序、下线故障机器、切换 IP 等。

运维平台的核心设计要素是“四化"——标准化、平台化、自动化、可视化。

  • 标准化:要制定运维标准,规范配置管理、部署流程、监控指标、应急能力等,各系统按照运维标准来实现,避免不同的系统不同的处理方式。

  • 平台化:传统的手工运维方式需要投入大量人力,效率低,容易出错,因此需要在运维标准化的基础上,将运维的相关操作都集成到运维平台中,通过运维平台来完成运维工作。

  • 自动化:传统手工运维方式效率低下的一个主要原因就是要执行大量重复的操作,运维平台可以将这些重复操作固化下来,由系统自动完成。

  • 可视化:运维平台有非常多的数据,如果全部通过人工去查询数据再来判断,则效率很低,可视化的主要目的就是为了提升数据查看效率。

测试平台

====

测试平台核心的职责当然就是测试了,包括单元测试、集成测试、接口测试、性能测试等,都可以在测试平台来完成。

测试平台的核心目的是提升测试效率,从而提升产品质量,其设计关键就是自动化。

高并发,我把握不住啊

数据平台

====

数据平台的核心职责主要包括三部分:数据管理、数据分析和数据应用。每一部分又包含更多的细分领域,详细的数据平台架构如下图所示:

高并发,我把握不住啊

  1. 数据管理

数据管理包含数据采集、数据存储、数据访问和数据安全四个核心职责,是数据平台的基础功能。

  • 数据采集:从业务系统搜集各类数据。例如,日志、用户行为、业务数据等,将这些数据传送到数据平台。

  • 数据存储:将从业务系统采集的数据存储到数据平台,用于后续数据分析。

  • 数据访问:负责对外提供各种协议用于读写数据。例如,SQL、 Hive、 Key-Value 等读写协议。

  • 数据安全:通常情况下数据平台都是多个业务共享的,部分业务敏感数据需要加以保护,防止被其他业务读取甚至修改,因此需要设计数据安全策略来保护数据。

  1. 数据分析

数据分析包括数据统计、数据挖掘、机器学习、深度学习等几个细分领域。

  • 数据挖掘:数据挖掘这个概念本身含义可以很广,为了与机器学习和深度学习区分开,这里的数据挖掘主要是指传统的数据挖掘方式。例如,有经验的数据分析人员基于数据仓库构建一系列规则来对数据进行分析从而发现一些隐含的规律、现象、问题等,经典的数据挖掘案例就是沃尔玛的啤酒与尿布的关联关系的发现。

  • 机器学习、深度学习:机器学习和深度学习属于数据挖掘的一种具体实现方式,由于其实现方式与传统的数据挖掘方式差异较大,因此数据平台在实现机器学习和深度学习时,需要针对机器学习和深度学习独立进行设计。

  1. 数据应用

数据应用很广泛,既包括在线业务,也包括离线业务。例如,推荐、广告等属于在线应用,报表、欺诈检测、异常检测等属于离线应用。数据应用能够发挥价值的前提是需要有 “大数据” ,只有当数据的规模达到一定程度,基于数据的分析、挖掘才能发现有价值的规律、现象、问题等。如果数据没有达到一定规模,通常情况下做好数据统计就足够了,尤其是很多初创企业,无须一开始就参考 BAT 来构建自己的数据平台。

管理平台

====

管理平台的核心职责就是权限管理,无论是业务系统(例如,淘宝网) 、中间件系统(例如,消息队列 Kafka) , 还是平台系统(例如,运维平台) ,都需要进行管理。如果每个系统都自己来实现权限管理,效率太低,重复工作很多,因此需要统一的管理平台来管理所有的系统的权限。

说到“平台”,不由地想起这几年一会儿被人猛吹,一会儿被人唱衰的“中台”。在平台里的数据平台,其实已经和所谓的“数据中台”类似了。“中台”是个概念性的东西,具体怎么实现,没有统一的标准方案。作者所在的公司,也跟风建了中台,以“数据中台”为例,我们数据中台的建设主要为了数据共享和数据可视化,简单说就是把各个业务模块的一些数据汇聚起来。说起来简单,落地很难,数据汇聚的及时性、数据共享的快速响应……最终的解决方案是采购了阿里的一些商业化组件,花了老鼻子钱,但是效果,不能说一地鸡毛,也差不多吧。

缓存层

===

虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的。

绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的 90%以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。

如果直接从DB中取数据,有两个问题,一个是DB查询的速度有瓶颈,会增加系统的响应时间,一个是数据库本身的并发瓶颈。缓存就是为了弥补读多写少场景下存储系统的不足。

在前面我们提到的CDN可以说是缓存的一种,它缓存的是静态资源。

从整个架构来看,一般采用多级缓存的架构,在不同层级对数据进行缓存,来提升访问效率。

高并发,我把握不住啊

简单说一下整体架构和流程,缓存一级一级地去读取,没有命中再去读取下一级,先读取本地缓存,再去读取分布式缓存,分布式缓存也没有命中,最后就得去读取DB。

分布式缓存

=====

为了提高缓存的可用性,一般采用分布式缓存。分布式缓存一般采用分片实现,即将数据分散到多个实例或多台服务器。算法一般釆用取模和一致性哈希。

要采用不过期缓存机制,可以考虑取模机制,扩容时一般是新建一个集群。

高并发,我把握不住啊

而对于可以丢失的缓存数据,可以考虑一致性哈希,即使其中一个实例出问题只是丢一小部分。

高并发,我把握不住啊

对于分片实现可以考虑客户端实现,或者使用如Twemproxy 中间件进行代理(分片对客户端是透明的)。

如果使用 Redis, 则 可 以考虑使用 redis-cluster 分布式集群方案。

高并发,我把握不住啊

热点本地缓存

======

对于那些访问非常频繁的热点缓存,如果每次都去远程缓存系统中获取,可能会因为访问量太大导致远程缓存系统请求过多、负载过高或者带宽过高等问题,最终可能导致缓存响应慢,使客户端请求超时。

一种解决方案是通过挂更多的从缓存,客户端通过负载均衡机制读取从缓存系统数据。不过也可以在客户端所在的应用/代理层本地存储一份,从而避免访问远程缓存,即使像库存这种数据,在有些应用系统中也可以进行几秒钟的本地缓存,从而降低远程系统的压力。

缓存的引入虽然提高了系统的性能,但同时也增加了系统的复杂度,带来了一些运维的成本。

缓存穿透

====

缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据,结果存储系统也没有数据。

缓存穿透的示意图:

高并发,我把握不住啊

一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。

通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。

这种情况的解决办法有两种:

一种比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值) 存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。

一种需要引入布隆过滤器,它的原理也很简单就是利用高效的数据结构和算法,快速判断出查询的Key是否在数据库中存在,不存在直接返回空,存在就去查了DB,刷新KV再返回值。

缓存击穿

====

缓存击穿和缓存穿透也有点难以区分,缓存穿透表示的是缓存和数据库中都没有数据,缓存击穿表示缓存中没有数据而数据库中有数据。缓存击穿是某个热点的key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。

缓存击穿示意图:

高并发,我把握不住啊

关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。

主要有两个解决办法:

  • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库。这种方式会阻塞其他的线程,此时系统的吞吐量会下降

  • 热点数据缓存永远不过期。

永不过期有两种方式:

  • 物理不过期,针对热点key不设置过期时间

  • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建

缓存雪崩

====

缓存雪崩,指的是是缓存不可用,或者同一时刻是大量热点key失效。

两种情况导致的同样的后果就是大量的请求直接落在数据库上,对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求,最严重的后果就是直接导致数据库宕机,可能会引起连锁反应,导致系统崩溃。

高并发,我把握不住啊

缓存雪崩的解决方案可以分为三个维度:

  • 事前:

① 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。

② 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。

③ 热点数据缓存永远不过期。

④ 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用 Redis集群等方式来避免 Redis 全盘崩溃的情况。

  • 事中:

① 互斥锁:在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降

② 使用熔断机制,限流降级。当流量达到一定的阈值,直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。

  • 事后:

① 开启Redis持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。

存储层

===

不管是为了满足业务发展的需要,还是为了提升自己的竞争力,关系数据库厂商(Oracle、DB2、MySQL 等)在优化和提升单个数据库服务器的性能方面也做了非常多的技术优化和改进。但业务发展速度和数据增长速度,远远超出数据库厂商的优化速度,尤其是互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。

读写分离

====

读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是其基本架构图:

高并发,我把握不住啊

读写分离的基本实现是:

  • 数据库服务器搭建主从集群,一主一从、一主多从都可以。

  • 数据库主机负责读写操作,从机只负责读操作。

  • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。

  • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。

高并发,我把握不住啊

读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:主从复制延迟和分配机制。

复制延迟

====

以 MySQL 为例,主从复制延迟可能达到 1 秒,如果有大量数据同步,延迟 1 分钟也是有可能的。

主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻 (1 秒 内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。

比如说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的 ID 写入消息队列,再由队列处理机依据 ID 在从库中 获取微博信息再发送给审核系统。此时如果主从数据库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。

高并发,我把握不住啊

解决主从复制延迟的常见方法:

  1. 数据的冗余

我们可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中重新查询数据。

  1. 使用缓存

我们可以在同步写数据库的同时,也把微博的数据写入到缓存里面,队列处理机在获取微博信息的时候会优先查询缓存,这样也可以保证数据的一致性。

  1. 二次读取

我们可以对底层数据库访问的API进行封装,一次读取从库发现不实时之后再读取一次,例如我们通过微博ID没有在从库里读到微博,那么第二次就直接去主库读取。

  1. 查询主库

我们可以把关键业务,或者对实时性有要求的业务读写操作全部指向主机,非关键业务或者实时性要求不高的业务采用读写分离。

分配机制

====

将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。

  1. 程序代码封装

程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为 “中间层封装” ) ,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离,基本架构是:

高并发,我把握不住啊

程序代码封装的方式具备几个特点:

  • 实现简单,而且可以根据业务做较多定制化的功能。

  • 每个编程语言都需要自己实现一次,无法通用,如果一个业务包含多个编程语言写的多个子系统,则重复开发的工作量比较大。

  • 故障情况下,如果主从发生切换,则可能需要所有系统都修改配置并重启。

如果不想自己造轮子,也可以用开源的方案,淘宝的TDDL是比较出名的一个。

高并发,我把握不住啊

  1. 中间件封装

中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。

其基本架构是:

高并发,我把握不住啊

数据库中间件的方式具备的特点是:

  • 能够支持多种编程语言,因为数据库中间件对业务服务器提供的是标准 SQL 接口。

  • 数据库中间件要支持完整的 SQL 语法和数据库服务器的协议(例如,MySQL 客户端和服务器的连接协议) ,实现比较复杂,细节特别多,很容易出现 bug, 需要较长的时间才能稳定。

  • 数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。

  • 数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态。例如,向某个测试表写入一条数据,成功的就是主机,失败的就是从机。

目前开源的数据库中间件有基于 MySQL Proxy 开发的奇虎 360 的 Atlas 、阿 里 的Cobar、基于 Cobar 开发的 Mycat 等。

分库分表

====

读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到干万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:

  • 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。

  • 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。

  • 数据文件越大,极端情况下丟失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。

基于上述原因,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围内。为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。

业务分库

====

业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。

高并发,我把握不住啊

虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我们详细分析一下。

  1. join 操作问题

业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查 询。

例如: "查询购买了化妆品的用户中女性用户的列表〃 这个功能,虽然订单数据中有用户的 ID信息,但是用户的性别数据在用户数据库中,如果在同一个库中,简单的 join 查询就能完成;但现在数据分散在两个不同的数据库中,无法做 join 查询,只能采取先从订单数据库中查询购买了化妆品的用户 ID 列表,然后再到用户数据库中查询这批用户 ID 中的女性用户列表,这样实现就比简单的 join 查询要复杂一些。

  1. 事务问题

原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL 的 XA) , 但性能实在太低,与高性能存储的目标是相违背的。

例如,用户下订单的时候需要扣商品库存,如果订单数据和商品数据在同一个数据库中,我们可订单,如果因为订单数据库异常导致生成订单失败,业务程序又需要将商品库存加上;而如果因为业务程序自己异常导致生成订单失败,则商品库存就无法恢复了,需要人工通过曰志等方式来手工修复库存异常。

  1. 成本问题

业务分库同时也带来了成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台。

基于上述原因,对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因:初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。业务分库后,表之间的 join 查询、数据库事务无法简单实现了。

业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏。

单表拆分

====

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。

单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:

高并发,我把握不住啊

分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。

两种分表方式可以用一个例子比喻,我们很多人可能都看过这么一篇文章,怎么把苹果切出星星来,答案是横着切。

高并发,我把握不住啊

  1. 垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,前面示意图中的nickname 和 desc 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外—张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取name、age、sex、nickname、description, 现在需要两次查询,—次查询获取 name、age、 sex, 另一次查询获取 nickname、desc。

不过相比接下来要讲的水平分表,这个复杂性就是小巫见大巫了。

  1. 水平分表

水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到干万级别时,这很可能是架构的性能瓶颈或者隐患。

水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:

  • 路由

水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。

常见的路由算法有:

高并发,我把握不住啊

范围路由:选取有序的数据列 (例如,整形、时间戳等) 作为路由的条件,不同分段分散到不同的数据库表中。以订单 Id 为例,路由算法可以按照 1000万 的范围大小进行分段。范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至2000 万之间,具体需要根据业务选取合适的分段大小。

范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。

Hash 路由:选取某个列 (或者某几个列组合也可以) 的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。同样以订单 id 为例,假如我们一开始就规划了 4个数据库表,路由算法可以简单地用 id % 4 的值来表示数据所属的数据库表编号,id 为 12的订单放到编号为 50的子表中,id为 13的订单放到编号为 61的字表中。

Hash 路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了 Hash 路由后,增加字表数量是非常麻烦的,所有数据都要重分布。

Hash 路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。

配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。

同样以订单id 为例,我们新增一张 order_router 表,这个表包含 orderjd 和 tablejd 两列 , 根据 orderjd 就可以查询对应的 table_id。

配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。

配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据) ,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。

  • join 操作

水平分表后,数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询,然后将结果合并。

  • count()操作

分表后就没那么简单了。常见的处理方式有下面两种:

count() 相加:具体做法是在业务代码或者数据库中间件中对每个表进行 count操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低。例如,水平分表后切分为 20 张表,则要进行 2 0 次 count()操作,如果串行的话,可能需要几秒钟才能得到结果。

记录数表:具体做法是新建一张表,假如表名为 "记录数表” ,包含 table_name、 row_count两个字段,每次插入或者删除子表数据成功后,都更新 "记录数表“。这种方式获取表记录数的性能要大大优于 count()相加的方式,因为只需要一次简单查询就可以获取数据。缺点是复杂度增加不少,对子表的操作要同步操作 “记录数表” ,如果有一个业务逻辑遗漏了,数据就会不一致;且针对 “记录数表” 的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导致数据不一致。

此外,记录数表的方式也增加了数据库的写压力,因为每次针对子表的 insert 和 delete 操作都要 update 记录数表,所以对于一些不要求记录数实时保持精确的业务,也可以通过后台定时更新记录数表。定时更新实际上就是 “count()相加” 和 “记录数表” 的结合,即定时通过count()相加计算表的记录数,然后更新记录数表中的数据。

  • order by 操作

水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。

实现方法

和数据库读写分离类似,分库分表具体的实现方式也是 “程序代码封装” 和 “中间件封装” ,但实现会更复杂。读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断SELECT、UPDATE、 INSERT、DELETE 几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。例如 order by 操作,需要先从多个库查询到各个库的数据,然后再重新 order by 才能得到最终的结果。

数据异构

====

完成分库分表以后,我们看到存在一些问题,除了"程序代码封装" 和 "中间件封装"之外,我们还有一种办法,就是数据异构。数据异构就是将数据进行异地存储,比如业务上将MySQL的数据,写一份到Redis中,这就是实现了数据在集群中的异地存储,也就是数据异构。

在数据量和访问量双高时使用数据异构是非常有效的,但增加了架构的复杂度。异构时可以通过双写、订阅 MQ 或者 binlog 并解析实现。

  • 双写:在写入数据的时候,同时将数据写入MySQL和异构存储系统;

  • MQ:写入MySQL成功后,发一个mq消息,缓存读取mq消息并将消息写入异构存储系统;

  • binlog:写入MySQL后,缓存系统x消费binlog,将变动写入异构存储系统。

这是一个异构的数据架构示意图:

高并发,我把握不住啊

在图中用到了ES搜索集群来处理搜索业务,同样也可以我们前面提到的跨库join的问题。

最后

四轮技术面+一轮hr面结束,学习到了不少,面试也是一个学习检测自己的过程,面试前大概复习了 一周的时间,把以前的代码看了一下,字节跳动比较注重算法,面试前刷了下leetcode和剑指offer, 也刷了些在牛客网上的面经。大概就说这些了,写代码去了~

祝大家都能收获大厂offer~

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

篇幅有限,仅展示部分内容

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值