读书笔记:高并发系统实战派
高并发系统实战派, 谢恩德
第1篇 高并发系统认知
第1章 什么是高并发系统 2
1.1 什么是高并发 2
高井发(High Concurrency),通常是指通过设计保证系统能够同时处理很多请求。即在同一个时间点,有很多的请求同时访问同一个接口。
1.2 高并发系统有哪些关键指标 3
1.2.1 响应时间(Response Time) 3
响应时间,是指从第一次发出请求到收到系统完整响应数据所需的时间。
1.2.2 吞吐量(Throughput) 3
吞吐量指单位时间内系统所处理的用户请求数。
1.2.3 每秒请求数(QPS) 4
QPS指服务器在一 秒内共处理了多少个请求,主要用来表示“读" 请求。
1.2.4 每秒事务数(TPS) 4
TPS即服务器每秒处理的事务数。
1.2.5 访问量(PV) 5
PV (Page View )指页面浏览量。用户每对网站中的1个网页访问1次均被记录1次。
1.2.6 独立访客(UV) 5
UV (Unique Visitor )指访问某个站点或点击某个链接的不同IP地址数。
在同一天内,UV只记录第一次进入网站的具有独立IP地址的访问者,在同一天内访问者再次访问该网站则不计数。
1.2.7 网络流量 5
- 流入流量:从外部访问服务器所消耗的流量。
- 流出流量:服务器对外响应的流量。
1.3 为什么要学习高并发系统 5
1.3.1 提升自身及企业核心竞争力 6
1.3.2 在面试中脱颖而出 6
1.4 对比单体系统、分布式系统和微服务系统 7
1.4.1 单体系统之痛 7
单体系统架构分层也有弊端:
- 从水平方向来看,的确降低了业务的深度复杂性。
- 从垂直方向来看,单体的业务边界不够清晰,因为在各层之间会进行网状的调用,比如,用户展现层的某个模块会调用业务层的多个模块(甚至所有的模块) ;业务层的模块同样会调用数据访问层的多个模块等。
1.4.2 高并发系统之分布式架构 11
分布式架构是指,将相同或者相关的应用放在多台计算机上运行,以达到分布式计算的目的。
通俗来讲,分布式架构就是将一个系统拆分为多个独立的应用,然后它们互相协作,组成一个整体,共同完成任务。
1.4.3 高并发系统之微服务架构 12
(2)围绕业务能力来组织开发团队。
单体系统的开发团队通常是按照技能标准来划分的。例如,分为前端组、服务端组及数据库组等,这样的组是被垂直分割的。即使一个小需求的改动都需要进行跨组的沟通,这是不利于项目正常发展的。
微服务系统开发团队是围绕着业务能力来划分的,一个服务相当于一个小应用,对应着一个特定的业务需求。开发团队规模较小,包含开发人员、测试人员及运维人员等。这样的好处是:沟通成本小,开发效率高。
第2章 从剖析两个高并发系统开始 20
2.1 案例一:千万级流量“秒杀”系统 20
2.1.1 千万级流量“秒杀”系统架构一览 20
2.1.2 动静分离方案设计 24
2.1.3 热点数据处理 27
2.1.4 大流量的高效管控 30
2.1.5 扣减库存的那些事 34
2.1.6 搭建千万级流量“秒杀”系统需要哪些技术 39
2.2 案例二:C2C二手电商平台的社会化治理系统 40
2.2.1 C2C二手电商平台的社会化治理系统架构一览 40
2.2.2 基础服务治理 42
2.2.3 RPC框架服务通信 50
RPC (Remote Procedure Call,远程过程调用)框架,可以让应用中的接口像调用本地方法那样去调用远程服务提供的服务(服务调用者和服务提供者分属于不同进程)。
1 RPC框架的核心原理
(1)RPC框架提供了一个Client Stub组件,其本质上是一个代理类实例,客户端通过它发起方法调用。
(2) Client Stub实现了服务端的接口,它会封装请求信息,主要分为两块:服务名称和请求参数。
(3) Client Stub将上面封装好的信息通过网络(Socket)传输给服务端。
常用的网络模型有4种:
- 同步阻塞I/O(BIO): 在客户端发送请求后,该请求会在内核中阻塞住,直到服务端的响应数据到来后才进行后续处理。对于客户端每次的请求,服务端都需要创建与之对应的线程,以处理到达的请求并响应数据。
- 同步非阻塞I/O:(NIO) 在客户端每次发送请求时,在内核空间中,该请求即使没有等到响应数据也不会阻塞住,会继续往下执行(避免进程阻塞在某一个连接 上,可以在同一个线程中处理所有连接)。同步非阻塞I/O模型需要频繁地轮询,很耗CPU资源。
- I/O多路复用:在客户端每次发送请求时,服务端不需要每次都创建对应的线程去进行处理,而是通过I/O多路复用技术将多个I/O通道复用到一个复用器上。这样可以实现单线程同时处理多个客户端的请求,提升了系统的整体性能。多路复用无须采用多线程的方式,也不用去轮询,只需要阻塞在select()函数 上即可。select()函数同时管理多个连接,并且不断地查看各个连接的请求。
- 异步非阻塞I/O(AIO):客户端在发起一个I/O操作后,不需要等待直接返回。等I/O操作完成之后,操作系统内核会主动通知客户端数据已经准备好了。AIO模型更适用于“连速接数比较多,且请求消耗比较重”的业务场景,但是其编程比较复杂,保行系统支持不好。在Linux中更多的还是采用I/O多路复用模型。
2.2.4 分布式事务管理 55
1.数据一致性
数据一致性是指,数据被多次操作或者以多种方式操作时,能保持业务的不变性。
对于数据一致性问题,很典型的案例就是银行转账:在双方转账前后,双方的余额总和应该是不变的,这就是数据的一致性。
2.数据库事务保证数据一致性
数据一致性的根本问题是如何保证操作的原子性。在关系型数据库中,可以利用其
ACID(Atomic、Consistency、Isolation、Durability )特性来保证原子操作。
关系型数据库的ACID特性。
- Atomic (原子性):事务中的所有语句要么都成功,要么都失败,只要其中一条语句失败就会回滚整个事务,不会出现“更新了其中一张表, 而没有更新其他表”的情况。
- Consistency (一致性) :事务总是从一个有效状态转移到另一个有效状态。即它保证读取的数据总是一致的。
- Isolation (隔离性) :在一个事务中所做的任何操作,在未提交前,其对他事务都是不可见的。事务是互相隔离的,自己运行自己的,互不影响。
- Durability (持久性):只要事务提交成功了,数据的所有修改就会持久化到磁盘中,即使宕机也不会造成数据的丢失或改变。
第2篇 搭建生产级系统
第3章 生产级系统框架设计的细节 64
3.1 幂等性设计——保证数据的一致性 64
一个典型的幕等性问题,由于下单接口没有做好幂等性设计,所以导致用户进行了两次同样的下单操作,系统给用户创建了两个订单。
3.1.1 什么是幂等性 64
所谓幂等性是指,用户对于同一个操作发起一次请求或多次请求,得到的结果都是一样的,不会因为请求了多次而出现异常现象。
2.数据库操作的幂等性分析
数据库的上层业务操作分为CRUD ( 即新增、读取、更新、删除4个动作)。
RESTful 规范中的HTTP请求方法: POST (C)、GET(R)、PUT (U)、DELETE (D)。
- POST:相当于新增,不具备幂等性。
- GET:对资源的获取。在浏览器中通过地址进行访问,每次结果都是-样的,是天然幂等的。
- PUT:将一个资源替换成另一个资源。这是非计算型的更新,无论更新多少次,结果都是一样的, 是天然幕等的。
- DELETE:同数据库的删除动作。无论删除多少次,结果都是一样的, 是天然幂等的。
3.1.2 如何避免重复提交 66
1.利用全局唯一 ID防止重复提交。雪花算法SnowFlow
2.利用"Token + Redis"机制防止重复提交
订单系统提供一个发放Token的接口。这个Token是一个防重令牌,即一串唯一字符串(可以使用UUID算法生成)。
3.1.3 如何避免更新中的ABA问题 68
那如何解决这种ABA问题才能保证并发更新时的幕等性呢?一个常用的方案是使用数据库的乐观锁来解决。即在订单系统的订单表中增加一个字段版本号(version),在每次更新时都判断两个版本号是否相等,如果不相等则不更新。
3.2 接口参数校验——增强服务健壮性 70
随着互联网行业的高速发展,前后端分离的模式已经越来越流行,连接前端与后端的桥梁是接口。
那么,是“先由前端进行参数校验,然后请求接口即可进行实际的后端操作”,还是“除前端需要进行参数校验外,接口也需要进行参数校验”呢?为了提高接口的稳定性和健壮性,接口的参数校验显得尤为重要。
3.2.1 【实战】Spring结合validation进行接口参数校验 70
接口对于参数的校验有多种标准,例如,有些参数有最大值和最小值的约束,有些参数有必须为数字的约束,有些参数有必须为手机号或电子邮箱的约束,有些参数有必须为身份证号的约束等。可以使用Spring 组合符合JSR303标准的validation (一个Java的数据校验包)优雅、高效地校验参数。
3.2.2 【实战】自定义参数校验注解 73
3.3 统一异常设计——跟杂乱无章的异常信息说再见 75
3.3.1 Spring Boot默认的异常处理机制 75
3.3.2 【实战】基于Spring Boot构建全局异常处理 76
在企业级系统中,有必要在框架层进行统一的异常处理封装,如果不进行统一的封装, 则会出现以下的问题:
- 由于没有统一的异常规范逻辑,从而导致每个开发人员都采用自己的异常处理习惯,所以整个系统的异常处理是杂乱无章的。
- 对于杂乱无章的异常信息,前端页面不知道怎么展示给用户,也不知道该如何进行后续的业务逻辑处理。
- 异常处理的不规范会给排查和定位问题带来一定的难度。
在出现异常的地方会直接抛出异常。例如在用户登录时,如果获取用户信息出错,则可以这样处理: throw new CustomException("获取用户信息异常”,HttpStatus. UNAUTHORIZED)。
3.4 统一封装Response——智能的响应数据 83
3.4.1 接口响应数据的模型 83
1.返回状态码模型
客户端在得到服务端响应数据后,一般根据响应状态码进行相应的处理,如果得到的状态码为200,则代表当前请求是成功的,可以直接给用户展示结果;如果得到的状态码为500,则代表服务端出现了异常,可能就给用户展示“兜底方案”。
状态码区间 | 分类描述 |
---|---|
100~199 | 服务器收到请求,需要请求者继续执行操作 |
200~299 | 请求成功,请求被成功接收且处理 |
300~399 | 重定向,需要转换到另一个地址完成请求 |
400~499 | 客户端错误,请求中的参数错误或者语法错误 |
500~599 | 服务器错误,服务端处理请求产生了异常 |
2.业务数据模型
例如,在用户登录成功后,个人中心模块获取的是用户的所有信息(姓名、电话、联系方式、图片、个人介绍等),这些用户信息就是业务数据模型。
3.4.2 【实战】开发统一的响应数据模型,以应对不同业务 87
3.5 编写高质量的异步任务 93
大部分请求和响应都是同步的,即是一个“客户端发起请求-->等待获取响应"的过程。但是会有这样一种场景: 在业务主流程完成后,需要调用另一个系统,或需要做一些辅助性的但不可或缺的逻辑操作。对于这类操作,则需要发起异步任务。
在电商平台中,在用户下单付款后,系统需要调用仓储平台进行发货,需要调用短信平台发送下单成功的短信通知,以及需要调用营销平台给用户发送优惠券等,这些都是异步任务。
3.5.1 为什么要编写异步任务 93
1.异步任务的使用场景
异步任务的优点:降低性能开销,以及为用户带来更好的体验。
3.5.2 【实战】基于Spring开发高质量的异步任务 94
3.6 DTO与PO的互相转换 98
3.6.1 什么是DTO、PO 98
3.6.2 【实战】实现DTO与PO的互相转换 99
3.7 优雅的API设计——对接“清爽”,不出错 102
3.7.1 最好采用“API先行”策略 103
API是指一些预先定义的接口(如函数、HTTP接口),或指软件系统不同组成部分衔接的约定。
"API先行”策略有以下两个好处:
- API提供者能从全局的角度把控微服务所提供服务的特性,以及业务的功能点。
- API调用者在前期可以根据API文档进行业务的开发,不必等待服务提供者完成全部的API功能。
3.7.2 API 的设计原则 104
RESTful API设计思想中有一个很好的方法在 API中利用版本号来选择具体版本。
http://xxx/v1/users 表示获取版本号为v1
3.7.3 RESTful API设计的规范 106
1.URI:URI表示资源。资源一般对应于服务器领域模型中的实体信息。URI有如下特性:
- 是地址也是资源。
- 不使用大写,使用小写。
- 使用中横杠(-),不用下横杠(_ )
- 可以携带版本号和后缀以区分功能。
- 在命名上,一般使用名词,不用动词。名词在表示资源集合时使用复数形式。
- 层级结构应清晰,用斜杠(/)来表示。
- 用问号(? )来过滤请求资源。
- 避免使用过深层级的URI,如GET /users?name=test。
2.HTTP动词
- GET (查询) :从服务器中获取单个资源或者多个资源的集合。
- POST ( 创建) :在服务器上新建单个资源。
- PUT (更新) :在服务器上更新资源,客户端提供需要更新的完整资源。
- PATCH (更新) :在服务器上更新资源,客户端提供需要更新的部分资源。
- DELETE (删除) :在服务器上删除资源。
GET /zoos:列出所有动物园。
POST /zoos:新建一个动物园。
GET /zoos/id:获取某个指定动物园的信息。
PUT /zoos/id:更新某个指定动物园的信息( 提供该动物园的全部信息)。
PATCH /zoos/id:更新某个指定动物园的信息( 提供该动物园的部分信息)。
DELETE /zoos/id:删除某个指定动物园。
GET /zoos/ID/animals:列出某个指定动物园中的所有动物。
DEL ETE /zoos/D/animals/id:删除某个指定动物园中的指定动物。
3.8 API治理——告别“接口满天飞” 108
API就是为了给使用者调用的。如果使用者在调用API时觉得很麻烦,甚至不能正确调用,那么在API提供者和API使用者之间势必会产生大量的沟通成本。
3.8.1 【实战】基于Swagger构建可视化的API文档 109
如果开发人员同时维护API服务和API文档,则随着时间的推移和版本的迭代,会出现API文档更新不及时(甚至不更新)的情况。所以,如果开发者只维护自己所熟悉的API服务且API文档能自动更新,则省去了维护的麻烦,也避免了API服务与API文档不一致的情况。
Swagger框架用于生成、描述、调用和可视化RESTful API服务,其总体目标是:使客户端、文件系统与API服务以同样的速度更新。
4.Swagger API文档的页面访问
启动服务端项目,然后在浏览器中输入"htp:/localhost:8090/swagger-ui.html" 即可访问Swagger API文档的页面。
3.8.2 API调用链管理 112
第4章 快速部署上线 113
4.1 反向代理配置 113
4.1.1 什么是反向代理,为什么要使用反向代理 113
反向代理具有以下特征:
- 客户端在获取处理结果后,以为处理结果来源于代理服务器,并不知道是来源于目标服务器。
反向代理的作用。
- 隐藏服务器的内部结构:使用反向代理可以对客户端隐藏服务器的详细信息,如真实的IP地址,从而加大了外界恶意破坏服务器的难度,起到保护服务器的作用。
- 负载均衡:通过配置合适的负载均衡算法,依据服务器的真实负载情况,将客户端流量分发到不同的服务器上。
3.正向代理服务器和反向代理服务器的区别
(1)服务对象不同。
- 正向代理服务器:代理的是客户端,帮助客户端访问其无法访问的服务器资源。
- 反向代理服务器:代理的是服务端,帮助服务端实现安全防护、网络加速和负载均衡等功能。
(3)所处网络模型不同。
- 正向代理服务器:和客户端在同一个网络中。例如,使用者在自己的机器上安装一个代理软件,通过这个代理软件来代理自己的请求。
- 反向代理服务器:和目标服务器在同一个网络中,一般在服务器上架设。例如,在目标服务器集群中部署一个反向代理服务器, 用来代理目标服务器。
(4)作用不同。
- 正向代理服务器:主要用来解决客户端访问限制的问题。
- 反向代理服务器:主要用来提供负载均衡、安全防护等功能。
4.1.2 【实战】使用Nginx配置线上服务 116
4.2 系统性能测试 120
4.2.1 【实战】进行单元测试 120
4.Mock的使用
在开发一个对象时,经常会依赖其他的对象,而被依赖的对象又有自己的依赖对象。
在对A对象进行单元测试时,其实更希望测试的是"A对象本身是否满足需求”。于是对于其依赖的对象只需要模拟出结果即可。这样在编写测试用例的过程中,降低了依赖所带来的各种开发上的复杂度。这种隔离依赖的方式将我们无须关心的依赖Mock (模拟)掉了。
4.2.2 【实战】用AB工具做上线前的性能测试 131
第5章 生产环境监测 136
5.1 服务器性能日常监测 136
5.1.1 在运维中常说的“服务器平均负载”是什么意思 136
5.1.2 为什么经常被问到“CPU上下文切换” 138
5.1.3 【实战】快速分析出CPU的性能瓶颈 141
2.CPU性能分析工具
- 运行top命令工具,可以获取CPU的使用率、平均负载和“僵尸”进程等信息。
- 运行vmstat命令工具,可以获取上下文切换次数、中断次数、运行状态和不可中断状态的进程数。
vmstat 5
# vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 2938056 543272 3730904 0 0 0 1 1 1 0 0 100 0 0
vmstat5: 每隔5s采集并输出一组数据。
procs中的r:正在执行和等待CPU的任务数,如果该值超过了CPU数量,则会出现CPU性能问题。
procs中的b:等待I/O的进程数量。
system中的in:每秒中断的次数,包括时钟中断。
system中的cs:每秒上下文切换的次数,该值越小越好。
- 运行pidstat命令工具,可以获取进程用户态CPU使用率、系统内核CPU使用率、自愿上下文切换和非自愿上下文的切换情况。
pidstat -w 5
5.2 优化服务器性能 144
5.2.1 CPU性能优化方法论 144
5.2.2 定位和处理内存泄漏问题 145
5.3 Java虚拟机(JVM)的生产调优 147
5.3.1 JVM内存模型分析 147
5.3.2 Java程序是如何在JVM中运行的 151
5.3.3 JVM优化的思路 152
第3篇 专项突破
第6章 应用集群化 158
6.1 为什么要应用集群化 158
6.1.1 什么是集群服务器 158
6.1.2 采用集群服务器有什么好处 160
6.1.3 集群系统和分布式系统有什么区别 161
- 集群系统:通俗的理解是,同一个业务被部署在多个服务器上,同时对外提供服务。
- 分布式系统:通俗的理解是,同一个业务被拆分成多个子业务,分别部署在不同的机器上这些子业务协作完成一件事情, 让用户感觉像是一个系统在提供服务。
6.2 搭建应用集群 162
6.2.1 【实战】使用反向代理搭建应用集群 163
可扩展性是怎么通过负载均衡来保证的。
- 基于无状态进行水平扩展:主要基于Round Robin或Least-Connected算法分发请求。但这种扩展方式无法应对单台服务器数据量较大的场景。
- 基于URL进行分发:将不同的功能部署在不同的节点上,使用Location算法将不同功能的URL分发给不同的节点。这种扩展方式能应对单台服务器数据量较大的场景,但是需要进行代码重构。
- 基于用户信息进行扩展:主要使用Hash算法进行自动路由,例如,利用用户的基本信息(IP地址或者用户名等)选择离用户较近的CDN节点或服务集群。这种方式不用进行代码重构即可实现水平扩展,能应对单台服务器数据量很大的场景。
6.2.2 【实战】搭建Linux服务器集群 166
第7章 缓存设计 168
使用缓存方案主要有几点优势:提高访问性能,降低网络拥堵,减轻服务负载,增强可扩展性。
7.1 什么是缓存 168
在缓存中通常存储一些经常被访问的热点数据。相比于传统数据库中的数据,这些数据的量要少很多。采用缓存来存储数据,可以减少网络流量、降低网络拥堵。同时,由于减少了解析和计算,使得调用方和存储服务的负载可以大幅降低。
7.1.1 缓存的定义 168
7.1.2 缓存的常见分类 169
(1)客户端缓存。
客户端缓存主要包括HTTP缓存及浏览器缓存。
(2) CDN缓存。
它也是将资源文件放入CDN服务器中,可以快速响应用户。
(3)反向代理缓存。
Nginx 可以用作反向代理。也可以在此基础上增加缓存层,代表技术有
Squid和Varnish。
2.远程缓存
远程缓存是指将缓存组件独立部署,应用程序可以远程获取缓存内容。这样可以避免本地缓存的相关缺陷(如进程重启后缓存内容丢失、内存容量不足等)。其中,远程缓存组件以Memcached和Redis为代表。
3.内存型缓存
内存型缓存是指将所需的数据存储在内存中。这样的读写性能很高。但缺点是:在缓存系统重启或者崩溃(Crash )后内存中的数据会丢失。
4.持久化型缓存
持久化型缓存是指将所需的数据存储在SSD或Fusion-io等固态技术介质中。这种缓存容量比内存型缓存容量大很多,同时,数据会被持久化存储,不会造成数据丢失,但是其读写性能比内存型慢1~2个数量级。
7.2 使用缓存 171
7.2.1 如何正确选择缓存的读写策略 171
1.Cache Aside Pattern
Cache Aside Pattern即旁路缓存策略
- 读策略:在读取时先读取缓存,如果命中,则直接返回;如果未命中,则访问数据库,然后回填到缓存中。
- 写策略:先更新数据库,然后删除缓存中的数据。
2.“先更新数据库, 再更新缓存"行不行
现在有这样一个场景:在电商平台中下单扣减库存时,本来商品A的库存为100,现在一个扣减的“请求1” 过来了,更新数据库库存为99.在准备更新缓存时,另一个扣减库存的“请求2”过来了,更新库存为98,并立刻更新缓存为98.接着“请求1" 更新缓存的动作完成了,缓存中的库存变为99.那么后面读请求得到的缓存中的库存都是99,而数据库中的库存是98,这样就造成了缓存数据与数据库数据不一致。
3.“先删除缓存中的数据, 再更新数据库"行不行
(1)先删除缓存中的库存数据,然后更新数据库中的库存为99,但在更新尚未完成时来了一个“读”请求,发现缓存中没有数据,它就去数据库中查询,查到是100, 接着就回填到缓存中。
(2)之前那个更新数据库的动作完成了,数据库中的数据变成了99,这时缓存中的数据是100。
所以,不能“先删除缓存中的数据,再更新数据库”,这样会造成数据的不一致。
7.2.2 如何使用多级缓存来提升服务性能 173
用户从发起请求到成功获取数据,其间经过了客户端缓存、CDN缓存、反向代理缓存、远程缓存、应用程序缓存及数据库。
7.2.3 多级缓存之痛 179
1.热点缓存问题
接下来再来想一下,虽然在当前缓存集群中出现了宕机的节点,但是其他正常的节点依然可以接受请求。那么,50 万条请求就又会到其他正常的缓存节点中获取缓存key 。相信读者现在已经很清楚接下来会发生什么事情了。显然,另台缓存节点也会因不堪重负而宕机,最终整个缓存集群都不可用了。
之所以会出现热点缓存节点不可用,乃至最终整个缓存集群不可用,主要是因为我们无法准确探测热点key。探测热点key有以下两种方案。
- 离线计算: 不停地分析历史数据,或者针对业务场景预测热点数据,然后将预测出来的热点数据放进缓存中。
- 实时计算:由于每秒的请求量过大,所以可采用大数据领域的流式计算技术来实时统计数据的访问次数。
2.数组一致性问题
“如何更新缓存才能保证数据库和各层缓存的数据一致性”。
对于单层缓存系统,可以采用“先删除缓存中的数据,然后更新数据库”的方案来解决数据一致性问题。
如果对实时性要求不是很高,则可以采用“全量同步+增量同步”的方式进行:
(1)按照预计的热点key,对系统进行缓存预热,即全量同步数据到缓存系统。
(2)在需要更新缓存时,采用增量同步的方式更新缓存。
3.缓存淘汰问题
缓存数据达到最大缓存大小时的淘汰策略:
- 使用FIFO算法来淘汰过期key.
- 使用LFU算法来淘汰过期key。
- 使用LRU算法来淘汰过期key。
如果没有达到最大缓存大小的淘汰策略:
- 定时删除策略:设置-个定时任务, 在规定时间内检查并删除过期key.
- 定期删除策略:设置删除的周期及时长(需要根据具体场台来计算)。
- 惰性删除策略:在使用时检查key是否过期。如果过期了,则更新缓存,否则直接返回。
7.3 缓存架构设计 183
7.3.1 缓存组件的选择 183
Redis集群管理主要有以下3种方式。
- Client分片访问:在进行缓存操作时,Client 对key进行Hash计算,然后依据取模的结果选择Redis的实例。
- 使用代理:在Redis前面增加一层proxy (代理),把路由策略、后端的Redis 状态维护工作都放到proxy 中进行,client 直接访问proxy.若后端Redis 需要变更,则只需要修改proxy的配置即可。
- 使用Redis 集群:从Redis 3.0开始引入了插槽(slot),Redis 集群中一共有16 384个插槽,每个key通过CRC16算法校验后取模,决定存放在哪个插槽中。
3.Redis 和Memcached的区别
- 在存储方式 上: Memcached 将所有数据都放在内存中,重启后数据会丢失,并且数据不能超过内存大小; Redis 将部分数据保存在硬盘上,这样能实现数据的持久性。
- 在数据支持类型上: Memcached 只支持简单的key-value数据类型; Redis 不仅支持key-value数据类型,还支持List、Hash 等数据类型。
- 在底层模型使用上:两者的底层实现方式及与客户端通信的应用协议不一样。Redis 自己构建了VM机制,因为般的系统调用函数会浪费定的时间去进行移动和请求。
- 在value大小上: Redis可以达到1GB,而Memcached只有1MB.
7.3.2 缓存数据结构的设计 187
在确定好缓存组件后,接下来就是依据业务访问的特点设计缓存数据结构。例如,对于可以直接通过key-value读写的业务,可以将这些业务数据封装为String、 JSON、Protocol Buffer等格式,并序列化成字节序列,然后将字节序列写入缓存中。
在读取时,首先从缓存中获取数据的字节序列,然后进行反序列化操作得到原始数据。
对于只需要存取部分字段或需要在缓存中进行计算的业务,可以把数据设计为Hash. Set、List、Geo等结构,存储到支持复杂集合数据类型的缓存(如Redis、Pika 等)中。
7.3.3 缓存分布的设计 187
接下来就需要设计缓存的分布了。主要从以下3个方面考虑。
1.分布算法
- 取模算法。
- 一致性Hash算法。
首先对这个缓存key做Hash算法计算得到Hash值,接着对Hash值进行取模得到最终的缓存节点。
一致性Hash算法可以解决取模算法中“在增加或者减少缓存节点时命中率下降"的问题。其原理如下:
- 在算法中,将整个Hash值空间组成一个虚拟的圆环(即由$2^{32}$个点组成的Hash环)。
- 使用缓存节点的IP地址或主机名进行Hash计算,然后将计算得到的结果对$2^{32}$取模:hash(ip) % 2^32。
- 取模的最终结果肯定是0到($2^{32}$-1)之间的一个整数,即在Hash环上会有一个数与该整数对应,那么各个节点就映射在Hash环上了。
- 当需要缓存对象时,先对这个对象的key采用同样的Hash计算将其映射到Hash环上。然后在Hash环上按顺时针查找,找到的第1个节点就可以作为缓存节点。
7.3.4 缓存架构部署 190
7.3.5 缓存架构设计的关键点 190
3.Key的数量
如果key的数量很大,则不能将缓存当作DB来使用,可以将热点数据放进缓存中,对于不会被频繁访问的数据直接去DB中读取。
4.读写峰值
对于读写峰值小于10万级别的QPS,可以使用独立的缓存池。但是,当数据的读写峰值超过10万甚至达到100万级别时,则需要对数据进行分层处理,如采用多级缓存方案。
5.命中率
缓存命中率直接影响着系统的整体性能:命中率越高,则系统整体性能越高。
7.4 用Redis构建生产级高性能分布式缓存 192
7.4.1 Redis的常见数据类型 192
6.bitmap(位图)
可以用setbit、bitfield 指令对bitmap 中每个bit进行置0或置1操作,也可以用bitcount
指令来统计bitmap 中被置1的bit数,还可以用bitop 指令对多个bitmap进行“求与”“求
或”“异或” 等操作。
7.geo(地理位置)
在LBS应用(如微信中的附近的人,美团、饿了么中的附近的美食,滴滴打车中的附近的车等)中,需要使用地理位置信息(经纬度)进行搜索。
在存储某个位置点时,Redis首先利用Geohash算法将该位置二维的经纬度映射编码成一维的52位整数值,将位置名称、经纬度编码score作为键值对存储到分类key对应的sortedset 中。
例如,要查找位置A附近的人,则过程如下。
(1)以位置A为中心点,以一定距离(如1km)作为半径,通过Geohash算法算出8个方位的范围。
(2)依次轮询方位范围内的所有位置点,只要某个位置点到位置A的距离在要求的范围内,
则它就是目标位置点。
(3)在轮询完所有范围内的位置点后重新排序,这样即得到位置A附近的所有目标。
8.hyperLogLog(基数统计)
hyperLogLog的特点:在统计过程中不记录独立元素,占用内存非常少,非常适合统计海量数据。在大中型系统中,统计每日、每月的UV (独立访客数),或者统计海量用户搜索的独立词条数,都可以使用hyperLogLog 类型。
什么是基数?比如数据集{1,3,5,7,5,7,8}, 那么这个数据集的基数集为{1,3,5,7,8},基数(不重复元素的个数)为5。基数估计就是在可接受的误差范围内快速计算基数。
7.4.2 【实战】通过Redis的读写分离扛住10万以上的QPS 197
7.4.3 【实战】在高并发场景下,缓存“雪崩”了该怎么办 201
3.缓存“雪崩"的解决方案
合理有效的预防,能减小发生缓存“雪崩”的概率。可以从以下3个关键点来预防。
(1)对DB访问增加读开关。
(2)给缓存系统增加多个副本。
(3)对缓存系统进行实时监控。
7.4.4 【实战】在高并发场景下,缓存“穿透”了该怎么办 204
用户访问一个不存在的key,“穿透” 到数据库中,数据库返回空值,并不会回填到缓存中,
那么后续不管查询这个key多少次,都会缓存命中失败,直接“穿透"到数据库中。这样会严重影响数据库的性能。
1.解决方案
(1)回种空值。在访问不存在的数据时,虽然第一次“穿透”到数据库获取的是空值(NULL),但可以用这个空值(NULL)回填缓存(称为“回种空值”)。
回种空值这种方案,会阻挡很大一部分的 “穿透”的请求。但是如果大批量地访问不存在的key,则这种方案会造成缓存容量的紧张,甚至占满内存空间。
(2)使用布隆过滤器。
布隆过滤器用来检测个元素是否在一 个集合中。这种算法由一 个二进制数组加上一 个Hash算法组成。其基本原理如下:
算法组成。共基个原建如下:
- 分配一块内存空间存储bit数组,数组的bit位初始值全部为0,这个bit数组就表示一个集合。
- 在添加元素时,按照提供的Hash算法计算出对应的Hash值。
- 将计算得到的Hash值对数组长度进行取模,得到需要计入数组的索引值。
- 将需要计入数组的索引值所在位置上的0改为1。
- 要判断一个元素是否在某个集合中,则需要采用与添加元素相同的Hash算法算出数组的索引值,如果索引值位置上的值为1则代表它在集合中。
布隆过滤器有可能会产生误判(比如将集合中的元素判定为不在集合中),但概率很小。另外,它不支持删除元素。
7.4.5 【实战】构建一个高性能、可扩展的Redis集群 206
4.客户端访问Redis集群
(2) smart客户端。
redis-cli这类客户端被称为Dummy客户端,因为它们在执行命令前不知道数据在哪个节点,需要借助MOVED错误进行重新定向。
与Dummy客户端相对应的是Smart客户端。
7.4.6 【实战】实现朋友圈的“点赞”功能 210
7.4.7 【实战】实现App中的“查找附近的人”功能 212
第8章 存储系统设计 213
8.1 池化技术 213
8.1.1 数据库连接池是如何预分配连接的 213
8.1.2 线程池是如何工作的 214
创建线程会产生一定的系统开销, 且每个线程会占用系统一定的内存资源。在高井发场景下,创建太多的线程会给系统稳定性带来危害。
在一个系统中不能无限制地创建线程。另外,在线程执行完后还需要将其回收,大量的线程回收会给垃圾回收带来很大的压力。
8.1.3 协程池有什么作用 216
在Linux内核中,以进程为单元来调度资源。线程是轻量级进程。即进程、线程都是由内核创建并调度的。
而协程是由应用程序创建出来的任务执行单元,比如Go语言中的协程"goroutine"。协程
运行在线程上,由应用程序调度,是比线程更轻量的执行单元。
在高井发场景下,线程的创建和销毁都会给CPU和内存带来一定的性能开销。 所以,协程就诞生了。
1.协程的作用
在Go语言中,一个协程的初始内存空间是2KB.相比线程和进程来说,它小很多。协程有
如下几个好处:
- 协程的创建和销毁完全是在用户态下执行的,不涉及用户态和内核态的切换。
- 协程完全由应用程序在用户态下调用,不涉及内核态的上下文切换。
- 在切换协程时不需要处理线程状态, 所以需要保存的上下文也很少,速度很快。
8.2 数据库采用主从架构——数据再也不会丢了 217
8.2.1 什么是数据库的主从架构 217
8.2.2 【实战】配置MySQL主从架构 219
8.2.3 主从架构中的数据是如何实现同步的 221
8.3 数据库读写分离——读/写数据再也不用争抢了 222
8.3.1 数据库读写分离能解决什么问题 223
8.3.2 数据库读写分离造成数据不一致,该怎么办 223
针对这种读写分离带来的数据不一致问题, 是没有绝对的技术手段可以解决的。通常都是在产品端采用中和的方案来解决。例如在订单支付场景下,在支付成功后不是直接跳转到订单页,而是跳转到一个“支付成功”的页面,这样可以让用户对于主从延迟无感知。
所以,对于那些“在数据更新后需要立刻查询到更新后的数据”的场景,可以采用事务来处理。例如,可以将“购物车更新”及“重新计算价格”放在同一个事务中:在数据更新之后查询主库以获取数据。这样就可以规避主从数据不一致的问题。
对于主从延迟带来的主从数据不一致的问题,并没有一种简单、高效且通用的方案通常的解决方案是:利用业务的特点来设计业务场景,尽量规避“在更新数据后立即去从库查询刚刚更断的数据”的情况。
8.3.3 【实战】在程序开发中实现读写分离 224
8.4 数据库分库分表——处理海量数据的“终极大招” 226
8.4.1 在什么情况下需要分库分表,如何分 226
1.分库分表的思考
(1)如果单张表的数据达到了“千万条”或“亿条”级别,那此时对数据库本身进行优化基本不会提升多少性能。
(2)数据量的飞速增长,使得磁盘空间特别紧张,此时数据的备份和恢复耗时增加不少。
所以,最有效的解决方法是“分片” ——对数据进行分片存储。 这样可以将海量数据分摊到各个数据库中,分摊数据库的“写”请求流量。
2.如何分库分表
分库分表的核心思想是:尽可能将数据平均地分散到多个数据库节点或者多张表中。分库分表能解决3个关键问题:
- 提升数据的查询性能,因为所有的节点都只存储了部分数据。
- 突破单机的存储瓶颈。
- 提升并发的写性能。因为数据被分摊到多个数据节点上了,所以数据的写请求从“单一主库的写入”变为“多个分片的写入”。
分库分表有两种常见的方案:
(1)垂直拆分。
垂直拆分是指,将-个数据库中的表拆分到多个数据库中,或者将一张表拆分成多张表。
通常按照业务模型来进行垂直拆分,即将相似的表或业务耦合度较高的表拆分到独立的库中。这就是所谓的“专库专用”原则。例如图书馆的图书管理,可以将不同题材类型的图书放在不同的书架上。
(2)水平拆分。
水平拆分是指,将单张数据表的数据按照某种算法规则拆分到多个数据库和数据表中。
水平拆分和垂直拆分的关注点不同:垂直拆分更关注业务类型,即将不同类型的业务拆分到不同的库中;水平拆分则更关注数据本身的特点。
- 按区间维度拆分。按区间维度拆分常用的是按时间段进行拆分。
- 按分区键进行Hash算法拆分。这种拆分方式比较适合实体表,例如将用户表按照用户ID字段进行拆分。
8.4.2 【实战】在分库分表后,如何处理主键ID 229
在单库单表场景下,有时会利用数据库的自增策略来创建主键ID。但在数据库分库分表后,数据被分到多个库的多个表中了,如果还使用主键自增策略,则在将两个数据插入两个不同的表后可能会出现相同的主键ID.此时该怎么办呢?
1.UUID
UUID (Universally Unique ldentifer, 通用唯一识别码 )的目的是,让分布式系统中的所有元素都有唯一的标识信息。
在业务中,如果对于全局唯一ID 没有特殊要求,或不需要遵循某种规律,则通常使用UUID来生成全局唯一ID。但是,如果业务中需要用全局唯一ID来进行查询,且该ID最好是单调递增的,那使用UUID就不太合适了。
全局唯一ID单调递增有以下好处。
(1)可以做排序。
(2)可以提升数据写入的速度。
在每次插入数据时,都是在递增排序后面按将数据追加到最后面。如果采用无序的全局唯一ID,则在每次插入数据前还得查找它应该在的位置,这无疑增增加了数据移动的开销。
2.Snowflake 算法的原理
Snowflake编码由64bit二进制数组成。
含义说明如下:
- 第1个bit默认不使用。
- 41位时间戳,可以支撑($2^{41}$/1000/60/60/24/365)年,即约70年。
- 10位机器ID可以被划分为两部分: 1) 2位或3位的IDC号可以支撑4-8个IDC机房;2) 7位或8位的机器ID可以支撑128-256台机器。
- 12位的序列号代表每个节点每毫秒可以生成4096个ID。
8.4.3 【实战】在程序开发中支持分库分表 232
8.4.4 分库分表会带来什么开发难题 233
分库分表的最大问题是:需要选择一个合适的字段或属 性作为分库分表的依据。这个字段一般被叫作分区键。
8.4.5 【实战】在分库分表后实行项目无感上线 234
8.5 引入NoSQL数据库 236
即使分了再多的库和再多的表,单表数据还是会达到“千万”级别或者“亿”级别(类比excel表单张主题的表很大),数据量还是会遇到瓶颈。这种问题是关系型数据库很难解决的,因为关系型数据库的扩展性是很弱的。
此时就可以采用NoSQL数据库,因为它有着天生的分布式能力,能够提供优秀的读写性能。
8.5.1 NoSQL数据库是什么,它和SQL数据库有什么区别 236
NoSQL数据库和SQL数据库的最主要的区别有:
- NOSQL数据库弥补了SQL数据库性能上的不足。
- NOSQL数据库适合互联网业务常见的大数据量场景。
- NoSQL数据库不支持SQL,所以没有强大的查询功能。
- NoSQL数据库没有强大的事务特性。
8.5.2 常用的NoSQL数据库 237
8.5.3 利用NoSQL数据库可以提升写入性能 237
NoSQL数据库常采用基于LSM树(Log Structured Merge Tree )的存储引擎。
LSM树的核心思想是:在写入数据时,会先写入内存的active MemTable中; active MemTable用于保存最近更新的数据,其中的数据是按照key排序的。
由于数据是暂时保存在内存中的,而内存不是可靠性高的存储设备,所以,为了防止内存中的数据因为断电或机器故障而丢失,通常会通过WAL ( Write-ahead logging,预写式日志)的方式将数据备份在磁盘上,以保证数据的可靠性。
active MemTable文件在达到一定大小(full)后,会被转化成immutable MemTable文件。immutable MemTable是将active MemTable文件转变为SSTable文件的一种中间状态。
当SSTable文件达到定数量后, 会将其进行合并,以减少文件的数量。SSTable文件是一个有序键值对的集合,所以合并速度非常快。
如果需要从LSM树中读取数据,则先从内存的active MemTable文件中查找;如果没有找到,再从SSTable文件中查找。因为磁盘上存储的数据都是有序的,所以查找效率是很高的。
8.5.4 利用NoSQL数据库可以提升扩展性 238
第9章 搜索引擎——让查询更便捷 240
9.1 为什么需要搜索引擎 240
在电商网站的商品列表页中,通常会有按照商品名称进行模糊搜索的需求,而且要求延时低、响应迅速。
对于这种搜索场景,最简单的做法是写一条SQL语句进行模糊查询,例如:
Select * from t_bas_product where name like %***%
9.2 搜索引擎的通用算法和架构 241
从搜索信息的对象来看,搜索引擎有如下几类。
- 全文搜索引擎:对网页的文字、图片、视频和链接等内容进行搜索,如百度、Google 等。
- 垂直搜索引擎:对网站垂直领域进行搜集和处理,如在商旅网站中对机票、旅行信息等进行搜索的搜索引擎。
- 元数据搜索引擎:对数据的数据进行搜索和处理,如文章中有多少字数、文件的大小等。可将其看作是将多种搜索引擎的数据进行整合后再提供给用户的搜索引擎。
9.2.1 必须知道的倒排索引 241
在搜索引擎中,每个文档都有一个ID,文档的内容是所有关键词的集合。搜索引擎能够实现快速查找的核心就是利用索引:通过用户输入的关键词查找匹配的索引,之后通过索引构建结果页面。
1.正排索引
正排索引根据文档内容构建出一个“文档ID→关键词列表"的关系。
正排索引是按照key去寻找具体的value。如果用户在搜索页面内搜索一个关键词(如xxx手机),则先扫描索引库中的所有文档,找出包含"xxx 手机”关键词的所有文档,
然后利用打分算法对文档打分,最后依据打分排名将结果展示给用户。
所以,在搜索引擎中采取倒排索引的方式来构建索引库,即把“文档ID到关键词的映射”转换为“关键词到文档ID的映射”,每个关键词都对应着一个文档ID列表。
2.倒排索引
先利用“分词器”将商品名称进行简单分词,然后建立分词与DOCID对应关系。
分词就是把一段连续的文本按照语义拆分成多个单词。
9.2.2 互联网搜索引擎的技术架构 243
第一部分,发生在用户搜索前
搜索引擎在启动后会进行各种数据的抓取、清洗及解析等工作(即提前处理好绝大部分数据)。
(1)使用爬虫技术抓取网络中的网页并下载到本地(这就是原始文档)。
(2)用去重模块对下载的网页进行去重,确保每个网页都包含独一无二的内容。
(3)用解析模块对去重后的网页进行解析(即抽取网页的内容和链接),用算法对抓取的网页进行解析,构建倒排索引表,并进行相关的操作;最终搭建出一个链接关系。
(4)对已经完成的倒排索引表及链接关系等进行反作弊处理,例如,剔除违法犯罪内容、删除坏网页等。
第二部分,发生在用户搜索过程中
(1)搜索引擎接收用户的搜索关键词,并进行查询分析。
(2)搜索引擎在缓存( Cache )系统中搜索是否有与用户搜索关键词匹配的内容。
9.2.3 Lucene与Elasticsearch的前世今生 245
在早期出现的一些开源的搜索引擎中最受欢迎的就是Lucene.
后来,开发者基于Lucene构建了一套功能强大的搜索平台—— Elasticsearch。
Elasticsearch是一个开源的、分布式且采用REST风格的搜索平台。
Elasticsearch相比Lucene具备如下优势。
(1)接近实时。
Elasticsearch是一个接近实时的搜索平台,主要体现在两个方面:
- 从索引一个文档到这个文档能够被搜索到只有很小的延时。
- 基于Elasticsearch执行搜索和分析可以达到“秒”级。
(4)文档(Document)。
文档是Elasticsearch的最小数据单元。一个文档可以是一条商品数据,也可以是一个订单数据,通常是以JSON结构来表示。
(5)索引(Index )。
索引主要用来存储Elasticsearch的数据。索引包含一堆相似结构的文档数据, 例如商品索引。一个索引包含很多文档(相似或者相同的文档)。
(6)文档类型(Type)。
文档类型(Type )用来规定文档中字段内容的数据类型和其他的一些约束,相当于关系型数据库中的表。一个索引(Index)可以有多个文档类型(Type)。
9.3 用Elasticsearch搭建高性能的分布式搜索引擎 247
9.3.1 Elasticsearch分布式架构的原理 247
Elasticsearch用于构建高可用和可扩展的系统。扩展的方式,可以是购买更好的服务器(纵向
扩展),也可以是购买更多的服务器(横向扩展)。
9.3.2 【实战】将Elasticsearch应用在电商系统中 250
虽然Elasticsearch主要用于搜索,但其本质还是一个存储系统。Elasticsearch和关系型数据库进行对比。
Elasticsearch | 关系型数据库 |
---|---|
Index | 表 |
Document | 行 |
Field | 列 |
Mapping | 表结构 |
9.3.3 【实战】快速实现Elasticsearch的搜索建议 253
9.3.4 【实战】在海量数据下,提高Elasticsearch的查询效率 254
第10章 消息中间件设计——解耦业务系统与核心系统 257
10.1 同步和异步 257
同步与异步指的是消息通信机制。
- 同步:在发出一个调用后没有得到结果之前,该调用不返回,即调用者主动等待这个调用的结果。
- 异步:与同步相反,在调用发出后直接返回结果。即在一个异步调用发出后, 调用者立刻返回去做其他事情。在调用结果发出之后,被调用者通过“状态”通知” “回调” 这3种方式通知调用者。使用哪一种方式,依赖于被调用者的业务实现,一般不受调用者控制。
- 如果被调用者使用“状态”来通知,那么调用者需要每隔一定时间检查一次,效率很低。
- .如果被调用者使用“通知”或“回调”来通知,则效率很高,因为被调用者几乎不需要做额外的工作。
10.1.1 何为同步/异步 257
10.1.2 【实战】使用回调函数获取数据 258
10.2 为何要使用消息中间件 260
10.2.1 什么是消息中间件,它有什么作用 260
1.什么是消息中间件
消息中间件(MQ)其实就是一个开发好的系统, 可以被独立部署。业务系统通过它来发消息和收消息,以达到异步调用的效果。
消息中间件可以被看作一个用来暂时存储数据的容器。 它还是平衡低速系统和高速系统处理任务时间差的工具。
10.2.2 生产级消息中间件的选型 263
(1)Kafka
Kafka消息中间件有如下优点:
- 支持高吞吐量。在“4 CPU +8 GB内存”配置下,一台机器可以扛住十几万的QPS,这是相当优秀的。
- 性能较高。发送消息基本都是“毫秒”级别的。
- 支持集群部署。部分机器宕机不会影响Kafka集群的正常使用。
其主要缺点如下:
- 有可能丢失数据。因为,它收到消息后并不是直接写入物理磁盘,而是先写入磁盘缓冲区中。
- 功能比较的单一。主要支持收/发消息,适用场景受限。
业界一般用Kafka来进行用户行为日志的采集和传输,因为在这种场景下可以接受数据的丢失且对吞吐量的要求极高。
10.2.3 在高并发场景下如何处理请求 264
2.页面缓存( PageCache )
为了保证高效的写性能,Kafka采用基于操作系统的页面缓存( PageCache )来实现文件的写入。
在向磁盘写入文件时,可以直接写入PageCache中,即写入内存中。接下来,由操作系统自
己决定什么时候把PageCache中的数据真正写入磁盘。
3.零拷贝(SendFile )
传统的网络1/O 过程如下。
( 1)操作系统把数据从磁盘中读到内核区的Read Buffer中。
(2)用户进程把数据从内核区的Read Buffer中复制到用户区的Application Buffer中。
(3)用户区的Application Buffer把数据写入Socket通道,数据被复制到内核区的Socket Buffer中。
(4)操作系统把数据从内核区的Socket Buffer发送到网卡中。
同一份数据在操作系统内核区的Read Buffer与用户区的Application Buffer之间需要复制两次。为了进行这两次复制,发生了好几次上下文切换(内核区用户区切换),一会儿是应用程序在执行,一会是操作系统在执行。所以,用这种方式读取数据是比较消耗性能的。
为了解决这个问题,Kafka在读取数据时使用了零拷贝技术:直接把数据从内核区的Read Buffer复制到Socket Buffer中,然后发送到网卡中。这样避免了在操作系统内核区的Read Buffer和用户区的Application Buffer之间来回复制数据的弊端。
10.3 RocketMQ在项目中的使用 267
10.3.1 RocketMQ架构原理 267
10.3.2 【实战】利用RocketMQ改造订单系统,提升性能 269
10.4 引入消息中间件会带来什么问题 275
10.4.1 需要保证消息中间件的高可用 275
10.4.2 需要保证消息不被重复消费 277
消费者会在消费条消息后就将外理结果写入数据库,如果重复消费消息,则在数据库中会存在多条相同的数据。所以,避免消息坡重复消费是很重要的。
要完全避免重复消费消息是很难做到的,因为网络的抖动、机器的宕机和处理的异常都是难以避免的,在工业上目前没有成熟的方法。通常都是从业务角度出发,只要保证“即使消费者重复消费了消息,重复消费的最终结果和只消费一次的结果是一样的”即可,即保证在生产消息过程和消费消息过程中是“幂等”的。
10.4.3 需要保证消息的顺序性 279
10.4.4 需要解决消息中间件中的消息延迟 280
第11章 微服务设计——将系统拆分 282
11.1 好好的系统为什么要拆分 282
随着业务的增加,很多业务需要对一些服务进行复用。此时可以对系统进行拆分,让系统变得低耦合,让服务变得更可复用,从而提升整个系统的处理能力。
单体系统主要面临以下问题:
- 代码分支管理困难。
- 编译和部署困难。
- 数据库连接易耗尽。
- 服务复用困难。
- 新增业务困难。
- 发布困难。
- 团队协作开发成本高。
11.2 如何拆分服务 283
11.2.1 不可忽略的SOA架构 283
在进入信息化浪潮后,很多传统企业开始采购大量的信息化系统,例如CRM、ERP、OA系统等。这些系统基本都不是同一个供应商所提供的,运行段时间后会逐渐形成信息孤岛。
SOA架构用服务拆分的思想来解决企业内部大量异构系统集成的问题。其解决思路如下。
(1)将系统所需要的能力封装成一个个的独 立接口或打包成独立的服务系统。
(2)外部系统通过这些独立接口或服务系统访问内部系统,达到异构系统互通的目的。
11.2.2 如何对已有系统进行微服务改造 284
11.2.3 微服务拆分的方式 287
1.从业务维度拆分
从业务维度拆分是指按照业务关联程度来拆分:将关联较紧密的业务放在一起,
2.从公用维度拆分
这种拆分方式需要提炼出系统中公用的业务模块。这部分模块依赖的资源相对独立,不与其他业务模块耦合。
11.2.4 有哪些好用的微服务开发框架 288
11.3 微服务设计参照模型 290
11.3.1 在开发中如何定义软件分层 290
一个设计良好的应用会不断被拆分出新的应用,从而形成这样一种情况: 应用之间相互依赖,边界开始越来越模糊,彼此间互相调用关系越来越复杂。应用就会瞬间膨胀起来,应用耦合性太高,从而造成长链条依赖和循环依赖问题。
1.架构分层设计
通用的架构分为5层
- 网关层:统一对外提供HTTP接口服务,主要用于实现统一的鉴权、 流控及降级等功能。
- 业务聚合层:进行所有业务的逻辑处理。它依赖中心服务, 通过对中心服务的编排实现业务场景的整合开发。它还具备业务流程异常的处理、超时重试和各种状态的处理能力。
- 中心服务层:独立的原子业务功能,是功能单- -的服务, 具备“高内聚、低耦合”特性对数据库可进行读/写操作。
- 数据服务层:只提供对业务数据的“读”操作,不提供“写”操作。
- 存储层:只提供业务数据存储服务。
11.3.2 运用好“微服务的使用模式”可以事半功倍 292
常见微服务的使用模式有:
- 事件溯源。
- 命令与查询职责隔离(CQRS)。
- 断路器
- 超时。
2.命令与查询职责隔离(CQRS)
命令与查询职责隔离模式是指,在接口服务层将查询操作和命令操作隔离开来,即在服务层实现读写分离。
11.4 引入微服务架构会带来什么问题及其解决方案 294
11.4.1 数据一致性问题 295
1.最终一致性方案
最终一致性方案如果SericeB处出现异常,则结果为a2、b1、c1,但是最终结果为a2、b2、c2。
实现最终一致比较流行的做法是使用消息队列, 其实现思路如下:
(1)在每个步骤完成后,生产一条消息到MQ中告知消费者接下来需要处理的数据。
(2)在消费者消费MQ中的消息完成数据处理后,生产者再生产消息并发送到MQ中。
(3)在消费者消费MQ中的消息失败后,消息会被保留,等待下次重试。
2.实时一致性
实时一致性方案如果在Step 3出现了异常,则Step 2和Step 1都需要立即回滚,将数据回滚到a1、b1。
可以使用数据库的分布式事务来确保微服务架构中数据的实时一致性。
11.4.2 分布式事务问题 296
11.4.3 复杂度问题 296
把单体架构改造成微服务架构,主要会在如下几个方面增加系统的复杂度。
2.发布/订阅服务
调用方如何能智能感知提供者的服务已经上线,并且在提供者的服务异常退出时,调用方又如何能立马感知服务已下线,这都很关键。
11.5 如何有效治理微服务 297
11.5.1 管理服务 298
在微服务架构中,服务都需要注册到注册中心中。
11.5.2 治理服务 298
11.5.3 监控服务 298
11.5.4 定位问题 299
11.5.5 查询日志 299
11.5.6 运维服务 299
第12章 API网关设计——让服务井然有序 300
12.1 为什么要引入API网关 300
12.1.1 什么是API网关 300
12.1.2 API网关的作用 301
12.2 API网关的通用设计方案 302
12.2.1 设计API网关要考虑哪些关键点 302
12.2.2 API网关的选型 304
12.3 将API网关应用到生产项目中 305
12.3.1 【实战】基于Zuul搭建生产级API网关 305
12.3.2 【实战】基于Spring Cloud Gateway搭建生产级API网关 308
第4篇 高并发项目设计及实战
第13章 高并发系统设计原则 316
13.1 高并发系统的通用设计原则 316
13.1.1 利用负载均衡分散流量 316
常用的负载均衡算法:
- 随机算法。
- 轮询算法。
- 加权轮询算法。
- 最少活跃连接算法
- 一致性Hash算法。
13.1.2 利用分布式缓存扛住“读”流量 322
13.1.3 实现数据库的读写分离 322
13.1.4 实现数据库分库分表 323
13.1.5 使用NoSQL、消息队列及搜索引擎技术 324
13.1.6 将大应用拆分为小应用 325
13.2 提升系统性能的策略 325
13.2.1 垂直伸缩 325
垂直伸缩是指提升单台服务器的处理能力。例如,用更快频率、更多核的CPU,用更大的内存,用更快的网卡,用更多的磁盘组成一台服务器, 使单台服务器的处理能力得到提升。
13.2.2 水平伸缩 326
水平伸缩是指,使用更多的服务器构成一个分布式集群, 这个集群统一对外提供服务 。水平伸缩并不会提升单机的处理能力,也不使用更昂贵的、更快的硬件。
第14章 【项目实战】搭建千万级流量“秒杀”系统 327
14.1 搭建“秒杀”系统工程 327
14.1.1 技术选型 327
14.1.2 工程搭建 329
14.2 分析“秒杀”业务 331
14.2.1 “秒杀”业务场景分析 331
3.高可用指标
- MTBF (Mean Time Between Failure,平均可用时长):一段时间中系统正常、 稳定运行的平均时长。比如,在3天内系统共出现了3次故障,每次持续1小时,那么这3天的平均可用时长是23小时。
- MTTR ( Mean Time To Repair,平均修复时长) :系统从失效到恢复正常所耗费的平均时间,比如前面提到的每次故障持续1小时。
- SLA ( Service-Level Agreement,服务等级协议) :用于评估服务可用性等级,计算公式是MTBF/(MTBF +MTTR)。一般我们所说的可用性高于99.99%,是指SLA高于99.99%。
14.2.2 “秒杀”痛点分析 334
14.3 具体设计与开发 335
14.3.1 数据库层的设计与开发 335
14.3.2 业务服务层的设计与开发 335
其中,用户密码采用的是两次MD5加密:
(1)将用户输入的密码和固定的salt 通过MD5算法加密,得到第1次加密后的密码。
(2)将第1次加密后的密码和随机生成的salt, 通过MD5算法进行第2次加密。
(3)将第2次加密后的密码和第1次固定的salt 一起存进数据库中。
这样操作主要有两个好处:
第1次加密,可以防止用户明文密码在网络进行传输。
第2次加密,可以防止数据库被盗后通过MD5工具反推出密码,起到双重保险的作用。
14.3.3 动静分离的实现 339
14.3.4 优化系统以应对千万级流量 340
4.优化处理
使用RateLimiter类来实现限流。Ratelimiter是Guava提供的、基于令牌桶算法的限流实现类,它通过调整生成Token的速率来限制用户频繁访问“秒杀”页面。
令牌桶算法的原理是:系统以一个恒定的速度往桶里放入令牌;如果请求需要被处理,则先从桶里获取一个令牌;如果在当前桶中没有令牌可取,则拒绝请求。
第15章 【项目实战】搭建C2C二手电商平台的社会化治理系统 342
15.1 搭建系统工程 342
15.1.1 技术栈列表 342
15.1.2 工程搭建 343
15.2 分析系统业务 343
15.2.1 C2C二手电商平台社会化治理系统的业务介绍 343
15.2.2 C2C二手电商平台社会化治理系统的痛点分析 343
15.3 整体架构设计 344
15.3.1 整体架构图 344
15.3.2 场景分析 345
15.4 微服务设计开发 345
15.4.1 服务拆分及高可用注册中心搭建 345
CAP理论是指,一个分布式系统不可能同时满足一致性(C: Consistency) 、可用性(A: Availability )和分区容错性(P: Partition Tolerance )这3个需求,最多只能同时满足其中两项。p是必须要保留的,所以需要在C和A之间进行取舍。
15.4.2 服务间通信框架选择 349
15.4.3 平台服务开发 349
15.5 服务治理开发 351
15.5.1 链路追踪的设计与开发 351
15.5.2 引入分布式事务框架 352
15.5.3 平台限流熔断的设计与开发 354
15.5.4 引入API网关 357
15.5.5 基于Nacos搭建环境隔离配置中心 358
第5篇 运维监控
第16章 运维之术——告别加班 360
16.1 什么是CI/CD 360
CI/CD中的CI和CD的含义分别如下。
- “CI”指的是持续集成,它属于开发人员的自动化流程。成功的CI意味着代码的更改会定期构建、测试并合并到代码仓库中。该解决方案可以解决因过多代码分支而导致的相互冲突的问题。
- “CD”指的是持续交付或持续部署,它们有时会交叉使用。两者都事关整个流程后续阶段的自动化,但它们有时也会单独使用,用于说明自动化程度。
16.2 为什么要CI/CD 361
16.3 搭建适合自己公司的CI/CD 362
16.3.1 【实战】基于GitLab搭建代码管理平台 362
16.3.2 【实战】基于Jenkins搭建持续集成与编译平台 363
16.3.3 【实战】基于Ansible搭建自动化部署平台 366
16.4 服务器通用运维 366
16.4.1 优化硬件 366
16.4.2 分析性能瓶颈 367
16.4.3 【实战】处理服务器丢包问题 369
1.链路层
可以通过netstat 或者ethtool命令来查看网卡的丢包情况:
# netstat -i
Kernel Interface table
Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
ens3 1500 54118280 0 14 0 2234453 0 0 0 BMRU
ens9 1500 13001728 0 43 0 72957 0 0 0 BMRU
- RX-OK:在接收时,正确的数据包数。
- RX-ERR:在接收时,产生错误的数据包数。
- RX-DRP:在接收时,丢弃的数据包数。
- RX-OVR:在接收时,由于过速而丢失的数据包数。
2.网络层和传输层
# netstat -s
Ip:
Forwarding: 1
17204996 total packets received //总收包数
3 with invalid addresses
0 forwarded //转发包数
0 incoming packets discarded // 接收的丢包数
17204822 incoming packets delivered // 接收的数据包数
11090476 requests sent out //发出的数据包数
20 outgoing packets dropped
Icmp:
74 ICMP messages received//收到的ICMP包数
0 input ICMP message failed//收到的ICMP包失败数
ICMP input histogram:
destination unreachable: 42
echo requests: 28
echo replies: 4
102 ICMP messages sent
0 ICMP messages failed
ICMP output histogram:
destination unreachable: 66
echo requests: 8
echo replies: 28
IcmpMsg:
InType0: 4
InType3: 42
InType8: 28
OutType0: 28
OutType3: 66
OutType8: 8
Tcp:
6956 active connection openings
228 passive connection openings
4070 failed connection attempts // 失败连接尝试数
64 connection resets received
9 connections established
10960988 segments received
11286357 segments sent out
7455 segments retransmitted// 重传报文数
17 bad segments received// 错误报文数
4374 resets sent
Udp:
351499 packets received
66 packets to unknown port received
0 packet receive errors
16543 packets sent
0 receive buffer errors
0 send buffer errors
IgnoredMulti: 5896249
3.iptables
查看各条规则的统计信息:
# iptables -t filter -nvL
Chain INPUT (policy ACCEPT 17M packets, 31G bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 11M packets, 29G bytes)
pkts bytes target prot opt in out source destination
16.4.4 【实战】分析服务吞吐量突然下降的原因 373
第17章 监控之术——天使之眼 374
17.1 如何定义系统监控 374
17.1.1 需要监控哪些系统指标 374
17.1.2 如何采集监控指标 375
17.1.3 如何存储监控指标 375
17.2 搭建一套可靠的监控系统 375
17.2.1 【实战】基于ELK搭建集中化日志监控平台 375
- Elasticsearch:开源的分布式搜索引擎,提供搜索数据、分析数据和存储数据这3大功能。
- Logstash:用于收集、分析、过滤日志的工具,支持大量的数据获取方式,一般采用C/S架构。其Client端安装在要收集日志的主机上,Server 端负责对收到的各节点日志进行过滤、修改等,再将它们一并发往Elasticsearch中。
- Kibana:为Logstash和Elasticsearch提供友好的Web界面,可以汇总、分析和搜索重要的数据日志。
17.2.2 【实战】基于Prometheus搭建系统指标监控预警平台 378
17.3 链路追踪——不漏过任何一个异常服务 385
在微服务架构中,通常一次请求会涉及对多个服务的调用;而每个服务可能由专属的团队负责,分布在不同的机器上。为了保证整个系统的可用性,有必要监测每个服务的调用情况,以达到快速找到请求失败的原因。
17.3.1 什么是链路追踪 385
链路追踪来源于谷歌在2010 年发布的论文Dapper, a Large Scale Distributed Systems Tracing Infrastructure, 该论文介绍了链路追踪的核心概念。
可以将分布在系统中的所有节点通过一个全局唯一ID 串联起来,然后以可视化视图的方式展
示一个请求从进入系统到得到响应的完整过程,如图所示。
在客户端发起请求时,先在第一层生成全局的tranceld, 每一次的RPC都会将这个tranceld传出去。这样就将整个请求链路都串联起来了。
同时,在第一层会产生spanld,表示当前请求所在的位置。请求到达服务B时spanld是“0.1”,到达服务 D时spanld是“0.2”。
17.3.2 常用的开源链路追踪系统 386
目前国内常用的链路追踪系统如下。
- Zipkin:由Twitter公司基于Java 语言开发,需要修改相关配置文件(如web.xml),可以将其和Spring Cloud很方便地集成。
- CAT:由美团点评团队基于Java语言开发,需要开发人员手动进行程序埋点。
- Pinpoint:由韩国开源,主要使用字节码增强技术。其使用简单(只需要在启动时增加相应参数即可),但比较耗费性能。
- SkyWalking。由国内开源爱好者吴晟(华为开发者)开源并提交到Apache孵化器的产品,目前支持Java、.Net、Node.js等探针,数据存储方式比较丰富,如MySQL、Elasticsearch等。它支持很多框架,如Dubbo、gRPC等。
17.3.3 【实战】在微服务架构中加入链路追踪系统SkyWalking 386
SkyWalking在逻辑上分为4个部分:
- 探测器:收集数据并重新格式化,以满足SkyWaking需求(不同的探测器支持不同的来源)。
- 平台后端:支持数据聚合、分析和流式处理,包括跟踪、度量和日志。
- 存储:通过一个开放/可插拔的接口存储SkyWalking数据。可以选择现有的实现(如Elasticsearch、H2、MySQL、TiDB、 XDB),也可以选择自己实现。
- UI:将追踪的结果信息通过浏览器展示出来。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签
2016-12-30 printf对齐