第1部分 数据持久化层场景实战

1)如果一个数据被标识为冷数据,业务代码不会再对它进行写操作。 2)不会同时存在读取冷、热数据的需求。

通过定时扫描数据库的方式来触发。这个方式就是通过quartz配置一个本地定时任务,或者通过类似于xxl-job的分布式调度平台配置一个定时任务。

2026/01/01 发表想法

how

原文:在热数据库中给需要的数据添加标识:ColdFlag=WaittingForMove。这个过程使用Update语句就可以完成,每次更新大概10万条记录。

在热数据库中给需要的数据添加标识:ColdFlag=WaittingForMove。这个过程使用Update语句就可以完成,每次更新大概10万条记录。

2026/01/01 发表想法

学习笔记

原文:因为是多线程并发迁移数据,所以要确保每个线程迁移的数据都是独立分开的,不能出现多个线程迁移同一条记录的情况。其实这就是锁的一个场景。

因为是多线程并发迁移数据,所以要确保每个线程迁移的数据都是独立分开的,不能出现多个线程迁移同一条记录的情况。其实这就是锁的一个场景。

那么如何实现幂等操作?使用MySQL的Insert…On Duplicate Key Update语句即可。使用这样的操作后,当前线程的处理就不会破坏数据的一致性。

第2章 查询分离

使用Elasticsearch查询数据库时,会涉及索引、分片、主从备份,其中每个动作又细分为很多子动作,这些内容后面的场景会讲到)

1)如何使用Elasticsearch设计表结构? 2)Elasticsearch的存储结构。 3)Elasticsearch如何修改表结构? 4)Elasticsearch的准实时性。 5)Elasticsearch可能丢数据。 6)Elasticsearch分页。

表2-4 Lucene与MySQL概念对照

第2部分 缓存层场景实战

目前,Redis比Memcached更流行,这里总结一下原因,共3点。 (1)数据结构 举个例子,在使用Memcached保存List缓存对象的过程中,如果往List中增加一条数据,则首先需要读取整个List,再反序列化塞入数据,接着再序列化存储回Memcached。而对于Redis而言,这仅仅是一个Redis请求,它会直接帮助塞入数据并存储,简单快捷。 (2)持久化 对于Memcached来说,一旦系统宕机数据就会丢失。因为Memcached的设计初衷就是一个纯内存缓存。 通过Memcached的官方文档得知,1.5.18版本以后的Memcached支持Restartable Cache(可重启缓存),其实现原理是重启时CLI先发信号给守护进程,然后守护进程将内存持久化至一个文件中,系统重启时再从那个文件中恢复数据。不过,这个设计仅在正常重启情况下使用,意外情况还是不处理。 而Redis是有持久化功能的。 (3)集群 这点尤为重要。Memcached的集群设计非常简单,客户端根据Hash值直接判断存取的Memcached节点。而Redis的集群因在高可用、主从、冗余、Failover等方面都有所考虑,所以集群设计相对复杂些,属于较常规的分布式高可用架构。

设计高可用方案时,需要考虑5个要点。 1)负载均衡:是否可以通过加节点的方式来水平分担读请求压力。 2)分片:是否可以通过划分到不同节点的方式来水平分担写压力。 3)数据冗余:一个节点的数据如果失效,其他节点的数据是否可以直接承担失效节点的职责。 4)Failover:任何节点失效后,集群的职责是否可以重新分配以保障集群正常工作。 5)一致性保证:在数据冗余、Failover、分片机制的数据转移过程中,如果某个地方出了问题,能否保证所有的节点数据或节点与数据库之间数据的一致性(依靠Redis本身是不行的)。

第5章 写缓存

业务场景:如何以最小代价解决短期高频写请求

项目最终采用的方案是不让预约的请求直接插入数据库,而是先存放到性能很高的缓冲地带,以此保证洪峰期间先冲击缓冲地带,之后再从缓冲地带异步、匀速地迁移数据到数据库中。

如果多个Insert语句同时执行,它们会根据排队情况按顺序执行,也可以与Select语句并发执行。所以多个Insert语句并行执行的性能未必会比单线程Insert更快。

只需要在并发性的设计方案中保证一次仅有一个线程批量落库即可。这个逻辑比较简单,就不赘述了。

以上各个步骤失败时的应对措施见表5-1。 表5-1 批量落库失败的应对措施 

第6章 数据收集

最简单的方式是通过Logstash直接把日志文件中的数据迁移到Elasticsearch

2026/01/23 发表想法

另外,写入使用了mmap,读取时使用了sendfile 零拷贝 批量收发消息 利用磁盘的顺序读 从page cache 读取数据都是kafka吞吐量大的原因

原文:这里可以把Kafka的存储架构简单理解为,Kafka写数据时通过追加数据到文件末尾来实现顺序写,读取数据时直接从文件中读,这样做的好处是读操作不会阻塞写操作,这也是其吞吐量大的原因

这里可以把Kafka的存储架构简单理解为,Kafka写数据时通过追加数据到文件末尾来实现顺序写,读取数据时直接从文件中读,这样做的好处是读操作不会阻塞写操作,这也是其吞吐量大的原因

第7章 秒杀架构

因此设计秒杀架构时,一般需要遵循商品不能超卖、下单成功的订单数据不能丢失、服务器和数据库不能崩溃、尽量别让机器人抢走商品这4个原则。

2026/01/23 发表想法

出口带宽这种非常靠底层的资源很难被想到,如果有相应的监控可以看还好,如果没有,那就需要非常有经验的工程师才可能会想到。另一个思路,工程师大概率会看 access log,在 access log中会记录响应体的大小,对于超大响应体的请求是应该重点关注的 我也遇到过带宽问题,并发问题上程序员的确是很容易忽略带宽的问题,因为这个跟程序逻辑和部署性能都无关。

原文:唯独出口带宽有问题,它被占满了

唯独出口带宽有问题,它被占满了

静态资源尽量使用CDN(内容分发网络),如果涉及PC网站,还必须首先进行前后端分离。

使用CDN的好处是不用花费自己的服务器资源和带宽,且响应速度快。

2026/01/23 发表想法

原理:访问静态资源比如图片时,原本放到你的服务器上,自然占用你的带宽。但是有了cdn,就会把该请求转到cdn服务商的服务器上。有则返给用户;无则请求你的,然后缓存起来。 比如B站,必然得用cdn

原文:使用CDN的好处是不用花费自己的服务器资源和带宽,且响应速度快。

具体做法是将秒杀结束的标识放在Cookie中,如

2026/01/23 发表想法

直接把结束的标识放在请求头里面。便于NGINX拦截 我的理解是全部请求走后端的gateway. 然后网关直接走缓存,然后后端在秒杀结束时候只要写一次结果到缓存中就行。这样就基本上拦截了大量请求结果到后端

原文:具体做法是将秒杀结束的标识放在Cookie中,如

秒杀时间一到,它便可以通过另一个请求获取这个URL。 2)用户点击下单页面的购买按钮后,将此按钮设为Disable(不可用),防止用户不断点击它。

网关层面过滤请求

1)限定每个用户的访问频率,比如每5秒下单一次。 2)限定每个IP的访问频率。这种方式是为了避免有人通过机器人自动下单,导致错杀真实用户。 3)把一个时间段内的请求拦截掉一定比例,或者只允许特定数量的请求进入后台服务器。这里可以使用限流的漏桶或令牌桶算法,第12章将详细展开。

后台服务器过滤请求

2026/01/23 发表想法

作者在秒杀场景的经验不够多,比较泛泛而谈

原文:表7-1中整理了一份秒杀系统设计Checklist,供大家参考。

表7-1中整理了一份秒杀系统设计Checklist,供大家参考。

第3部分 基于常见组件的微服务场景实战

配置烦琐,上线容易出错 系统上线部署时,因为每次增服务、加机器、减机器时,Nginx都需要手工配置,而且每个环境都不一样,所以很容易出错。

经常需要对全系统调用库进行升级,为了保证所有服务不被遗漏,就必须有一个后台服务清单。

每个服务自动将服务和IP注册到协调服务(比如ZooKeeper)

将所有的服务都部署到容器上,然后利用Kubernetes的Service与Pod的特性进行服务注册发现

先在部署User服务的Pod上打上“User-App”标签,部署Order服务的Pod上打上“Order-App”标签。

然后启动两个Service(类似于Nginx的负载均衡),一个Service叫UserService,

③从Client发出的请求首先会到达OrderService,再自动负载均衡到某个Order服务的Pod。当Order的服务要调用User的服务时,它就会调用UserService,UserService会负载均衡到User其中的一个Pod。

每个服务会自动将服务和IP注册到协调服务(比如ZooKeeper),然后设计一个工具自动获取ZooKeeper中后台服务的机器列表,最终根据列表来自动更新Nginx的配置,更新完成后再重启。

1)每个后台服务自动把服务类型和IP注册到中心存储。 2)中心存储将服务列表推送到每个后台服务。 3)后台服务在本地做负载均衡,轮流访问同服务的不同节点。  • 图8-3 基于协调服务的服务注册发现

ZooKeeper本身为了一致性牺牲了高可用性,它同时兼作Leader、Follower和Observer这3种角色,如果Leader或半数的Follower宕机了,ZooKeeper就会进入漫长的恢复模式。而在这段时间里,ZooKeeper不接受客户端的任何请求。为什么ZooKeeper要这样设计呢?

也就是说,在微服务间的协调这个场景里面,AP比CP更合适,所以Eureka比ZooKeeper更合适。而Nacos可以通过配置来选择AP或CP,这也是推荐Nacos的原因。

第9章 全链路日志

2026/01/23 发表想法

状态压栈的时候,父子关系怎么做

原文:通过图9-3还能发现,Span中又包含了一个子Span,比如调用Product Service的过程中,Product Service会访问一次数据库(③④),这也是一个Span。因此可以得出,一个Span可以包含多个子Span,而Span与Span之间的关系就叫Reference。

通过图9-3还能发现,Span中又包含了一个子Span,比如调用Product Service的过程中,Product Service会访问一次数据库(③④),这也是一个Span。因此可以得出,一个Span可以包含多个子Span,而Span与Span之间的关系就叫Reference。

2026/01/23 发表想法

基本的思路是在 JVM 启动的时候添加一个代理(Java Agent),每个代理是一个 Jar 包,其 MANIFEST.MF 文件里指定了代理类,这个代理类包含一个 premain 方法。JVM 在类加载时候会先执行代理类的 premain 方法,再执行 Java 程序本身的 main 方法,这就是 premain 名字的来源。在 premain 方法中可以对加载前的 class 文件进行修改。 我们直接在网关实现,重点是packet方面

原文:如何以最小的业务代码侵入性引入这些功能? • 图9-4 请求日志列表示意图 • 图9-5 请求日志树状示意图 项目组希望日志数据的收集过程对写业务代码的人保持透明,因此,一种比较理想的解决方案是使用Java的探针,通过字节码加强的方式进行埋点。不过,这种方式对系统性能也会产生一定影响。

如何以最小的业务代码侵入性引入这些功能?  • 图9-4 请求日志列表示意图  • 图9-5 请求日志树状示意图 项目组希望日志数据的收集过程对写业务代码的人保持透明,因此,一种比较理想的解决方案是使用Java的探针,通过字节码加强的方式进行埋点。不过,这种方式对系统性能也会产生一定影响。

第10章 熔断

2026/01/23 发表想法

为啥不快速失败

原文:之前运维人员针对这个问题做过相关处理,考虑响应时间长,就把超时时间设置得很长,这样虽然超时报错少了,其他页面也保持正常,但是会导致客服后台查看用户信息的页面响应时间长。

1.线程隔离

2026/01/23 发表想法

线程池 并发控制

原文:1.线程隔离

下面从4个方面介绍一下Hystrix的设计思路。 1)线程隔离机制。 2)熔断机制。 3)滚动(滑动)时间窗口。 4)Hystrix调用接口的请求处理流程。

在Hystrix机制中,当前服务与其他接口存在强依赖关系,且每个依赖都有一个隔离的线程池

一般来说,当前服务依赖的一个接口响应慢时,正在运行的线程就会一直处于未释放状态,最终把所有的连接线程都卷入慢接口中。

当然,在Hystrix机制中,除了使用线程池来隔离线程,还可以使用信号量(计数器)。

仍以调用接口A为例,因并发线程的最大个数是10,在信号量隔离的机制中,Hystix并不是使用size为10的线程池,而是使用一个信号量semaphoresA来隔离,每当调用接口A时即执行semaphoresA++,调用之后执行semaphoresA-,semaphoresA一旦超过10,就不再调用

在Hystrix机制中,会配置一个不断滚动的统计时间窗口metrics.rollingStats.timeInMilliseconds

调用超时或异常的调用次数与总调用次数之比

如果熔断被触发,在circuitBreakerSleepWindowInMilliseconds的时间内,便不再对外调用接口,而是直接调用本地的一个降级方法,代码如下所示。

然后尝试使用一个请求去调用接口,如果调用成功,则恢复正常(断路器状态为CLOSED)

另外,Hystrix还有requestcaching(请求缓存)和requestcollapsing(请求合并)这两个功能

10.5.1 数据一致性 这里通过一个例子来帮助理解。假设服务A更新了数据库,在调用服务B时直接降级了,那么服务A的数据库更新是否需要回滚? 再举一个复杂点的例子,比如服务A调用了服务B,服务B调用了服务C,在服务A中成功更新了数据库并成功调用了服务B,而服务B调用服务C时降级了,直接调用了Fallback方法,此时就会出现两个问题:服务B向服务A返回成功还是失败?服务A的数据库更新是否需要回滚? 以上两个例子体现的就是数据一致性的问题。关于这个问题并没有一个固定的设计标准,只要结合具体需求进行设计即可。

第11章 限流

熔断一般发生在服务调用方,比如服务A需要调用服务B,调用几次后发现服务B出现了问题且无法再调用,此时服务A必须立即触发熔断,在一段时间内不再调用服务B。 限流一般发生在服务被调用方,且主要在网关层做限流操作。

11.2.1 固定时间窗口计数算法

11.2.2 滑动时间窗口计数算法

11.2.3 漏桶算法

11.2.4 令牌桶算法

项目组最终使用开源库Google-Guava中RateLimiter的相关类来实现限流,它是基于令牌桶算法的实现库

Tips 面试官很喜欢问熔断与限流原理相关的问题,尤其是滑动时间窗口计数。这里列举几个在高并发场景下常见的相关问题。 1)在秒杀架构中怎么保证不超卖? 2)熔断是基于什么条件触发的?这个条件的数据又是怎么收集的? 3)限流和熔断有什么不同?你了解几种限流算法?用过哪种限流算法?为什么用这个算法? 4)项目中熔断(限流)的参数在上线后调整过吗?是根据什么调整的?调整后如何观察效果?

第4部分 微服务进阶场景实战

分布式事务这个痛点对于微服务来说,简直就是地狱。

随着需求越来越多,服务之间的依赖关系就会变成千丝万缕、难以理清的架构,如图12-7所示。  • 图12-7 服务关系现实结构

12.3.8 痛点:联调的痛苦

第13章 数据一致性

2026/01/23 发表想法

XA即跨学习协议标准: 通过XA规范定义了多个资源管理器(RM)参与一个全局分布式事务的标准流程。 典型流程是两个阶段式提交,两阶段分别为prepare(预提交)和commit(正式提交)。 协调者(TM)在启用事务时从RM获取id,RM执行本地事务,prepare阶段所有的RM准备好后事务可以正式提交。 MySQL 5.X版本提供了XA Transaction Manger和Resourc Manager的功能: 使用InnoDB引擎实现对XA事务的支持。 可以将多个MySQL作为RM协作参与分布式事务。

原文:分布式事务方案MySQL XA

分布式事务方案MySQL XA

在TCC模式中,会把原来的一个接口分为Try接口、Confirm接口、Cancel接口。

2026/01/23 发表想法

TCC模式这里讲的倒是清楚: 以某一个负责支付的服务作为例子,发生支付的时候需要从用户账户里面扣除对应的金额 如果这个流程很长,又要生单又要支付,好几个服务必须同时成功,有一个失败都不行,那么支付这个服务拆成3个方法,写法如下: (1) 校验账户余额满不满足条件,这里如果满足就得用预扣除或者其他锁定的方法先把资源锁上 (2)所有服务第一个方法校验都通过了之后,走第二个提交方法。实际发生资源扣除,更新业务资源 (3)如果某一个服务在第二步出现问题,第三个方法负责当前流程的回滚,把扣除的资源再还回去。

原文:所以,TCC模式是一个实施起来很麻烦的方案,除了每个业务代码的工作量乘3之外,还需要通过相应逻辑应对上面的注意事项,这样出错的概率就太高了。

所以,TCC模式是一个实施起来很麻烦的方案,除了每个业务代码的工作量乘3之外,还需要通过相应逻辑应对上面的注意事项,这样出错的概率就太高了。

Seata中AT模式的自动回滚

对于Seata的内在机制,AT模式的自动回滚往往需要执行以下步骤(分为3个阶段)。 阶段1 1)解析每个服务方法执行的SQL,记录SQL的类型(Update、Insert或Delete),修改表并更新SQL条件等信息。 2)根据前面的条件信息生成查询语句,并记录修改前的数据镜像。 3)执行业务的SQL。 4)记录修改后的数据镜像。 5)插入回滚日志:把前后镜像数据及业务SQL相关的信息组成一条回滚日志记录,插入UNDOLOG表中。 6)提交前,向TC注册分支,并申请相关修改数据行的全局锁。 7)本地事务提交:业务数据的更新与前面步骤生成的UNDOLOG一并提交。 8)将本地事务提交的结果上报给事务控制器。 阶段2 收到事务控制器的分支回滚请求后,开启一个本地事务,执行如下操作。 1)查找相应的UNDOLOG记录。 2)数据校验:将UNDOLOG中的后镜像数据与当前数据进行对比,如果存在不同,说明数据被当前全局事务之外的动作做了修改,此时需要根据配置策略进行处理。 3)根据UNDOLOG中的前镜像数据和业务SQL的相关信息生成回滚语句并执行。 4)提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报事务控制器。 阶段3 1)收到事务控制器的分支提交请求后,将请求放入一个异步任务队列中,并马上返回提交成功的结果给事务控制器。 2)异步任务阶段的分支提交请求将异步、批量地删除相应的UNDOLOG记录。 以上就是Seata AT模式的简单介绍。

Seata AT模式与TCC模式相比,只有增加一个@GlobalTransactional的工作量,因此两者的工作量相差很多,也就是说,对项目组来说,投入产出比更高,值得冒险。

第15章 BFF

15.2 API层 一般来说,客户端的接口会有以下需求。 1)聚合:一个接口需要聚合多个后台服务返回的数据,然后再返回给客户端。 2)分布式调用:一个接口可能需要依次调用多个后台服务,去修改多个后台服务的数据。 3)装饰:一个接口需要重新装饰一下后台返回的数据,删除一些字段,或者对某些字段再加一个封装,组成客户端需要的数据。

15.4 BFF(BackendforFront) BFF不是一个架构,而是一个设计模式。 它的主要理念是专门为前端设计优雅的后台服务(也就是API)。换句话说,就是每一种客户端有自己的API服务。这样调用关系就变成图15-7所示。

第17章 一人一套测试环境

使用流程是这样的,每次新建一个工程时(新的API或者后台服务)都会在Jenkins上配置一个Job,而这个Job需要接受以下3个参数。 1)Branch,即需要部署的代码分支。 2)测试环境test1/test2/test3(已经有3个测试环境,它决定了部署需要使用哪个测试环境的中间件)。 3)容器测试环境标识,也就是JIRA Issue ID。 这个Job启动时,需要调用一个小工具,而这个小工具需要连接Kubernetes创建namespace(=JIRA Issue ID),然后在namespace中增加一个pod(pod中运行的是专门为JIRA Issue ID打包的代码)。 在做某个项目时,假设XXX123需要使用UserAPI、UserService、OrderService、ProductService,就会配置一个新的Jenkins Job来联动UserAPI、UserService、OrderService、ProductService的Job,并且将各个服务对应的Branch、测试环境和JIRA Issue ID传入Jenkins Job(这些值都通过硬编码配置在新的Jenkins Job中)。之后,每次点击这个项目的Jenkins Job时,就可以对其容器测试环境进行部署了。 当然,如果项目成员想自己部署一套环境,此时只需单独配置一个新的Jenkins Job,并找一个不一样的(比如开发任务的Issue ID)容器测试环境标识即可。 通过这套方案可以实现图17-4所示的效果,项目基本不会再陷入因缺少测试环境而延期的境地。

 来自微信读书