English ∙ 日本語 ∙ 简体中文 ∙ 繁體中文 | العَرَبِيَّة ∙ বাংলা ∙ Português do Brasil ∙ Deutsch ∙ ελληνικά ∙ עברית ∙ Italiano ∙ 한국어 ∙ فارسی ∙ Polski ∙ русский язык ∙ Español ∙ ภาษาไทย ∙ Türkçe ∙ tiếng Việt ∙ Français | Add Translation
学习如何设计大型系统。
为系统设计的面试做准备。
学习如何设计可扩展的系统将会有助于你成为一个更好的工程师。
系统设计是一个很宽泛的话题。在互联网上,关于系统设计原则的资源也是多如牛毛。
这个仓库就是这些资源的组织收集,它可以帮助你学习如何构建可扩展的系统。
这是一个不断更新的开源项目的初期的版本。
欢迎贡献!
在很多科技公司中,除了代码面试,系统设计也是技术面试过程中的一个必要环节。
实践常见的系统设计面试题并且把你的答案和例子的解答进行对照:讨论,代码和图表。
面试准备的其他主题:
这里提供的抽认卡堆使用间隔重复的方法,帮助你记忆关键的系统设计概念。
随时随地都可使用。
你正在寻找资源以准备编程面试吗?
请查看我们的姐妹仓库互动式编程挑战,其中包含了一个额外的抽认卡堆:
从社区中学习。
欢迎提交 PR 提供帮助:
一些还需要完善的内容放在了正在完善中。
请查看贡献指南。
各种系统设计主题的摘要,包括优点和缺点。每一个主题都面临着取舍和权衡。
每个章节都包含着更多的资源的链接。
基于你面试的时间线(短、中、长)去复习那些推荐的主题。
问:对于面试来说,我需要知道这里的所有知识点吗?
答:不,如果只是为了准备面试的话,你并不需要知道所有的知识点。
在一场面试中你会被问到什么取决于下面这些因素:
那些有经验的候选人通常会被期望了解更多的系统设计的知识。架构师或者团队负责人则会被期望了解更多除了个人贡献之外的知识。顶级的科技公司通常也会有一次或者更多的系统设计面试。
面试会很宽泛的展开并在几个领域深入。这会帮助你了解一些关于系统设计的不同的主题。基于你的时间线,经验,面试的职位和面试的公司对下面的指导做出适当的调整。
短期 | 中期 | 长期 | |
---|---|---|---|
阅读 系统设计主题 以获得一个关于系统如何工作的宽泛的认识 | :+1: | :+1: | :+1: |
阅读一些你要面试的公司工程博客的文章 | :+1: | :+1: | :+1: |
阅读 真实架构 | :+1: | :+1: | :+1: |
复习 如何处理一个系统设计面试题 | :+1: | :+1: | :+1: |
完成 系统设计的面试题和解答 | 一些 | 很多 | 大部分 |
完成 面向对象设计的面试题和解答 | 一些 | 很多 | 大部分 |
复习 其它的系统设计面试题 | 一些 | 很多 | 大部分 |
系统设计面试是一个开放式的对话。他们期望你去主导这个对话。
你可以使用下面的步骤来指引讨论。为了巩固这个过程,请使用下面的步骤完成系统设计的面试题和解答这个章节。
把所有需要的东西聚集在一起,审视问题。不停的提问,以至于我们可以明确使用场景和约束。讨论假设。
使用所有重要的组件来描绘出一个高层级的设计。
对每一个核心组件进行详细深入的分析。举例来说,如果你被问到设计一个 url 缩写服务,开始讨论:
确认和处理瓶颈以及一些限制。举例来说就是你需要下面的这些来完成扩展性的议题吗?
论述可能的解决办法和代价。每件事情需要取舍。可以使用可扩展系统的设计原则来处理瓶颈。
你或许会被要求通过手算进行一些估算。附录涉及到的是下面的这些资源:
查看下面的链接以获得我们期望的更好的想法:
普通的系统设计面试题和相关事例的论述,代码和图表。
与内容有关的解答在
solutions/
文件夹中。
常见面向对象设计面试问题及实例讨论,代码和图表演示。
与内容相关的解决方案在
solutions/
文件夹中。
注:此节还在完善中
不熟悉系统设计?
首先,你需要对一般性原则有一个基本的认识,知道它们是什么,怎样使用以及利弊。
接下来,我们将看看高阶的权衡和取舍:
记住每个方面都面临取舍和权衡。
然后,我们将深入更具体的主题,如 DNS、CDN 和负载均衡器。
如果服务性能的增长与资源的增加是成比例的,服务就是可扩展的。通常,提高性能意味着服务于更多的工作单元,另一方面,当数据集增长时,同样也可以处理更大的工作单位。1
另一个角度来看待性能与可扩展性:
延迟是执行操作或运算结果所花费的时间。
吞吐量是单位时间内(执行)此类操作或运算的数量。
通常,你应该以可接受级延迟下最大化吞吐量为目标。
在一个分布式计算系统中,只能同时满足下列的两点:
网络并不可靠,所以你应要支持分区容错性,并需要在软件可用性和一致性间做出取舍。
等待分区节点的响应可能会导致延时错误。如果你的业务需求需要原子读写,CP 是一个不错的选择。
响应节点上可用数据的最近版本可能并不是最新的。当分区解析完后,写入(操作)可能需要一些时间来传播。
如果业务需求允许最终一致性,或当有外部故障时要求系统继续运行,AP 是一个不错的选择。
有同一份数据的多份副本,我们面临着怎样同步它们的选择,以便让客户端有一致的显示数据。回想 CAP 理论中的一致性定义 ─ 每次访问都能获得最新数据但可能会收到错误响应
在写入之后,访问可能看到,也可能看不到(写入数据)。尽力优化之让其能访问最新数据。
这种方式可以 memcached 等系统中看到。弱一致性在 VoIP,视频聊天和实时多人游戏等真实用例中表现不错。打个比方,如果你在通话中丢失信号几秒钟时间,当重新连接时你是听不到这几秒钟所说的话的。
在写入后,访问最终能看到写入数据(通常在数毫秒内)。数据被异步复制。
DNS 和 email 等系统使用的是此种方式。最终一致性在高可用性系统中效果不错。
在写入后,访问立即可见。数据被同步复制。
文件系统和关系型数据库(RDBMS)中使用的是此种方式。强一致性在需要记录的系统中运作良好。
有两种支持高可用性的模式: 故障切换(fail-over)和复制(replication)。
关于工作到备用的故障切换流程是,工作服务器发送周期信号给待机中的备用服务器。如果周期信号中断,备用服务器切换成工作服务器的 IP 地址并恢复服务。
宕机时间取决于备用服务器处于“热”待机状态还是需要从“冷”待机状态进行启动。只有工作服务器处理流量。
工作到备用的故障切换也被称为主从切换。
在双工作切换中,双方都在管控流量,在它们之间分散负载。
如果是外网服务器,DNS 将需要对两方都了解。如果是内网服务器,应用程序逻辑将需要对两方都了解。
双工作切换也可以称为主主切换。
这个主题进一步探讨了数据库部分:
域名系统是把 www.example.com 等域名转换成 IP 地址。
域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于存活时间 TTL。
CNAME
记录( example.com 指向 www.example.com )或映射到一个 A
记录。CloudFlare 和 Route 53 等平台提供管理 DNS 的功能。某些 DNS 服务通过集中方式来路由流量:
内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由 CDN 提供,虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。
将内容存储在 CDN 上可以从两个方面来提供性能:
当你服务器上内容发生变动时,推送 CDN 接受新内容。直接推送给 CDN 并重写 URL 地址以指向你的内容的 CDN 地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。
CDN 拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写 URL 指向 CDN 地址。直到内容被缓存在 CDN 上为止,这样请求只会更慢,
存活时间(TTL)决定缓存多久时间。CDN 拉取方式最小化 CDN 上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。
高流量站点使用 CDN 拉取效果不错,因为只有最近请求的内容保存在 CDN 中,流量才能更平衡地分散。
负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:
负载均衡器可以通过硬件(昂贵)或 HAProxy 等软件来实现。
增加的好处包括:
通常会设置采用工作─备用 或 双工作 模式的多个负载均衡器,以免发生故障。
负载均衡器能基于多种方式来路由流量:
四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。
七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。
以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。
负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。
反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。
带来的好处包括:
将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。
单一职责原则提倡小型的,自治的服务共同合作。小团队通过提供小型的服务,可以更激进地计划增长。
应用层中的工作进程也有可以实现异步化。
与此讨论相关的话题是 微服务,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1
例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。
像 Consul,Etcd 和 Zookeeper 这样的系统可以通过追踪注册名、地址、端口等信息来帮助服务互相发现对方。Health checks 可以帮助确认服务的完整性和是否经常使用一个 HTTP 路径。Consul 和 Etcd 都有一个内建的 key-value 存储 用来存储配置信息和其他的共享信息。
像 SQL 这样的关系型数据库是一系列以表的形式组织的数据项集合。
校对注:这里作者 SQL 可能指的是 MySQL
ACID 用来描述关系型数据库事务的特性。
关系型数据库扩展包括许多技术:主从复制、主主复制、联合、分片、非规范化和 SQL调优。
主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。
两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。
联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:论坛、用户和产品,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。
类似联合的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。
非规范化试图以写入性能为代价来换取读取性能。在多个表中冗余数据副本,以避免高成本的联结操作。一些关系型数据库,比如 PostgreSQL 和 Oracle 支持物化视图,可以处理冗余信息存储和保证冗余副本一致。
当数据使用诸如联合和分片等技术被分割,进一步提高了处理跨数据中心的联结操作复杂度。非规范化可以规避这种复杂的联结操作。
在多数系统中,读取操作的频率远高于写入操作,比例可达到 100:1,甚至 1000:1。需要复杂的数据库联结的读取操作成本非常高,在磁盘操作上消耗了大量时间。
SQL 调优是一个范围很广的话题,有很多相关的书可以作为参考。
利用基准测试和性能分析来模拟和发现系统瓶颈很重要。
基准测试和性能分析可能会指引你到以下优化方案。
CHAR
类型存储固定长度的字段,不要用 VARCHAR
。CHAR
在快速、随机访问时效率很高。如果使用 VARCHAR
,如果你想读取下一个字符串,不得不先读取到当前字符串的末尾。TEXT
类型存储大块的文本,例如博客正文。TEXT
还允许布尔搜索。使用 TEXT
字段需要在磁盘上存储一个用于定位文本块的指针。INT
类型存储高达 2^32 或 40 亿的较大数字。DECIMAL
类型存储货币可以避免浮点数表示错误。BLOBS
存储实际对象,而是用来存储存放对象的位置。VARCHAR(255)
是以 8 位数字存储的最大字符数,在某些关系型数据库中,最大限度地利用字节。NOT NULL
约束来提高搜索性能。SELECT
、GROUP BY
、ORDER BY
、JOIN
)的列如果用了索引会更快。NoSQL 是键-值数据库、文档型数据库、列型数据库或图数据库的统称。数据库是非规范化的,表联结大多在应用程序代码中完成。大多数 NoSQL 无法实现真正符合 ACID 的事务,支持最终一致。
BASE 通常被用于描述 NoSQL 数据库的特性。相比 CAP 理论,BASE 强调可用性超过一致性。
除了在 SQL 还是 NoSQL 之间做选择,了解哪种类型的 NoSQL 数据库最适合你的用例也是非常有帮助的。我们将在下一节中快速了解下 键-值存储、文档型存储、列型存储和图存储数据库。
抽象模型:哈希表
键-值存储通常可以实现 O(1) 时间读写,用内存或 SSD 存储数据。数据存储可以按字典顺序维护键,从而实现键的高效检索。键-值存储可以用于存储元数据。
键-值存储性能很高,通常用于存储简单数据模型或频繁修改的数据,如存放在内存中的缓存。键-值存储提供的操作有限,如果需要更多操作,复杂度将转嫁到应用程序层面。
键-值存储是如文档存储,在某些情况下,甚至是图存储等更复杂的存储系统的基础。
抽象模型:将文档作为值的键-值存储
文档类型存储以文档(XML、JSON、二进制文件等)为中心,文档存储了指定对象的全部信息。文档存储根据文档自身的内部结构提供 API 或查询语句来实现查询。请注意,许多键-值存储数据库有用值存储元数据的特性,这也模糊了这两种存储类型的界限。
基于底层实现,文档可以根据集合、标签、元数据或者文件夹组织。尽管不同文档可以被组织在一起或者分成一组,但相互之间可能具有完全不同的字段。
MongoDB 和 CouchDB 等一些文档类型存储还提供了类似 SQL 语言的查询语句来实现复杂查询。DynamoDB 同时支持键-值存储和文档类型存储。
文档类型存储具备高度的灵活性,常用于处理偶尔变化的数据。
抽象模型:嵌套的
ColumnFamily<RowKey, Columns<ColKey, Value, Timestamp>>
映射
类型存储的基本数据单元是列(名/值对)。列可以在列族(类似于 SQL 的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。
Google 发布了第一个列型存储数据库 Bigtable,它影响了 Hadoop 生态系统中活跃的开源数据库 HBase 和 Facebook 的 Cassandra。像 BigTable,HBase 和 Cassandra 这样的存储系统将键以字母顺序存储,可以高效地读取键列。
列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。
抽象模型: 图
在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。
图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 REST API 访问。
选取 SQL 的原因:
选取 NoSQL 的原因:
适合 NoSQL 的示例数据:
缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。
数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。
缓存可以位于客户端(操作系统或者浏览器),服务端或者不同的缓存层。
CDN 也被视为一种缓存。
反向代理和缓存(比如 Varnish)可以直接提供静态和动态内容。Web 服务器同样也可以缓存请求,返回相应结果而不必连接应用服务器。
数据库的默认配置中通常包含缓存级别,针对一般用例进行了优化。调整配置,在不同情况下使用不同的模式可以进一步提高性能。
基于内存的缓存比如 Memcached 和 Redis 是应用程序和数据存储之间的一种键值存储。由于数据保存在 RAM 中,它比存储在磁盘上的典型数据库要快多了。RAM 比磁盘限制更多,所以例如 least recently used (LRU) 的缓存无效算法可以将「热门数据」放在 RAM 中,而对一些比较「冷门」的数据不做处理。
Redis 有下列附加功能:
有多个缓存级别,分为两大类:数据库查询和对象:
一般来说,你应该尽量避免基于文件的缓存,因为这使得复制和自动缩放很困难。
当你查询数据库的时候,将查询语句的哈希值与查询结果存储到缓存中。这种方法会遇到以下问题:
将您的数据视为对象,就像对待你的应用代码一样。让应用程序将数据从数据库中组合到类实例或数据结构中:
建议缓存的内容:
由于你只能在缓存中存储有限的数据,所以你需要选择一个适用于你用例的缓存更新策略。
应用从存储器读写。缓存不和存储器直接交互,应用执行以下操作:
1 | def get_user(self, user_id): |
Memcached 通常用这种方式使用。
添加到缓存中的数据读取速度很快。缓存模式也称为延迟加载。只缓存所请求的数据,这避免了没有被请求的数据占满了缓存空间。
应用使用缓存作为主要的数据存储,将数据读写到缓存中,而缓存负责从数据库中读写数据。
应用代码:
1 | set_user(12345, {"foo":"bar"}) |
缓存代码:
1 | def set_user(user_id, values): |
由于存写操作所以直写模式整体是一种很慢的操作,但是读取刚写入的数据很快。相比读取数据,用户通常比较能接受更新数据时速度较慢。缓存中的数据不会过时。
在回写模式中,应用执行以下操作:
你可以将缓存配置成在到期之前自动刷新最近访问过的内容。
如果缓存可以准确预测将来可能请求哪些数据,那么刷新可能会导致延迟与读取时间的降低。
异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。
消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:
不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。
Redis 是一个令人满意的简单的消息代理,但是消息有可能会丢失。
RabbitMQ 很受欢迎但是要求你适应「AMQP」协议并且管理你自己的节点。
Amazon SQS 是被托管的,但可能具有高延迟,并且消息可能会被传送两次。
任务队列接收任务及其相关数据,运行它们,然后传递其结果。 它们可以支持调度,并可用于在后台运行计算密集型作业。
Celery 支持调度,主要是用 Python 开发的。
如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。背压可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是指数退避。
HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。
一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:
动词 | 描述 | *幂等 | 安全性 | 可缓存 |
---|---|---|---|---|
GET | 读取资源 | Yes | Yes | Yes |
POST | 创建资源或触发处理数据的进程 | No | No | Yes,如果回应包含刷新信息 |
PUT | 创建或替换资源 | Yes | No | No |
PATCH | 部分更新资源 | No | No | Yes,如果回应包含刷新信息 |
DELETE | 删除资源 | Yes | No | No |
多次执行不会产生不同的结果。
HTTP 是依赖于较低级协议(如 TCP 和 UDP)的应用层协议。
TCP 是通过 IP 网络的面向连接的协议。 使用握手建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:
如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行流量控制和拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。
为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 memcached 服务器。连接池 可以帮助除了在适用的情况下切换到 UDP。
TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。
以下情况使用 TCP 代替 UDP:
UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。
UDP 可以通过广播将数据报发送至子网内的所有设备。这对 DHCP 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。
UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。
以下情况使用 UDP 代替 TCP:
Source: Crack the system design interview
在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 Protobuf、Thrift 和 Avro。
RPC 是一个“请求-响应”协议:
RPC 调用示例:
1 | GET /someoperation?data=anId |
RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。
当以下情况时选择本地库(也就是 SDK):
遵循 REST 的 HTTP API 往往更适用于公共 API。
REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。
RESTful 接口有四条规则:
REST 请求的例子:
1 | GET /someresources/anId |
REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,通过 header 来表述并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。
操作 | RPC | REST |
---|---|---|
注册 | POST /signup | POST /persons |
注销 | POST /resign { “personid”: “1234” } | DELETE /persons/1234 |
读取用户信息 | GET /readPerson?personid=1234 | GET /persons/1234 |
读取用户物品列表 | GET /readUsersItemsList?personid=1234 | GET /persons/1234/items |
向用户物品列表添加一项 | POST /addItemToUsersItemsList { “personid”: “1234”; “itemid”: “456” } | POST /persons/1234/items { “itemid”: “456” } |
更新一个物品 | POST /modifyItem { “itemid”: “456”; “key”: “value” } | PUT /items/456 { “key”: “value” } |
删除一个物品 | POST /removeItem { “itemid”: “456” } | DELETE /items/456 |
资料来源:你真的知道你为什么更喜欢 REST 而不是 RPC 吗
这一部分需要更多内容。一起来吧!
安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容:
一些时候你会被要求做出保守估计。比如,你可能需要估计从磁盘中生成 100 张图片的缩略图需要的时间或者一个数据结构需要多少的内存。2 的次方表和每个开发者都需要知道的一些时间数据(译注:OSChina 上有这篇文章的译文)都是一些很方便的参考资料。
1 | Power Exact Value Approx Value Bytes |
1 | Latency Comparison Numbers |
基于上述数字的指标:
常见的系统设计面试问题,给出了如何解决的方案链接
问题 | 引用 |
---|---|
设计类似于 Dropbox 的文件同步服务 | youtube.com |
设计类似于 Google 的搜索引擎 | queue.acm.org stackexchange.com ardendertat.com stanford.edu |
设计类似于 Google 的可扩展网络爬虫 | quora.com |
设计 Google 文档 | code.google.com neil.fraser.name |
设计类似 Redis 的键值存储 | slideshare.net |
设计类似 Memcached 的缓存系统 | slideshare.net |
设计类似亚马逊的推荐系统 | hulu.com ijcai13.org |
设计类似 Bitly 的短链接系统 | n00tc0d3r.blogspot.com |
设计类似 WhatsApp 的聊天应用 | highscalability.com |
设计类似 Instagram 的图片分享系统 | highscalability.com highscalability.com |
设计 Facebook 的新闻推荐方法 | quora.com quora.com slideshare.net |
设计 Facebook 的时间线系统 | facebook.com highscalability.com |
设计 Facebook 的聊天系统 | erlang-factory.com facebook.com |
设计类似 Facebook 的图表搜索系统 | facebook.com facebook.com facebook.com |
设计类似 CloudFlare 的内容传递网络 | cmu.edu |
设计类似 Twitter 的热门话题系统 | michael-noll.com snikolov .wordpress.com |
设计一个随机 ID 生成系统 | blog.twitter.com github.com |
返回一定时间段内次数前 k 高的请求 | ucsb.edu wpi.edu |
设计一个数据源于多个数据中心的服务系统 | highscalability.com |
设计一个多人网络卡牌游戏 | indieflashblog.com buildnewgames.com |
设计一个垃圾回收系统 | stuffwithstuff.com washington.edu |
添加更多的系统设计问题 | 贡献 |
关于现实中真实的系统是怎么设计的文章。
Source: Twitter timelines at scale
不要专注于以下文章的细节,专注于以下方面:
类型 | 系统 | 引用 |
---|---|---|
Data processing | MapReduce - Google的分布式数据处理 | research.google.com |
Data processing | Spark - Databricks 的分布式数据处理 | slideshare.net |
Data processing | Storm - Twitter 的分布式数据处理 | slideshare.net |
Data store | Bigtable - Google 的列式数据库 | harvard.edu |
Data store | HBase - Bigtable 的开源实现 | slideshare.net |
Data store | Cassandra - Facebook 的列式数据库 | slideshare.net |
Data store | DynamoDB - Amazon 的文档数据库 | harvard.edu |
Data store | MongoDB - 文档数据库 | slideshare.net |
Data store | Spanner - Google 的全球分布数据库 | research.google.com |
Data store | Memcached - 分布式内存缓存系统 | slideshare.net |
Data store | Redis - 能够持久化及具有值类型的分布式内存缓存系统 | slideshare.net |
File system | Google File System (GFS) - 分布式文件系统 | research.google.com |
File system | Hadoop File System (HDFS) - GFS 的开源实现 | apache.org |
Misc | Chubby - Google 的分布式系统的低耦合锁服务 | research.google.com |
Misc | Dapper - 分布式系统跟踪基础设施 | research.google.com |
Misc | Kafka - LinkedIn 的发布订阅消息系统 | slideshare.net |
Misc | Zookeeper - 集中的基础架构和协调服务 | slideshare.net |
添加更多 | 贡献 |
Company | Reference(s) |
---|---|
Amazon | Amazon 的架构 |
Cinchcast | 每天产生 1500 小时的音频 |
DataSift | 每秒实时挖掘 120000 条 tweet |
DropBox | 我们如何缩放 Dropbox |
ESPN | 每秒操作 100000 次 |
Google 的架构 | |
1400 万用户,达到兆级别的照片存储 是什么在驱动 Instagram | |
Justin.tv | Justin.Tv 的直播广播架构 |
Facebook 的可扩展 memcached TAO: Facebook 社交图的分布式数据存储 Facebook 的图片存储 | |
Flickr | Flickr 的架构 |
Mailbox | 在 6 周内从 0 到 100 万用户 |
从零到每月数十亿的浏览量 1800 万访问用户,10 倍增长,12 名员工 | |
Playfish | 月用户量 5000 万并在不断增长 |
PlentyOfFish | PlentyOfFish 的架构 |
Salesforce | 他们每天如何处理 13 亿笔交易 |
Stack Overflow | Stack Overflow 的架构 |
TripAdvisor | 40M 访问者,200M 页面浏览量,30TB 数据 |
Tumblr | 每月 150 亿的浏览量 |
Making Twitter 10000 percent faster 每天使用 MySQL 存储2.5亿条 tweet 150M 活跃用户,300K QPS,22 MB/S 的防火墙 可扩展时间表 Twitter 的大小数据 Twitter 的行为:规模超过 1 亿用户 | |
Uber | Uber 如何扩展自己的实时化市场 |
Facebook 用 190 亿美元购买 WhatsApp 的架构 | |
YouTube | YouTube 的可扩展性 YouTube 的架构 |
你即将面试的公司的架构
你面对的问题可能就来自于同样领域
有兴趣加入添加一些部分或者帮助完善某些部分吗?加入进来吧!
整个仓库都提供了证书和源
特别鸣谢:
欢迎联系我讨论本文的不足、问题或者意见。
可以在我的 GitHub 主页上找到我的联系方式
Creative Commons Attribution 4.0 International License (CC BY 4.0)http://creativecommons.org/licenses/by/4.0/
]]>
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可提高数据库中特定数据的查询速度。
索引的目的在于提高查询效率,就像书的目录一样。一本 1000 页的书,如果你想快速找到其中的某一个知识点,在不借助目录的情况下,这个事情是不是很难完成?同样,对于数据库的表而言,索引其实就是它的“目录”。
常见的索引类型有: 主键索引
、唯一索引
、普通索引
、全文索引
、组合索引
。
即主索引,根据主键 pk_clolum(length)建立索引,不允许重复,不允许空值。
1 | ALTER TABLE 'table_name' ADD PRIMARY KEY('col'); |
用来建立索引的列的值必须是唯一的,允许空值。
1 | ALTER TABLE 'table_name' ADD UNIQUE('col'); |
MySQL 中的基本索引类型,允许在定义索引的列中插入重复值和空值。
1 | ALTER TABLE 'table_name' ADD INDEX index_name('col'); |
全文索引类型为 FULLTEXT,用大文本对象的列构建的索引,在定义索引的列上支持值的全文查找,允许在这些索引的列中插入重复值和空值。全文索引可以在 CHAR、VARCHAR 或 TEXT 类型的列上创建。MySQL 中只有 MyISAM
存储引擎支持全文索引。
在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用。使用组合索引时遵循最左前缀原则,并且组合索引这多个列中的值不允许有空值。
1 | ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3'); |
注意了,遵循最左前缀原则很重要!!!把最常用作为检索或排序的列放在最左,依次递减,组合索引相当于建立了 col1、col1col2、col1col2col3 三个索引,而 col2 或者 col3 是不能使用索引的。
索引的出现是为了提高查询效率,但是实现索引的方式却有很多种,所以这里也就引入了索引模型的概念。可以用于提高读写效率的数据结构很多,这里我先给你介绍几种常见的数据结构,它们分别是哈希表、有序数组和搜索树。搜索树又可以分为二叉搜索树、平衡二叉树、B 树、B+ 树。
MySQL 中实现索引的方式主要有两种: BTREE 和 HASH,具体和表的存储引擎有关;MyISAM 和 InnoDB 存储引擎只支持 BTREE 索引,而 MEMROY/HEAP 存储引擎可以支持 HASH 和 BTREE 索引。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。这样的话,索引查找过程中就要产生磁盘 I/O 消耗,相对于内存存取,I/O 存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘 I/O 操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘 I/O 的存取次数。
MySQL 中与慢 SQL 有关的几个重要系统变量如下:
参数 | 含义 |
---|---|
slow_query_log | 是否启用慢查询日志,ON 为启用,OFF 为未启用,默认为 OFF。开启会影响性能,MySQL 重启会失效。 |
slow_query_log_file | 指定慢查询日志文件的路径和名字,缺省文件名 host_name-slow.log。 |
long_query_time | 执行时间超过该值才记录到慢查询日志,单位为秒,默认为 10。 |
log_output | 日志输出位置,默认为 FILE,即保存为文件,若设置为 TABLE,则将日志记录到 mysql.show_log 表中,支持设置多种格式。 |
执行如下语句看是否启用了慢查询日志,ON 为启用,OFF 为未启用,默认为 OFF。
1 | SHOW VARIABLES LIKE '%slow_query_log%'; |
/ˈveərɪəblz/
1 | slow_query_logOFF |
如果你的没有开启,可以使用如下两种方式来开启慢查询。
在 [mysqld] 段落在加入如下配置:
1 | [mysqld] |
如下打开慢查询日志,设置超时时间为 3 秒,并且将日志记录到文件以及 mysql.show_log 表中。
1 | SET GLOBAL slow_query_log = 1; |
注意: 设置 slow_query_log_file 时,目录必须存在,并且mysql 有权读写该目录.
1 | sudo chown mysql:mysql -R /Users/mxz/mxz-code/mysql-log |
MySQL 环境变量配置
1 | # mysql |
想要永久生效得用上面那个配置文件里配置,否则数据库重启后,这些配置失效。
查看慢查询日志记录数:
1 | SHOW GLOBAL STATUS LIKE '%Slow_queries%'; |
模拟语句:
1 | select sleep(10); |
查看日志:
1 | ➜ mysql-log sudo cat slow.log |
MySQL 内置了 mysqldumpslow 这个工具来帮我们分析慢查询日志。
1 | ➜ mysql-log mysqldumpslow -help |
1 | # 得到返回记录集最多的10个SQL |
pt-query-digest 是一款很强大的慢查询日志分析工具,可以分析 MySQL 数据库的 binary log 、 general log 日志,同时也可以使用 show processlist 或从 tcpdump 抓取的 MySQL 协议数据来进行分析。
explain 执行后输出的结果集包含 12 列,分别是 id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered 和 Extra,下面对这些字段进行解释。
id: Query Optimizer 所选定的执行计划中查询的序列号
select_type: 显示本行是简单或复杂 select。如果查询有任何复杂的子查询,则最外层标记为PRIMARY、DERIVED.、UNION、UNION RESUIT 等。
table: 显示这一步所访问的数据库中的表的名称
partitions: 查询时匹配到的分区信息,对于非分区表值为 NULL,当查询的是分区表时,partitions 显示分区表命中的分区情况。
type: 数据访问、读取操作类型(ALL、index、range、ref、eq_ref、const、system)等, 查询使用了何种类型,它在 SQL优化中是一个非常重要的指标,以下性能从好到坏依次是: system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
possible_keys: 该查询可以利用的索引,如果没有任何索引可以使用,就会显示成 null,这一项内容对于优化时候索引的调整非常重要。
key: MySQL Query Optimizer 从 possible_keys 中所选择使用的索引。区别于 possible_keys,key 是查询中实际使用到的索引,若没有使用索引,显示为 NULL。
key_len: 被选中使用索引的索引键长度,原则上长度越短越好 。
ref: 列出是通过常量(const),还是某个表的某个字段(如果是 join)来过滤(通过 key)的。
rows: MySQL Query Optimizer 通过系统收集到的统计信息估算出来的结果集记录条数
filtered: 表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。
Extra: 查询中每一步实现的额外细节信息,如 Using filesort、index 等。
]]>返回多少查多少数据
union 加个 ALL: 数据库资源比服务器资源更加珍贵,需要在代码中处理去重
Exists和in的取舍
尽量批量操作
char和varchar
多表关联数量
函数在等号右侧
善用 straight_join
like百分号向右
巧用Limit
清表用 truncate
数据类型最小可用
force index 强制化
适当索引策略
varchar长度最小可用
过滤优先一切
Xxl-Job最最核心的功能,就是任务触发的原理
任务触发原理分下面5个小点来讲解
调度中心在启动的时候,会开启一个线程,这个线程的作用就是来计算任务触发时机,这里我把这个线程称为调度线程
这个调度线程会去查询xxl_job_info
这张表
这张表存了任务的一些基本信息和任务下一次执行的时间
调度线程会去查询下一次执行的时间 <= 当前时间 + 5s的任务
这个5s是XxlJob写死的,被称为预读时间,提前读出来,保证任务能准时触发
举个例子,假设当前时间是2023-11-29 08:00:10
,这里的查询就会查出下一次任务执行时间在2023-11-29 08:00:15
之前执行的任务
查询到任务之后,调度线程会去将这些任务根据执行时间划分为三个部分:
2023-11-29 08:00:05
(不包括05s)之前的执行的任务2023-11-29 08:00:05
和2023-11-29 08:00:10
(不包括10s)之间执行的任务对于第一部分的已经超过5s以上时间的任务,会根据任务配置的**调度过期策略(页面配置:忽略、立即执行一次)**来选择要不要执行
对于第二部分的超时时间在5s以内的任务,就直接立马执行一次,之后如果判断任务下一次执行时间就在5s内,会直接放到一个时间轮里面,等待下一次触发执行
对于第三部分任务,由于还没到执行时间,所以不会立马执行,也是直接放到时间轮里面,等待触发执行
当这批任务处理完成之后,不论是前面是什么情况,调度线程都会去重新计算每个任务的下一次触发时间,然后更新xxl_job_info
这张表的下一次执行时间
到此,一次调度的计算就算完成了
之后调度线程还会继续重复上面的步骤,查任务,调度任务,更新任务下次执行时间,一直死循环下去,这就实现了任务到了执行时间就会触发的功能
这里在任务触发的时候还有一个很有意思的细节
由于调度中心可以是集群的形式,每个调度中心实例都有调度线程,那么如何保证任务在同一时间只会被其中的一个调度中心触发一次?
我猜你第一时间肯定想到分布式锁,但是怎么加呢?
XxlJob实现就比较有意思了,它是基于八股文中常说的通过数据库来实现的分布式锁的
在调度之前,调度线程会尝试执行下面这句sql
1 | preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); |
一旦执行成功,说明当前调度中心成功抢到了锁,接下来就可以执行调度任务了
当调度任务执行完之后再去关闭连接,从而释放锁
由于每次执行之前都需要去获取锁,这样就保证在调度中心集群中,同时只有一个调度中心执行调度任务
当任务达到了触发条件,并不是由调度线程直接去触发执行器的任务执行
调度线程会将这个触发的任务交给线程池去执行
所以上图中的最后一部分触发任务执行其实是线程池异步去执行的
那么,为什么要使用线程池异步呢?
主要是因为触发任务,需要通过Http接口调用具体的执行器实例去触发任务
这一过程必然会耗费时间,如果调度线程去做,就会耽误调度的效率
所以就通过异步线程去做,调度线程只负责判断任务是否需要执行
并且,Xxl-Job为了进一步优化任务的触发,将这个触发任务执行的线程池划分成快线程池和慢线程池两个线程池
1 | public void start(){ |
在调用执行器的Http接口触发任务执行的时候,Xxl-Job会去记录每个任务的触发所耗费的时间
注意并不是任务执行时间,只是整个Http请求耗时时间,这是因为执行器执行任务是异步执行的,所以整个时间不包括任务执行时间,这个后面会详细说
当任务一次触发的时间超过500ms,那么这个任务的慢次数就会加1
如果这个任务一分钟内触发的慢次数超过10次,接下来就会将触发任务交给慢线程池去执行
所以快慢线程池就是避免那种频繁触发并且每次触发时间还很长的任务阻塞其它任务的触发的情况发生
当任务需要触发的时候,调度中心会向执行器发送Http请求,执行器去执行具体的任务
那么问题来了
由于一个执行器会有很多实例,那么应该向哪个实例请求?
这其实就跟任务配置时设置的路由策略有关了
从图上可以看出xxljob支持多种路由策略
除了分片广播,其余的具体的算法实现都是通过ExecutorRouter的实现类来实现的
最不经常使用(LFU:Least Frequently Used):Xxl-Job内部会有一个缓存,统计每个任务每个地址的使用次数,每次都选择使用次数最少的地址,这个缓存每隔24小时重置一次
最近最久未使用(LRU:Least Recently Used):将地址存到LinkedHashMap中,它利用LinkedHashMap可以根据元素访问(get/put)顺序来给元素排序的特性,快速找到最近最久未使用(未访问)的节点
故障转移:调度中心都会去请求每个执行器,只要能接收到响应,说明执行器正常,那么任务就会交给这个执行器去执行
忙碌转移:调度中心也会去请求每个执行器,判断执行器是不是正在执行当前需要执行的任务(任务执行时间过长,导致上一次任务还没执行完,下一次又触发了),如果在执行,说明忙碌,不能用,否则就可以用
分片广播:XxlJob给每个执行器分配一个编号,从0开始递增,然后向所有执行器触发任务,告诉每个执行器自己的编号和总共执行器的数据
我们可以通过XxlJobHelper#getShardIndex获取到编号,XxlJobHelper#getShardTotal获取到执行器的总数据量
举个例子,比如你现在需要处理30w条数据,有3个执行器,此时使用分片广播,那么此时可将任务分成3分,每份10w条数据,执行器根据自己的编号选择对应的那份10w数据处理
当执行器接收到调度中心的请求时,会把请求交给ExecutorBizImpl来处理
所以前面提到的故障转移和忙碌转移请求执行器进行判断,最终执行器也是交给ExecutorBizImpl处理的
执行器处理触发请求是这个ExecutorBizImpl的run方法实现的
当执行器接收到请求,在正常情况下,执行器会去为这个任务创建一个单独的线程,这个线程被称为JobThread
每个任务在触发的时候都有单独的线程去执行,保证不同的任务执行互不影响
之后任务并不是直接交给线程处理的,而是直接放到一个内存队列中,线程直接从队列中获取任务
为什么不直接处理,而是交给队列,从队列中获取任务呢?
那就得讲讲不正常的情况了
如果调度中心选择的执行器实例正在处理定时任务,那么此时该怎么处理呢?**
这时就跟阻塞处理策略有关了
阻塞处理策略总共有三种:
单机串行的实现就是将任务放到队列中,由于队列是先进先出的,所以就实现串行,这也是为什么放在队列的原因
丢弃调度的实现就是执行器什么事都不用干就可以了,自然而然任务就丢了
覆盖之前调度的实现就很暴力了,他是直接重新创建一个JobThread来执行任务,并且尝试打断之前的正在处理任务的JobThread,丢弃之前队列中的任务
打断是通过Thread#interrupt方法实现的,所以正在处理的任务还是有可能继续运行,并不是说一打断正在运行的任务就终止了
这里需要注意的一点就是,阻塞处理策略是对于单个执行器上的任务来生效的,不同执行器实例上的同一个任务是互不影响的
比如说,有一个任务有两个执行器A和B,路由策略是轮询
任务第一次触发的时候选择了执行器实例A,由于任务执行时间长,任务第二次触发的时候,执行器的路由到了B,此时A的任务还在执行,但是B感知不到A的任务在执行,所以此时B就直接执行了任务
所以此时你配置的什么阻塞处理策略就没什么用了
如果业务中需要保证定时任务同一时间只有一个能运行,需要把任务路由到同一个执行器上,比如路由策略就选择第一个
当任务处理完成之后,执行器会将任务执行的结果发送给调度中心
XXL-JOB 是什么?它的主要作用是什么?
XXL-JOB 是一款分布式任务调度平台,用于解决分布式系统中的定时任务和异步任务调度问题。它提供了任务的注册、调度、执行和监控等功能,能够帮助开发者简化任务调度的复杂性,提高任务执行的准确性和稳定性。
XXL-JOB 和 Quartz 有什么区别?
答:XXL-JOB 是在 Quartz 基础上进行的封装和扩展,拥有更简单的任务调度方式、更灵活的任务管理和更强大的任务监控功能。XXL-JOB 提供了分布式任务调度和分片任务等支持,同时具备分布式任务协调和高可用调度中心的能力。
XXL-JOB 的原理是什么?请描述一下 XXL-JOB 的工作流程。
答:XXL-JOB 的原理是通过一个任务调度中心和多个任务执行器实现任务的分布式调度和执行。其工作流程如下:
任务调度中心通过任务注册接口将任务信息注册到注册中心。
任务执行器从注册中心获取任务信息,并在指定的时间点执行任务。
任务执行器执行任务后,将执行结果上报给任务调度中心。
任务调度中心根据执行结果进行任务状态的统计和管理。
XXL-JOB 支持哪些任务调度方式?请分别说明它们的优缺点。
答:XXL-JOB 支持定时任务、CRON 表达式、API 调用等任务调度方式。
定时任务:可以设置固定的时间间隔来触发任务执行,适用于周期性的任务。
CRON 表达式:可以通过配置 CRON 表达式来触发任务执行,灵活性更高,适用于复杂的时间调度需求。
API 调用:可以通过调用 API 接口来触发任务执行,可以动态灵活地管理任务的执行,适用于需要根据实际业务情况动态触发任务的场景。
XXL-JOB 的任务触发方式有哪些?如何配置触发方式?
答:XXL-JOB 的任务触发方式包括手动触发、Cron 触发和调度触发。
手动触发:任务需要手动点击触发执行。
Cron 触发:通过配置 Cron 表达式来触发任务的执行,可以设置定时、周期性的任务执行方式。
调度触发:通过调度中心的任务调度功能自动触发任务的执行,可以根据任务的优先级、依赖关系等进行调度。
触发方式可以通过在任务配置中设置相应的参数来实现。
XXL-JOB 中的任务执行器是什么?它如何配置和使用?
答:任务执行器是 XXL-JOB 执行任务的具体实体,是任务的执行者。任务执行器需要独立部署在任务执行的机器上,通过配置连接调度中心的相关信息(如注册中心地址、执行器名称等)来实现与调度中心的通信。任务执行器被调度中心调用时会触发任务的执行。
XXL-JOB 的任务失败重试是如何实现的?它支持哪些重试策略?
答:XXL-JOB 支持任务的失败重试机制。在任务配置中可以设置失败重试次数和重试间隔时间,当任务执行失败时,任务调度中心会根据设置的重试次数和间隔时间进行重试操作。支持的重试策略包括固定重试间隔、指数退避重试间隔、随机重试间隔等。
XXL-JOB 如何实现分片任务?请描述一下分片任务的原理。
答:XXL-JOB 实现了分片任务的功能,可以将一个任务分片执行,每个执行器运行其中的一片任务。
实现分片任务的原理是:
任务注册时,可以设置分片参数,指定任务分片的总数和当前执行器的分片序号。
在任务执行器获取任务时,通过判断当前执行器的分片序号来决定是否执行该任务。
执行器只会执行当前分片序号符合的任务片段,实现任务的分片执行。
XXL-JOB 如何实现任务的动态添加和删除?
答:XXL-JOB 支持动态添加和删除任务。通过调用调度中心提供的 API 接口,可以动态地注册新的任务和删除已注册的任务。添加任务时,需要提供任务的相关信息,如任务名称、任务组、执行器等。删除任务时,需要提供任务的唯一标识,可以通过任务调度中心的任务管理页面获得。
XXL-JOB 如何监控和管理任务执行情况?
答:XXL-JOB 提供了丰富的任务监控和管理功能。通过任务调度中心的任务管理页面可以查看已注册和已触发的任务,可以实时监控任务的执行情况和状态,包括任务的执行日志、耗时等信息。此外,XXL-JOB 还提供报警机制,可以根据任务的执行结果进行报警通知。
XXL-JOB 支持分布式任务调度吗?如果支持,它是如何实现的?
答:是的,XXL-JOB 支持分布式任务调度。它通过任务调度中心和多个任务执行器实现分布式任务调度。任务调度中心负责任务的注册、调度和监控,而任务执行器负责具体的任务执行。不同的任务执行器可以部署在不同的机器上,实现任务的分布式执行。
XXL-JOB 是否支持任务的依赖关系?如何配置任务的依赖关系?
答:是的,XXL-JOB 支持任务的依赖关系。通过在任务配置中设置依赖的任务ID,可以实现任务之间的依赖关系。当依赖的任务执行完毕后,才会触发当前任务的执行。
XXL-JOB 的任务执行超时如何处理?它支持哪些超时策略?
答:XXL-JOB 支持任务执行超时处理。在任务配置中可以设置任务的超时时间,如果任务执行超过设置的时间,任务调度中心会根据配置的超时策略进行相应的处理。支持的超时策略包括忽略、丢弃以及失败告警等。
XXL-JOB 是否支持任务的并行执行?如何配置任务的并行度?
答:是的,XXL-JOB 支持任务的并行执行。并行度指的是同一任务的多个分片可以同时执行。在任务注册时,可以设置任务的分片总数和当前执行器的分片序号,通过并行执行不同分片来实现任务的并行。
XXL-JOB 是否支持任务的暂停和恢复?如何实现任务的暂停和恢复?
答:是的,XXL-JOB 支持任务的暂停和恢复。在任务调度中心的任务管理页面,可以对任务进行暂停和恢复操作。暂停任务后,任务将停止调度和执行;恢复任务后,任务将重新开始调度和执行。
XXL-JOB 的任务分片策略有哪些?如何选择合适的任务分片策略?
答:XXL-JOB 提供了四种任务分片策略,包括平均分片、故障转移、一致性HASH和广播。选择合适的任务分片策略需要根据具体的业务需求来决定。比如,平均分片适用于任务执行时间相对均匀的场景,广播适用于任务需要同时在多个执行器上执行的场景等。
XXL-JOB 是否支持任务的重复执行和跳过已执行的任务?
答:是的,XXL-JOB 支持任务的重复执行和跳过已执行的任务。通过在任务配置中设置任务的重复执行次数和重复执行间隔,可以控制任务的重复执行。同时,任务调度中心会记录每次任务的执行结果和执行状态,避免重复执行已经成功执行过的任务。
XXL-JOB 的调度中心和执行器是否支持集群部署?如何实现集群部署?
答:是的,XXL-JOB 的调度中心和执行器都支持集群部署。调度中心和执行器可以通过配置相同的注册中心地址和注册中心集群模式来实现集群部署。调度中心和执行器会自动进行协调和负载均衡,实现任务的高可用性和分布式执行。
XXL-JOB 是否支持任务的日志记录和查看?如何查看任务的执行日志?
答:是的,XXL-JOB 支持任务的日志记录和查看。任务执行时,可以将任务的日志信息记录下来,并通过任务调度中心的任务管理页面查看任务的执行日志。任务执行日志包括任务的执行状态、执行结果和执行日志内容等。
XXL-JOB 是否支持任务的报警功能?如何配置任务的报警方式?
答:是的,XXL-JOB 支持任务的报警功能。在任务配置中可以设置任务的报警方式和报警接收人,当任务执行失败或超时时,任务调度中心会发送报警通知给指定的接收人。
无论是采用邮箱验证码还是短信验证码,都需要防止用户进行盗刷。手机验证码厂商会额外产生费用,而邮箱验证码虽然无需额外收费,但是被大量盗刷,带宽、连接等都被占用,仍会导致系统无法正常使用。
验证码5分钟过期,一分钟内不准重复发送。
对Redis的Key和Value进行特殊处理来实现防刷设计:
1 |
|
1 |
|
请你计算该表达式。返回一个表示表达式值的整数。
注意:
有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = [“2”,“1”,“+”,“3”,“*”]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = [“4”,“13”,“5”,“/”,“+”]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = [“10”,“6”,“9”,“3”,“+”,“-11”,““,”/“,””,“17”,“+”,“5”,“+”]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
1 | class Solution { |
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
1 | public boolean isValid(String str) { |
1 | public class Solution { |
在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (‘…’) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,‘//’)都被视为单个斜杠 ‘/’ 。 对于此问题,任何其他格式的点(例如,‘…’)均被视为文件/目录名称。
请注意,返回的 规范路径 必须遵循下述格式:
返回简化后得到的 规范路径 。
示例 1:
输入:path = “/home/”
输出:“/home”
解释:注意,最后一个目录名后面没有斜杠。
示例 2:
输入:path = “/…/”
输出:“/”
解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:
输入:path = “/home//foo/”
输出:“/home/foo”
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:
输入:path = “/a/./b/…/…/c/”
输出:“/c”
1 | public class Solution { |
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = “a”, magazine = “b”
输出:false
示例 2:
输入:ransomNote = “aa”, magazine = “ab”
输出:false
示例 3:
输入:ransomNote = “aa”, magazine = “aab”
输出:true
1 | class Solution { |
1 | class Solution { |
根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她)至少发表了 h 篇论文,并且 至少 有 h 篇论文被引用次数大于等于 h 。如果 h 有多种可能的值,h 指数 是其中最大的那个。
示例 1:
输入:citations = [3,0,6,1,5]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
示例 2:
输入:citations = [1,3,1]
输出:1
首先我们可以将初始的 H 指数 h 设为 0,然后将引用次数排序,并且对排序后的数组从大到小遍历。
根据 H 指数的定义,如果当前 H 指数为 h 并且在遍历过程中找到当前值 citations[i]>h,则说明我们找到了一篇被引用了至少 h+1 次的论文,所以将现有的 h 值加 1。继续遍历直到 h 无法继续增大。最后返回 h 作为最终答案。
1 | class Solution { |
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
注意肯定是能到最后一个元素
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
1 | /** |
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
1 | public boolean canJump(int[] nums) { |
1 | public boolean canJump(int[] nums) { |
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
1 | /** |
1 | public int maxProfit(int prices[]) { |
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
1 | class Solution { |
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
1 | class Solution { |
如果将数组 nums 中的所有元素按照单调递增或单调递减的顺序排序,那么下标为 n/2 的元素(下标从 0 开始)一定是众数。
1 | class Solution { |
这个思路很独特,感觉看脸。
因为超过 n/2 的数组下标被众数占据了,这样我们随机挑选一个下标对应的元素并验证,有很大的概率能找到众数。
1 | class Solution { |
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
1 | // nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 |
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
1 | class Solution { |
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109
1 | class Solution { |
1 | class Solution { |