第一章 概述
1. 概述
互联网服务的价值在于高流量,高流量带来平台的成长和可能性,而高流量就带来了高并发
1.1. 成长路径

CRUD Boy:代码可复用性,业务逻辑组合
进阶选手:开源资料,技术图书
黄金成长期:主持高并发改造,活跃于技术社区等
实力战将…..
我应该是出于1和2这两个阶段中,实际工作是做CRUD,然后学习是通过开源资料。下一步要走入3,就需要在开源社区或者公司中接触高并发系统,进行锻炼
1.2. 如何实践高并发
步骤: 识别系统类型、完善监控系统、梳理改造要点、小步改造验证。
1.2.1. 识别系统类型

读多写少: 聚焦于如何通过缓存分担数据库查询压力,所以我们的学习重点就是做好缓存,包括但不限于数据梳理、做数据缓存、加缓存后保证数据一致性等等工作。
强一致性:承接高并发流量的同时,还要做好系统隔离性、事务一致性以及库存高并发争抢不超卖
写多读少: 写高并发的服务通常需要借助一些开源才能实现
读多写多: 这类系统数据基本都是在内存中直接对外服务,同时服务都要拆成很小的单元,数据是周期落到磁盘或数据库,而不是实时更新到数据库。因此我们的学习重点是如何用内存数据做业务服务、系统无需重启热更新、脚本引擎集成、脚本与服务互动交换数据、直播场景高并发优化、一些关于网络优化CDN和DNS、知识以及业务流量调度、客户端本地缓存等相关知识。
第二章 读多写少的系统
1. 结构梳理
读多写少 —> 整理数据,以便于缓存
1.1. 精简字段
长度小的数据在吞吐、查询、传输上都会很快,也会更好管理和缓存。
表字段如果缺少冗余会导致业务实现更为繁琐
“更多的字段”和“更少的职能”之间找到平衡
1.2. 判断不同种类的表是否适合缓存
实体数据表 通常适合长期缓存
实体关系表 实体辅助表 比较灵活,可能会判断是否需要缓存
动作历史表 通常不适合缓存
原则
- 缓存是hash索引,可以提升用ID直接查询的数据的效率, 根据ID能够精准匹配的数据实体很适合做缓存;而通过String、List或Set指令形成的有多条value的结构适合做(1:1、1:n、m:n)辅助或关系查询;最后还有一点要注意,虽然Hash结构很适合做实体表的属性和状态,但是Hgetall指令性能并不好,很容易让缓存卡顿,建议不要这样做。

- 范围查询、条件查询、统计计算的需要提前算好,定期更新,属于临时缓存
- 数据增长量大或者跟设计初衷不一样的表数据,这种不适合、也不建议去做做缓存。
2. 缓存一致性

2.1. 临时热点缓存
2000万用户数据,都放到缓存中,不现实
用临时缓存,放空值,防止缓存穿透
1 | // 尝试从缓存中直接获取用户信息 |
2.2. 缓存更新
2.2.1. 单个实体数据更新
先更后删,也可以用队列通知更新
2.2.2. 关系型或者统计型数据更新
例子:缓存中有user_friend_list_cache_XXXX 表示XXXX的好友信息,好友信息中存着好友的实体信息,当我们在修改一个用户的昵称时,需要删除包含它的缓存。
- 人工写逻辑 服务代码中写,耦合性强
- 订阅变更后由消息队列处理可以实现解耦
- 如果数据更新少,可以给表设置全局版本号,或者是划分范围+局部版本号
- 如果涉及到的关联的key包含更新的实体的ID的话,可以方便的删除
- 异步脚本遍历数据库刷新所有相关缓存
2.3. Token 减低身份鉴权的流量压力
这个例子主要是学习不用缓存查询,而直接从请求中拿到想要的信息
session方式:所有的请求都需要查询session cache进行鉴权,有性能瓶颈,且容易单点故障
token方式:在请求中携带token,验证token合法性即可完成鉴权,避免session的问题
1 | //header |
token的离线和更换
这个token内部包含过期时间,快过期的token会在客户端自动和服务端通讯更换,这种方式可以大幅提高截取客户端token并伪造用户身份的难度。
token过期需要到用户中心更换
需要两种token:一种是refresh_token,用于更换access_token,有效期是30天;另一种是access_token,用于保存当前用户信息和权限信息,每隔15分钟更换一次。如果请求用户中心失败,并且App处于离线状态,只要检测到本地refresh_token没有过期,系统仍可以继续工作,直到refresh_token过期为止,然后提示用户重新登陆。这样即使用户中心坏掉了,业务也能正常运转一段时间。
短token 降低token被盗风险
长token 降低用户中心崩溃带来的客户无法跟换token
| 情况 | 风险结果 | 风险窗口 |
|---|---|---|
| 短 Token 被盗 | 攻击者可以直接访问资源 | 仅在短 Token 的有效期内(如 15 分钟) |
| 长 Token 被盗 | 攻击者需要去用户中心换取短 Token;如果用户中心有风控(IP 限制、单设备策略),可以拦截 | 风险较低,取决于用户中心安全性 |
| 长 Token + 短 Token 都被盗 | 攻击者立刻能用短 Token 访问资源,并在过期后用长 Token 继续刷新 | 风险窗口 = 长 Token 的有效期,或者直到用户中心吊销它 |
| 长 Token + 短 Token 都被盗 + 用户中心崩溃 | 攻击者手上有短 Token 可以继续访问,过期后由于用户中心挂掉,刷新过程也停了。风险窗口 = 用户中心恢复之前的时间 | 风险时间 = 短 Token 的剩余有效期 + 用户中心恢复时间 |
2.4. 同城双活
防止单机故障,引发的生产事故
难点:延迟,如何保证双活
主从同步(单次请求)延迟:
- 同机房服务器:0.1 ms
- 同城服务器(100公里以内) :1ms(10倍 同机房)
- 北京到上海: 38ms(380倍 同机房)
- 北京到广州:53ms(530倍 同机房)
otter工具
HttpDNS调度,让一个用户在某一段时间内只在一个机房内活跃,这样可以降低数据冲突的情况
2.5. 多机房一致性 Raft

第三章 强一致性的系统
强一致性 –> 要用到锁、分布式事务 —> 有较大的性能瓶颈 —-> 系统隔离
1. 领域拆分
随着排期不断调整和新排期的不断加入,订单数据就会持续增加,一年内订单数据量达到了一亿多条。因为数据过多、合作周期长,并且包含了售后环节,所以这些数据无法根据时间做归档,导致整个系统变得越来越慢。
考虑到这是核心业务,如果持续存在问题影响巨大,因此朋友找我取经,请教如何对数据进行分表拆分。但根据我的理解,这不是分表分库维护的问题,而是系统功能设计不合理导致了系统臃肿。于是经过沟通,我们决定对系统订单系统做一次领域拆分。
拆分理论

抽象服务的方法
被动拆分法、动态辅助表方式、标准抽象
| 对比点 | 三层架构(MVC) | DDD |
|---|---|---|
| 分层依据 | 技术维度(表现层、逻辑层、数据层) | 业务维度(应用、领域、基础设施) |
| 业务逻辑位置 | Service 层(容易膨胀) | 领域层(聚合/实体/领域服务) |
| 实体模型 | 贫血模型(只有数据,没有行为) | 充血模型(有数据+业务方法) |
| 适用场景 | 业务简单,偏 CRUD,快速开发 | 业务复杂,领域知识密集,规则演化频繁 |
| 维护成本 | 初期低,后期复杂时 Service 容易失控 | 初期学习成本高,后期业务清晰可控 |
| 核心思想 | 分清技术层次 | 分清业务边界和领域规则 |
2. 强一致锁
几种加锁的方式
行锁、互斥锁:最差的锁,导致串行化
单个动作
原子操作、redis中放库存拆分key(数量少的时候导致多次判断)、redis维护令牌队列
多个动作
自旋互斥超时锁(redis set nx)、CAS乐观锁、lua脚本
1 | //开启事务 |

3. 系统隔离
3.1. 网关
作用
- 外网网关是对外的“城门口守卫”,防坏人、管流量。
- 内网网关是对内的“交通枢纽”,管车流、调度路线。
例子:
外网网关:Spring Cloud Gateway,处理所有外部请求。
/api/order/**→ 转发到订单服务。/api/user/**→ 转发到用户服务。
内网网关:微服务之间通过 Feign + 内网网关 调用,
- 订单服务 → 内网网关 → 用户服务。
- 内网网关做负载均衡(调用不同实例)、熔断(Hystrix/Resilience4j)。

1️⃣ 外网请求访问
用户在浏览器下单:
1 | 用户浏览器 → 外网网关 → 外网订单服务 → 数据库 |
- 外网网关:做 HTTPS、鉴权、限流、防爬虫
- 外网订单服务:处理订单逻辑
- 数据库:存储订单信息
2️⃣ 内网访问
假设内网风控系统要校验订单风险:
1 | 外网订单服务 → 内网网关 → 风控服务 |
- 外网不能直接调用风控服务,只能通过 内网网关
- 内网网关做安全控制、路由、熔断
3️⃣ 数据同步到内网(异步 Kafka)
- 外网订单服务下单成功后,不直接写内网数据库,而是发送事件到 Kafka:
1 | 外网订单服务 → Kafka Topic(order_created) → 内网清算/风控/数据分析服务订阅 |
- 内网服务根据事件更新自己的数据库
- 异步机制避免高峰期直接打内网数据库,保证稳定性
4️⃣ 各系统独立
- 外网服务、内网服务都在 独立集群
- 每个服务有自己的数据库、网关
- 即使外网流量很高,也不会直接压到内网服务
3.2. 网关隔离和随时熔断
熔断的核心目的是 保护整个系统的可用性,而不是停掉服务。
- 阻止请求打垮下游
- 提供降级方案保证部分功能可用
- 给下游恢复时间
例子:
拒绝请求调用库存服务
快速返回降级结果,比如:
- 返回默认库存值
- 返回缓存库存
- 返回友好提示:“库存服务暂不可用”
3.3. 减少内网API互动
为了防止共享的数据被多个系统同时修改,我们会在活动期间把参与活动的数据和库存做推送,然后自动锁定,这样做可以防止其他业务和后台对数据做修改。若要禁售,则可以通过后台直接调用前台业务接口来操作;活动期间也可以添加新的商品到外网业务中,但只能增不能减。

我们需要把活动交易结果同步回内网,而同步期间外网还是能继续交易的。如果不保持锁定,数据的流向不小心会成为双向同步,这种双向同步很容易出现混乱,系统要是因此出现问题就很难修复 。

3.4. 分布式队列控流和离线同步
- 队列拥有良好吞吐并且能够动态扩容,可应对各种流量冲击场景;
- 可通过动态控制内网消费线程数,从而实现内网流量可控;
- 内网消费服务在高峰期可以暂时离线,内网服务可以临时做一些停机升级操作;
- 内网服务如果出现bug,导致消费数据丢失,可以对队列消息进行回放实现重新消费;
- Kafka是分区消息同步,消息是顺序的,很少会乱序,可以帮我们实现顺序同步;
- 消息内容可以保存很久,加入TraceID后查找方便并且透明,利于排查各种问题。
4. 分布式事务
背景:DDD领域驱动是一种拆分微服务的方式, 服务被拆分得更细,并且都是独立部署,拥有独立的数据库,这就导致要想保持事务一致性实现就更难了,因此跨越多个服务实现分布式事务已成为刚需。
第四章 读多写少的系统
读多写多,第一反应是应该避免这种情况,如果实时性、一致性要求不高,可以有降级、限流、多级缓存等等操作,如果是在不行就只能分片扩容
用缓存的时候如果想避免网络开销,甚至可以在同一台机器上开一个小的redis
1. 本地缓存

2. 业务脚本
我们已经习惯了使用缓存集群对数据做缓存,但是这种常见的内存缓存服务有很多不方便的地方,比如集群会独占大量的内存、不能原子修改缓存的某一个字段、多次通讯有网络损耗。
很多时候我们获取数据并不需要全部字段,但因为缓存不支持筛选,批量获取数据的场景下性能就会下降很多。这些问题在读多写多的场景下,会更加明显。
2.1. 缓存即服务

通过缓存预热,把缓存中的数据用脚本处理后再放到本地的Map、Set等,可以对其直接进行操作,避免用的时候再
3. 流量拆分
一般来说,这种服务多数属于实时互动服务,因为时效性要求很高,导致很多场景下,我们无法用读缓存的方式来降低核心数据的压力。所以,为了降低这类互动服务器的压力,我们可以从架构入手,做一些灵活拆分的设计改造。
3.1. 可预估用户量的服务
3.2. 不可预估用户量的服务
3.2.1. 聊天:信息合并
点赞或大量用户输入同样内容的刷屏情境可以合并信息,压缩整理后的聊天内容会被分发到多个聊天内容分发服务器上,直播间内用户的聊天长连接会收到消息更新的推送通知,接着客户端会到指定的内容分发服务器群组里批量拉取数据,拿到数据后会根据时间顺序来回放。请注意,这个方式只适合用在疯狂刷屏的情况,如果用户量很少可以通过长链接进行实时互动。

3.2.2. 答题:瞬时信息拉取高峰
除了交互流量极大的聊天互动信息之外,还有一些特殊的互动,如做题互动。直播间老师发送一个题目,题目消息会广播给所有用户,客户端收到消息后会从服务端拉取题目的数据。
如果有10w用户在线,很有可能导致瞬间有10w人在线同时请求服务端拉取题目。这样的数据请求量,需要我们投入大量的服务器和带宽才能承受,不过这么做这个性价比并不高。
理论上我们可以将数据静态化,并通过CDN阻挡这个流量,但是为了避免出现瞬时的高峰,推荐客户端拉取时加入随机延迟几秒,再发送请求,这样可以大大延缓服务器压力,获得更好的用户体验。
切记对于客户端来说,这种服务如果失败了,就不要频繁地请求重试,不然会将服务端打沉。如果必须这样做,那么建议你对重试的时间做退火算法,以此保证服务端不会因为一时故障收到大量的请求,导致服务器崩溃。
如果是教学场景的直播,有两个缓解服务器压力的技巧。第一个技巧是在上课当天,把抢答题目提前交给客户端做预加载下载,这样可以减少实时拉取的压力。
第二个方式是题目抢答的情况,老师发布题目的时候,提前设定发送动作生效后5秒再弹出题目,这样能让所有直播用户的接收端“准时”地收到题目信息,而不至于出现用户题目接收时间不一致的情况。
至于非抢答类型的题目,用户回答完题目后,我们可以先在客户端本地先做预判卷,把正确答案和解析展示给用户,然后在直播期间异步缓慢地提交用户答题结果到服务端,以此保证服务器不会因用户瞬时的流量被冲垮。
3.2.3. 点赞:服务端树形汇总
一致性要求不高的计数器

这个方式可以将用户点赞流量随机压到不同的写缓存服务上,通过第一层写缓存本地的实时汇总来缓解大量用户的请求,将更新数据周期性地汇总后,提交到二级写缓存。
之后,二级汇总所在分片的所有上层服务数值后,最终汇总同步给核心缓存服务。接着,通过核心缓存把最终结果汇总累加起来。最后通过主从复制到多个子查询节点服务,供用户查询汇总结果。
3.2.4. 打赏&购物:服务端分片及分片实时扩容
具有强一致性
因为事务一致性的要求,这种服务我们不能做成多层缓冲方式提供服务,而且这种服务的数据特征是读多写多,所以我们可以通过数据分片方式实现这一类服务

分片算法
一致性哈希: 一致性哈希让集群扩容变简单,但你很难“挑选”哪些数据去哪个节点,需要额外工具,否则控制起来麻烦。
树形热迁移切片法: 虚拟桶,把数据分片到桶上,然后一个服务器管理几个桶,满足动态扩容要求
3.3. 服务降级:分布式队列汇总缓冲
限流,利用队列将消息合并 牺牲实时性
4. 流量调度:DNS、全站加速及机房负载均衡
第五章 写多读少的系统
1. 稀疏索引
这节课,我们讨论了OLAP和OLTP数据库的索引、存储、数据量以及应用的不同场景。
OLAP相对于关系数据库的数据存储量会更多,并且对于大量数据批量写入支持很好。很多情况下,高并发批量写数据很常见,其表的字段会更多,数据的存储多数是用列式方式存储,而数据的索引用的则是列索引,通过这些即可实现实时大数据计算结果的查询和分析。
相对于离线计算来说,这种方式更加快速方便,唯一的缺点在于这类服务都需要多台服务器做分布式,成本高昂。
可以看出,我们使用的场景不同决定了我们的数据底层如何去做更高效,HTAP的出现,让我们在不同的场景中有了更多的选择,毕竟大数据挖掘是一个很庞大的数据管理体系,如果能有一个轻量级的OLAP,会让我们的业务拥有更多的可能。
第六章 内网服务设计
1. 统一缓存数据平台
1.1. 实体数据主动缓存


双写:缓存的内容是多个表聚合来的,写通常比删的开销要大
这个方案服务在写的时候是先更再删,中间件负责监听以及回填缓存(需要写回填的脚本),消息队列保证可靠性
1.2. L1缓存及热点缓存延期

1.3. 关系数据缓存
为此,我们首先需要改进消息监听服务,将它做成Kafka Group Consumer服务,同时实现可动态扩容,这能提升系统的并行数据处理能力,支持更大量的并发修改。
其次,对于量级更高的数据缓存系统,还可以引入多种数据引擎共同提供不同的数据支撑服务,比如:
- lua脚本引擎(具体可以回顾第十七节课)是数据推送的“发动机”,能帮我们把数据动态同步到多个数据源;
- Elasticsearch负责提供全文检索功能;
- Pika负责提供大容量KV查询功能;
- ClickHouse负责提供实时查询数据的汇总统计功能;
- MySQL引擎负责支撑新维度的数据查询。
1.4. 多数据引擎平台
