系统设计与架构常见题整理
GitHub地址:点击跳转
1. 如何设计一个短链服务
参考链接:点击跳转
1.1 短链的价值
- 更加简洁:比起一长串无意义的问题,只有差不多 10 个字符的字符串显然更加简洁
- 便于使用:第一,有些平台对内容长度有限制(微博只能发 140 个字),此时短网址就可以输入更多内容。第二,我们将链接转为二维码时,短链接生成的二维码更容易识别。第三,有些平台无法识别特殊的长链参数,转为短链就没这个问题
- 节省成本:当我们需要发短信的时候,短信是按照长度计费的,短网址可以节省成本
1.2 短链的原理
当我们输入短链时,其实访问的是短链服务器的地址。短链服务器获取到对应的长链地址之后,返回一个 302 的 HTTP 响应,在响应中包含了长链接地址。浏览器收到响应后,转而去请求长链接地址。 访问短链的整个流程如下图所示:

从上面的流程中可以知道,短链涉及到的技术原理主要有两点,分别是:HTTP 重定向和短链服务的设计
对于 HTTP 重定向来说,301 和 302 都是重定向:
- 301 代表永久重定向。它表示第一次拿到长链接之后,下次浏览器如果再去请求短链的话,不会再向短链服务器请求了,而是直接从浏览器的缓存中获取。
- 302 代表临时重定向。它表示每次请求短链都会去请求短链服务器,不会从浏览器缓存中获取
如果我们希望统计短链接的点击次数信息,从而来分析活动的效果的话。那么我们就需要使用 302 重定向码,这样才能获取到每次的请求数据。 一般情况下,我们都是需要获取到请求的数据的,因此对于短链服务都是用 302 临时重定向
1.3 实现思路
系统的处理流程:
- 用户访问短链生成页面,输入长链字符串,短链服务返回生成的短链
- 用户访问短链,短链服务返回 302 响应,用户浏览器跳转到长链地址
如果我们要实现上面的系统流程,我们大致的处理思路是:
- 生成短链。 生成短链时,短链服务获取到长链,随后生成一个短链,并把短链与长链的映射关系保存下来,最后将短链返回给用户
- 找到长链。 访问短链时,短链服务获取到短链,根据短链去获取到长链,返回返回 302 响应
根据上面的分析,我们可以知道短链系统设计主要得解决如下两个问题:
- 如何根据长链生成唯一短链?
- 如何保存短链与长链的映射关系
对于第 1 点,我们有 2 个思路生成一个唯一短链,分别是:
- 使用哈希算法生成唯一值
- 使用分布式唯一 ID 生成作为锻炼 ID
对于第 2 点,保存短链与长链的映射关系,考虑到持久性的问题,我们肯定需要落库,所以使用 MySQL 表保存即可。如果有需要的话,可以在 MySQL 前做一层缓存。因此第 2 点相对来说比较简单
1.3.1 哈希算法生成短链
要生成一个短链,我们可以将原有的长链做一次哈希,然后就可以得到一个哈希值,计算哈希值会遇到如下2个问题:
使用什么哈希算法
我们都知道哈希算法是一种摘要算法,它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。我们常见的哈希算法有:MD5、SHA-1、SHA-256、SHA-512 算法等。但我们最好还是使用另一种叫做 MurmurHash 的哈希算法。为什么呢
因为 MD5 和 SHA 哈希算法,它们都是加密的哈希算法,也就是说我们无法从哈希值反向推导出原文,从而保证了原文的保密性
但对于我们这个场景而言,我们并不关心安全性,我们关注的是运算速度以及哈希冲突。而 MurmurHash 算法是一个非加密哈希算法,所以它的速度回更快
哈希冲突
学过 HashMap 的同学都知道,哈希冲突是哈希算法不可避免的问题。而解决哈希冲突的方式有两种,分别是:链表法和重哈希法。HashMap 使用了链表法,但我们这里使用的是重哈希法
所谓的重哈希法,指的是当发生哈希冲突的时候,我们在原有长链后面加上固定的特殊字符,后续拿出长链时再将其去掉,如下所示
1 | 原有长链:https://mp.weixin.qq.com/s1caec8eb1b81d6ee5dd7b |
通过这种办法,我们就可以解决哈希冲突的问题了。如果再次发生,那么就再进行哈希,一直到不冲突位置。一般来说,哈希冲突的可能性微乎其微
好了,现在我们通过哈希算法得到了一个哈希值:29541341303115543223957290326355,变成了这样:http://dwz.com/29541341303115543223957290326355
有没有办法让网址变得再短一点呢
我们知道在网址 URL 中,常用的合法字符有 0~9、a~z、A~Z 这样 62 个字符。如果我们用哈希值与 62 取余,那么余数肯定是在 0-61 之间
这 62 个数字刚好与 62 个合法网址字符一一对应。接着,我们再用除 62 得到的值,再次与 62 取余,一直到位 0 为止。通过这样的处理,我们就可以得到一个字符为 62 个字符、长度很短的字符串了
上面讲有点晦涩难懂,我们来举个例子。假设我们得到的哈希值为 181338494,那么上面的处理流程为:
- 将 181338494 除以 62,得到结果为 2924814,余数为 26,此时余数 26 对应字符为 q。
- 将 2924814 除以 62,得到结果为 47174,余数为 26,此时余数 26 对应字符为 q。
- 将 47174 除以 62,得到结果为 760,余数为 54,此时余数 54 对应字符为 S。
- 省略剩余步骤
整个处理流程如下图所示:

可以看到,我们把 181338494 这个十进制数,转成了由合法网址字符组成的「62 进制数」—— cgSqq
到这里,我们不仅生成了短链,还将短链的长度极大地缩短了
这就是使用哈希算法生成唯一锻炼的全部内容了,我们总结一下:首先,使用 MurmurHash 生成哈希值,并且用重哈希法解决哈希冲突的问题。接着,将 10 进制的哈希值转成 62 进制的合法网址字符,从而缩短网址长度
1.3.2 分布式 ID 生成短链
上面使用哈希算法生成唯一短链的方式,相对来说是比较形象的。但其实我们也可以用分布式 ID 的方式,来完成唯一短链的生成
例如第一次请求的长链,我们为其生成一个唯一 ID,将其长链与唯一 ID 对应起来。第二次请求,我们再为其生成一个唯一 ID,再次将长链与唯一 ID 对应起来,如下所示。
1 | 第一次请求:https://mp.weixin.qq.com/s1caec8eb1b81d6ee5dd7b |
因为生成的唯一 ID 也可能非常长,因此我们可以采用上面同样的方式,将 10 进制的唯一 ID 转成 62 进制的合法网址字符,从而缩短字符长度
那么接下来的问题就变成了:如何设计一个全局唯一 ID 发号器了
对于如何设计一个全局唯一的 ID 发号器,就属于另外一个话题,我们这里就不深入探讨了
1.4 性能优化
看到这里,我们基本上有了一个完整的思路:拿到长链地址后,可以用哈希算法或唯一 ID 分号器获取唯一字符串,从而建立长链与短链的映射关系。为了缩短短链长度,我们还可以将其用 62 进制数表示,整个短链生成过程如下图所示

短链生成完,并且已经存到了数据库中,接下里该使用了。通常的做法是会根据请求的短链字符串,从数据库中找到数据,然后返回 HTTP 重定向原始地址。而在不断使用过程中,还有一些可能发现的优化点,这里简单讲讲
索引优化
- 如果使用关系型数据库的话,对于短链字段需要创建唯一索引,从而加快查询速度
增加缓存
- 并发量小的时候,我们都是直接访问数据库。但当并发量再次升高时,需要加上缓存抗住热点数据的访问
读写分离
- 短链服务肯定是读远大于写的,因此对于短链服务,可以做好读写分离
分库分表
- 如果是商用的短链服务,那么数据量上亿是很正常的,更不用说常年累月积累下的量了。这时候可以一开始就做好分库分表操作,避免后期再大动干戈
- 对于分库分表来说,最关键的便是根据哪个字段去作为分库分表的依据了。对于短链服务来说,当然是用转化后的 62 进制数字做分表依据了,因为它是唯一的嘛
防止恶意攻击
开放到公网的服务,什么事情都可能发生,其中一个可能的点就是被恶意攻击,不断循环调用
一开始我们可以做一下简单地限流操作,例如:
- 没有授权的用户,根据 IP 进行判断,1 分钟最多只能请求 10 次
- 没有授权的用户,所有用户 1 分钟最多只能请求 4000 次,防止更换 IP 进行攻击
简单地说,就是要不断提高攻击的成本,使得最坏情况下系统依然可以正常提供服务
1.5 总结
在短链服务的设计思路上,最重要是解决两个问题:根据长链生成短链、根据短链找到长链。在根据长链生成短链的思路上,有两种实现思路,分别是:哈希算法生成短链、分布式全局 ID 生成短链,其中哈希算法涉及到哈希算法的选择,以及哈希冲突的处理
最后还列举了一些短链服务后续可能的优化点,包括:如何让网址变得更短、索引优化、增加热点数据、读写分离、分库分表、防止恶意攻击等等

2. 如何设计一个秒杀系统
参考链接:点击跳转
秒杀系统的设计是高级职位面试中非常高频的一道题目,它可以较好地考察候选人的知识体系情况。对于我们来说,学习秒杀系统的设计,能够让我们学以致用,设计系统的时候考虑得更加全面
活动一般出现在电商的促销活动中,一般是指定了很少数量的商品,以极低的价格,让大量的用户参与,从而造成大量用户在极短的时间内参与活动,进而造成系统在极短的时间内有极高的流量。系统设计的目的是使系统能够稳定地支撑活动的进行,因此其稳定性、高可用是我们考虑的第一位
要知道如何进行秒杀系统的优化,那我们需要先对请求的整个流程有个全局的认识。一般来说,秒杀活动请求以公网为划分点,可以分为:前端部分、后端部分。 前端部分指的是从用户端到进入后端服务前的部分,包括了移动端的处理、DNS 解析、公网的数据传递等
后端部分指的是经公网进入了后端的服务器网络里,包括了前置的负载均衡(Nginx 等)、应用服务器、数据库层等。秒杀活动的整个流程可以用下图来表示

我们要去设计一个秒杀系统,那自然也是从这两大部分来进行优化。整体思路是尽量将流量挡在前面,让尽量少的流量留到后端部分。因为越往后端,我们的处理逻辑就越重,其处理能力也越弱
2.1 前端优化
对于前端部分来说,常见的优化手段有:页面静态化 + CDN、请求频率限制
2.1.1 页面静态化 + CDN
一般来说,活动页面是流量最大的地方。活动页面上绝大部分内容都是固定的,比如:商品描述、图片等。这时候没有必要每次都去请求服务端,而是将这些静态的内容放到 CDN 上
每次打开页面的时候,直接去请求 CDN 服务器,能极大地减少后端的请求流量。加入了 CDN 之后,其请求过程如下:

所谓的 CDN 就是内容分发网络,它由非常多台分布在世界各地的缓存服务器组成。每次用户请求特定域名的时候,会转发到对应 CDN 的 DNS 解析服务器,随后会返回一台离用户地理位置最近的一台 CDN 服务器
随后,用户直接请求这台 CDN 服务器获取数据,从而极大地减少了长途网络传输的时间,并且也减少了后端服务器的压力
因此,对于秒杀活动设计来说,我们可以将所有可以静态化的内容全部静态化,然后将其配置在 CDN 服务器上。这样既提高了用户打开页面的时间,又减少了后端服务器的压力
2.1.2 请求频率限制
请求频率限制,指的是根据业务的特点,在前端做一些流量拦截,减少后端服务器的压力。常见的拦截方式有:
- 设定一个请求概率,只允许 30% 的概率向后端发送接口请求。
- 设定一个请求频率,例如 10 秒钟只能请求 1 次,随后按钮置灰
通过这种方式,我们可以减少很大一部分流量。但在具体实现的时候,可能需要考虑安全问题,预防某些用户直接调用后台接口,绕过前端的频率检查
常见的方法是在频率检查时生成一个参数,随后请求后端服务时携带上该参数。没有该参数的请求,都视为非法请求,直接拒绝该请求
2.2 后端优化
无论我们做多大的努力,始终还是会有不少流量会来到后端服务器这里。一般来说,后端的优化有如下几种方式:
- 增加缓存层 + 预热数据
- MQ 异步处理
- 限流、熔断、兜底
- 业务侧优化
2.2.1 增加缓存层 + 预热数据
如果我们所有数据都去读取数据库,数据库可能无法承受较大的流量,此时一个常见的优化就是增加缓存层
当我们需要查询数据库之前,我们先去查询缓存,这样可以减少绝大部分的数据库请求,减轻数据库压力。如果在缓存中找不到数据,我们再去请求数据库,随后再将数据缓存到缓存中
在引入缓存层的时候,我们需要考虑缓存击穿、缓存穿透的可能性,在写相关代码的时候就要做好这些优化。另外,我们在秒杀活动开始之前,可以手动将热点数据加载到缓存中,从而避免秒杀时去请求数据库
2.2.2 MQ 异步处理
我们知道秒杀活动一般涉及抢购、下单、支付、发货等阶段,而抢购与后续的几个阶段是可以异步执行的。为了避免对下单、支付、发货等阶段产生影响,我们可以将抢购阶段与后续阶段用 MQ 进行解耦处理。当用户抢购成功后,往消息队列中丢入一台消息,随后再由订单系统消费进行下单处理
通过各系统之间的解耦处理,我们可以将原本同步的处理方式变为异步处理,从而大大的减少了请求的处理时间,提高了系统的并发处理能力。其次,也能避免系统之间相互影响,提高了整体系统的稳定性
2.2.3 限流、熔断、兜底
我们可以在每个业务系统做限流操作,从而避免因为请求太多,导致整个系统都无法工作。当并发请求在正常范围内时,我们正常处理请求。当超过设置的限流阈值时,我们则直接拒绝该请求,提示用户抢购失败
如果没有限流操作,那么系统直接崩溃了,一个请求都处理不了。而通过限流这种方式,系统至少还可以保持正常工作,而不至于一个请求都处理不了。而超量的需求,本来就处理不了,因此提示失败也是情理之中
除了限流之外,不同的系统还可以采用熔断、降级的服务治理措施
熔断指的是请求的错误次数超过阈值时,不再到用后端服务,直接返回失败。同时每隔一定时间放几个请求去重试后端服务,看看是否正常。如果正常则关闭熔断状态,如果失败则继续快速失败。熔断的目的是避免因下游短暂的异常,导致上游不断重试,最终造成下游有太多请求,最终压垮下游系统
降级指的是当服务失败或异常后,返回指定的默认信息。降级的目的是保证有基本的信息,当下游异常时,与其返回空信息,不如返回一个有业务含义的默认信息,可以提高用户体验
2.2.4 业务侧优化
一般来说,经过上述的整体优化之后,系统已经能够比较稳当地应对秒杀活动了。如果此时还是流量比较大,那么或许应该从业务侧去进行优化了
例如 12306 刚开始的时候,购买时间都在同一时刻,这导致同一时刻并发量太大,系统经常支撑不住。后来 12306 将购票周期放长,可以提前 20 天购买火车票。通过业务侧的优化,我们将本来在 1 个小时的抢购分摊到了 20 天,服务器压力一下子降低了 480 倍
因此从业务侧进行优化,是一个四两拨千斤的办法,可以极大地降低技术侧实现的难度
2.3 总结
设计一个秒杀系统,整体而言可以从前端与后端进行优化。
对于前端优化而言,可以从「页面静态化 + CDN」、请求频率限制进行优化
其中「页面静态化 + CDN」指的是将不变的静态数据固定下来,然后放入 CDN 服务器,从而降低用户请求的响应速度,降低服务器的并发压力。请求频率限制,则是通过抢购概率与抢购频率限制,降低后端服务器的服务压力。
对于后端优化而言,一般有「增加缓存层 + 预热数据」、「MQ 异步处理」、「限流、熔断、降级」、业务侧优化这 4 种优化方式
其中「增加缓存层 + 预热数据」指的是将热点数据存入缓存,并在活动开始前提前加载到缓存中,降低数据库层的读取压力。「MQ 异步处理」指的是对于非必要的业务逻辑,通过 MQ 进行异步处理,降低请求处理延时,同时提高业务系统整体稳定性
「限流、熔断、降级」是对于整体微服务的保护,其中限流指的是对请求进行限制,当超过限流阈值时,直接拒绝请求,保护系统本身;熔断指的是保护下游系统,当请求下游系统连续错误超过阈值时,自动不去请求下游系统,避免因重试流量过大击垮下游系统。
降级指的是当请求失败时,自动返回默认数据,提高用户体验。业务侧优化,则是指从业务层面去进行逻辑优化,从而降低技术复杂度,使得业务与技术复杂度达到一个平衡的状态,有利于更好地实现秒杀系统的高可用与高并发。
上面说到的 6 个优化思路,是设计秒杀系统常见的优化思路。但在实际业务场景中,除了要保障正常的功能设计之外,还还考虑防刷、安全、黑产等问题,此时可能需要多考虑一些其他优化,例如:黄牛利用抢购工具抢购,导致正常用户无法抢到商品等
这时候可能需要考虑增加验证码,用 App 设备指纹等风控措施。此外,对于秒杀系统而言,做好业务指标和系统指标的埋点监控也是非常重要的

本文链接:
https://huajun-chen.github.io/2022/11/01/系统设计与架构/