> For the complete documentation index, see [llms.txt](https://lewiszlw.gitbook.io/notebooks/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lewiszlw.gitbook.io/notebooks/system-design/system-design.md).

# system-design

## 设计模式六大原则

**单一职责原则（Single Responsibility Principle, SRP）**

就一个类而言，应该仅有一个引起它变化的原因。

如果一个类承担的职责过多，就等于把这些职责耦合在一起，一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计，当变化发生时，设计会遭受到意想不到的破坏。

**开放‑封闭原则（Open-Closed Principle, OCP）**

是说软件实体（类、模块、函数等待）应该可以扩展，但是不可修改（Open for extension, closed for modification）。

无论模块多么地“封闭”，都会存在一些无法对之封闭的变化。既然不可能完全封闭，设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类，然后构造抽象来隔离那些变化。在我们最初编写代码时，假设变化不会发生。当变化发生时，我们就创建抽象来隔离以后发生的同类变化。面对需求，对程序的改动是通过增加新代码进行的，而不是更改现有的代码。

**依赖倒置原则（Dependence Inversion Principle，DIP）**

高层模块不应该依赖底层模块。两个都应该依赖抽象。抽象不应该依赖细节。细节应该依赖抽象。针对接口编程，不要对实现编程。

举例：访问数据库逻辑写到一个函数（底层模块）用以复用，业务层（高层模块）进行调用。如果需要更换不同数据库或者不同连接池等等，但此时高层模块与底层模块耦合在一起。办法就是在高层模块和底层模块之间做一层抽象（接口或者抽象类）。

**接口隔离原则(Interface Segregation Principle, ISP)** 使用多个专门的接口，而不使用单一的总接口，即客户端不应该依赖那些它不需要的接口。

根据接口隔离原则，当一个接口太大时，我们需要将它分割成一些更细小的接口，使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色，不干不该干的事，该干的事都要干。这里的“接口”往往有两种不同的含义：一种是指一个类型所具有的方法特征的集合，仅仅是一种逻辑上的抽象；另外一种是指某种语言具体的“接口”定义，有严格的定义和结构，比如Java语言中的interface。

**里式替换原则（Liskov Substitution Principle, LSP）**

子类型必须能够替换掉它们的父类型。

也就是说，在软件里面，把父类都替换成它的子类，程序的行为没有变化。只有当子类可以替换掉父类，软件单位的功能不受到影响时，父类才能真正被复用，而子类也能够在父类的基础上增加新的行为。

**迪米特法则（Law of Demeter, LoD）**

如果两个类不必彼此直接通信，那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话，可以通过第三者转发这个调用。根本思想是强调了类之间的松耦合。

**接口隔离原则和单一职责原则的区别**

1. 单一职责原则注重的是职责，而接口隔离原则注重的是对接口依赖的隔离。
2. 单一职责原则主要是约束类，它针对的是程序中的实现和细节；接口隔离原则主要约束接口，主要针对抽象和程序整体框架的构建。

## 设计模式

**简单工厂、工厂方法和抽象工厂区别**

* 简单工厂：唯一工厂类，一个产品抽象类，工厂类的创建方法依据入参判断并创建具体产品对象。（举例：一个汽车生产商承接了各种品牌汽车的生产，当为宝马汽车生产产品时只需要在调用该工厂的方法时传入参数“BM”即可创建相应产品，现用简单模式来模拟实现）
* 工厂方法：多个工厂类，一个产品抽象类，利用多态创建不同的产品对象，避免了大量的if-else判断。（举例：一个汽车生产商承接了各种品牌汽车的生产，将其原有的汽车工厂进行分割，为每种品牌的汽车各自设立一个子工厂，宝马汽车工厂专门生产宝马汽车，奔驰汽车厂专门生产奔驰汽车，如果再次承接了其他品牌汽车的生产合作，只需要添加一个对应的工厂即可，无需对原有的工厂进行修改）
* 抽象工厂：多个工厂类，多个产品抽象类，产品子类分组，同一个工厂实现类创建同组中的不同产品，减少了工厂子类的数量。（举例：一个汽车生产商承接了各种品牌汽车的生产，又可以生产多种种类的汽车，像公共汽车、小轿车等，相同品牌的汽车构成一个产品族，而相同类型的汽车又构成了一个产品等级结构，使用抽象工厂模式实现该应用，如宝马汽车厂生产宝马公汽和小轿车，奔驰汽车厂生产奔驰公汽和小轿车）

## 文章海量评论分页设计

**场景**：

文章的评论量非常大, 比如说一篇热门文章就有几百万的评论, 设计一个后端服务, 实现评论的时序展示与分页。

**设计**：

1.不支持页码跳转：传评论id 用 offset 实现翻页，(文章id, 评论id) 建联合索引，评论 id 需递增。

如果表中存在`连续`的数字列并为索引，那么通过页码即可计算出此字段的范围，直接作范围查询即可：

```
start = (page-1) * pagesize
end = page * pagesize
select * from table where id > start and id <= end
```

2.支持页码跳转：不能做精准深分页，否则压力太大，在50或100页后数据分页是否可以不完全精确，假如可以，那么缓存深页码的起始评论 id。

## 海量数据出现频率

TODO

## 分库分表下的分页查询

每张表查 N 条数据，数据在服务层进行内存排序，得到数据全局视野，再取目标页数据，便能够得到想要的全局分页数据。

## 缓存重建

1. 互斥锁：只允许一个线程重建缓存，其他线程等待重建缓存的线程执行完，重新从缓存获取数据即可
2. 永不过期：从缓存层面来看，确实没有设置过期时间，所以不会出现热点 key 过期后产生的问题，也就是“物理”不过期。从功能层面来看，为每个 value 设置一个逻辑过期时间，当发现超过逻辑过期时间后，会使用单独的线程去构建缓存。

## 六边形架构

![image](https://raw.githubusercontent.com/lewiszlw/notebooks/master/assets/system-design/%E5%85%AD%E8%BE%B9%E5%BD%A2%E6%9E%B6%E6%9E%84.jpg)

在六边形架构风格中，应用程序的内部（中间的橙色六边形）包含业务规则，基于业务规则的计算，领域对象，领域事件等。而外部的，也是我们平时最熟悉的诸如REST，SOAP，NoSQL，SQL，Message Queue等，都通过一个端口接入，然后在内外之间有一个适配器组成的层，它负责将不同端口来的数据进行转换，翻译成领域内部可以识别的概念（领域对象，领域事件等）。

内部不关心数据从何而来，不关心数据如何存储，不关心输出时JSON还是XML，事实上它对调用者一无所知，它可以处理的数据已经是经过适配器转换过的领域对象了。

## SOLID原则

**单一责任原则（The Single Responsibility Principle）**

一个类只应承担一种责任。

**开放封闭原则（The Open Closed Principle）**

实体应该对扩展是开放的，对修改是封闭的。

举例：比如系统支付方式有多种，每新增一种需要修改原有代码，可以抽离出来一个PayHandler接口，这样新增支付方式只需要新增一个PayHandler实现类即可，不用改动原有代码。

**里氏替换原则（Liskov Substitution Principle）**

一个对象在其出现的任何地方，都可以用子类实例做替换，并且不会导致程序的错误。

**接口分离原则（The Interface Segregation Principle）**

客户(client)不应被强迫依赖它不使用的方法。即，一个类实现的接口中，包含了它不需要的方法。将接口拆分成更小和更具体的接口，有助于解耦，从而更容易重构、更改。

**依赖倒置原则（The Dependency Inversion Principle）** 高层次的模块不应依赖低层次的模块，他们都应该依赖于抽象；抽象不应依赖于具体实现，具体实现应依赖抽象。

举例：数据库连接Driver、六边形架构、MVC模式中面向接口编程

## 缓存穿透、缓存击穿和缓存雪崩

缓存穿透是指缓存和数据库中都没有的数据，而用户不断发起请求，如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者，攻击会导致数据库压力过大。

* 接口层增加校验，如用户鉴权校验，id做基础校验
* 布隆过滤器
* 从缓存取不到的数据，在数据库中也没有取到，这时也可以将key-value对写为key-null，缓存有效时间可以设置短点，如30秒（设置太长会导致正常情况也没法使用）

缓存击穿是指缓存中没有但数据库中有的热点数据（一般是缓存时间到期），这时由于并发用户特别多，同时读缓存没读到数据，又同时去数据库去取数据，引起数据库压力瞬间增大，造成过大压力。

* 设置热点数据永远不过期
* 加互斥锁

缓存雪崩是指缓存中数据大批量到过期时间，而查询数据量巨大，引起数据库压力过大甚至down机。和缓存击穿不同的是，缓存击穿指并发查同一条数据，缓存雪崩是不同数据都过期了，很多数据都查不到从而查数据库。

* 缓存数据的过期时间设置随机，防止同一时间大量数据过期现象发生
* 如果缓存数据库是分布式部署，将热点数据均匀分布在不同搞得缓存数据库中
* 设置热点数据永远不过期
* 加互斥锁

## 缓存热点问题解决方案

问题描述： 如redis集群的热点key（一个key map到一台redis机器上）会导致局部机器是热的，其他机器是冷的

解决方案： 1.可以采用多级缓存，将热点key-value缓存到本地缓存上，这样不用再访问redis机器了，利用Redis自带的消息通知机制，对于热点Key客户端建立一个监听，当热点Key有更新操作的时候，客户端也随之更新。 2.可以在程序层面加工key，如key#redis1、key#redis2、key#redis3。

## 读写分离

读写分离，基本的原理是让主数据库处理事务性增、改、删操作（INSERT、UPDATE、DELETE），而从数据库处理SELECT查询操作。

优点

1. 提高数据库的并发处理能力；
2. 避免写请求锁表阻塞读请求；
3. 避免单点，提高数据库的可用性；

**读写分离和缓存比较**

1. 价格，缓存基于内存价格较贵，而主从库基于磁盘价格便宜
2. 缓存存在未命中情况，仍然需要从数据库读取，数据库写入会影响读取性能
3. 缓存更多的是抵挡请求对DB的压力，针对热点数据

## 微服务

微服务 (Microservices) 就是一些协同工作小而自治的服务。

* 小，小就是简单，简单到通过名字或者定义就可以知道它大概能干什么事了，而且只干这一件事
* 自治，独立部署、独立运行，并能独立完成一个业务闭环

优点

1. 技术异构性。不同服务内部的开发技术可以不一致
2. 隔离性。一个服务不可用不会导致另一个服务也瘫痪，因为各个服务是相互独立和自治的系统
3. 可扩展性。可以只对那些影响性能的服务做扩展升级
4. 简化部署。单体服务几百万行代码，即使修改了几行代码也要重新编译整个应用，这显然是非常繁琐的，部署风险更高
5. 易优化。代码量不会很大，容易重构

缺点

1. 分布式带来的复杂性，如分布式数据库、分布式事务
2. 测试挑战。对微服务进行测试，需要启动它依赖的所有其他服务
3. 涉及多个服务时部署复杂

## Service Mesh 服务网格

TODO

服务网格（Service Mesh）是处理服务间通信的基础设施层。它负责构成现代云原生应用程序的复杂服务拓扑来可靠地交付请求。在实践中，Service Mesh 通常以轻量级网络代理阵列的形式实现，这些代理与应用程序代码部署在一起，对应用程序来说无需感知代理的存在。

如果用一句话来解释什么是 Service Mesh，可以将它比作是应用程序或者说微服务间的 TCP/IP，负责服务之间的网络调用、限流、熔断和监控。对于编写应用程序来说一般无须关心 TCP/IP 这一层（比如通过 HTTP 协议的 RESTful 应用），同样使用 Service Mesh 也就无须关心服务之间的那些原本通过服务框架实现的事情，比如 Spring Cloud、Netflix OSS 和其他中间件，现在只要交给 Service Mesh 就可以了。

## 接口加密方案

TODO
