![null 90194c74e4877cce86f07dd3d02885ad.png](https://img-blog.csdnimg.cn/img_convert/90194c74e4877cce86f07dd3d02885ad.png)
这是一个系统设计问题,要求从头开始设计一个类似于TinyURL或Bitly的URL短链接工具。我们将涵盖从设计需求、架构和组件设计到高性能扩展和安全最佳实践的各个方面。
定义范围:功能性和非功能性需求
首先,我们需要定义该系统的功能性和非功能性需求。
我们有两个功能性需求:
1.给定一个长URL时,我们必须创建一个短URL2.给定一个短URL时,我们必须将用户重定向到长URL。
![null 8efb3ead770e177093e48a6cd3edfc6d.png](https://img-blog.csdnimg.cn/img_convert/8efb3ead770e177093e48a6cd3edfc6d.png)
该服务的非功能性需求是优先考虑低延迟(快速响应)和高可用性(始终在线)。
![null f2675d737f176b25b5f647c73a9c50d0.png](https://img-blog.csdnimg.cn/img_convert/f2675d737f176b25b5f647c73a9c50d0.png)
明确业务问题
以下是一些我们可能需要明确的问题,以确保我们对系统的规模有一致的理解:
•使用情况:估计我们每秒需要创建多少个URL(假设是1000个)。•字符:我们可以使用数字和字母(字母数字)还是其他符号?(我们假设使用字母数字字符)。•唯一性:每次生成的短URL是否唯一,即使多个用户提交相同的长URL?(在这个设计中,我们假设是唯一的)。
估算:数据计算
有了这些信息,我们需要计算缩短后的URL应该有多长。当然,我们希望它尽可能短,但我们需要考虑到每年的URL创建数量。
![null 872517b32c02071d4621c99d616a8b47.png](https://img-blog.csdnimg.cn/img_convert/872517b32c02071d4621c99d616a8b47.png)
首先,让我们估算一个显著时期内所需的唯一URL数量。常见的方法是计划至少几年的运营。为了简化计算,我们假设计算10年的数据。
•一年中的秒数:每分钟60秒 × 每小时60分钟 × 每天24小时 × 每年365天 = 31.536百万秒•10年中的总秒数:31.536百万 × 10 = 315.36百万秒•10年中的总URL数:1000 × 315.36百万 = 315.36十亿个唯一URL
这意味着我们的数据库每秒需要处理1000次写入,每年将生成1000 × 60 × 60 × 24 × 365 = 31.5B个URL。如果我们假设读取次数通常是写入次数的10倍,这意味着我们每秒将获得超过10 × 1000 = 10000次读取。
现在,我们需要弄清楚多少个字符能为我们未来十年的量提供足够的唯一短URL。考虑到字符集大小为62,可以按如下计算URL标识符的长度:
•62¹ = 62个唯一URL(1个字符)•62² = 3844个唯一URL(2个字符)•…等等。
![null db38d26e128babc93ca153cc03e97e7e.png](https://img-blog.csdnimg.cn/img_convert/db38d26e128babc93ca153cc03e97e7e.png)
继续这种计算,我们看到62⁷(大约3.5万亿)是第一个大于我们预计的3150亿URL所需的值。
因此,为了支持我们未来十年的预期增长,我们的缩短URL需要至少7个字符。
高层次架构
我们的系统将有以下关键组件:
用户:用户发送他们的长URL以生成短URL,或发送短URL,我们需要将他们重定向到长URL。
负载均衡器:所有这些请求通过负载均衡器,它将流量分配到多个Web服务器实例,以确保高可用性和负载均衡。
Web服务器:这些服务器副本负责处理传入的HTTP请求。
URL短链接服务:我们还需要一个包含生成短URL、存储URL映射和检索原始URL以进行重定向的核心逻辑的URL短链接服务。
数据库:存储短URL及其长URL之间的连接。在设计数据库之前,我们需要考虑缩短URL的潜在存储需求。
每个URL将包括唯一标识符(大约7个字节)、长URL(最多100个字节)和用户元数据(估计为500个字节)。这意味着我们每个URL需要最多1000个字节。根据我们的预期量,这相当于大约315TB的数据。
![null 23157186ab03bf81acb03cd0e8fd8fb2.png](https://img-blog.csdnimg.cn/img_convert/23157186ab03bf81acb03cd0e8fd8fb2.png)
在继续之前,让我们先考虑一下单个Web服务器的API设计。
API设计
让我们定义服务的基本API操作。根据我们的功能需求,我们将使用REST API,并需要两个端点。
1. 创建短URL (POST **/urls**
)
输入:包含长URL的JSON负载 {“longUrl”: “[https://example.com/very-long-url](https://example.com/very-long-url)"}
输出:带有短URL的JSON负载 {“shortUrl”: “[https://tiny.url/3ad32p9](https://tiny.url/3ad32p9)"}
和 201 Created
状态码。
如果请求无效或格式错误,我们将返回 400 Bad Request
响应,如果请求的URL已经存在于系统中,我们将返回 409 Conflict
。
2. 重定向到长URL (GET **/urls/{shortUrlId}**
)
输入:shortUrlId
路径参数
输出:带有 301 Moved Permanently
的响应,响应体中包含新创建的短URL作为JSON { "shortUrl": "https://tiny.url/3ad32p9" }
![null 1c045a4f629571202fb52a6c402e59d8.png](https://img-blog.csdnimg.cn/img_convert/1c045a4f629571202fb52a6c402e59d8.png)
301状态码指示浏览器缓存信息,这意味着下次用户输入短URL时,浏览器会自动重定向到长URL而不需要再次访问服务器。
然而,如果你想跟踪每个请求的分析并确保它通过你的系统,可以使用302状态码。
数据库:存储短URL
下一部分是数据库层。该层存储短URL和长URL之间的映射。它应该针对快速读写操作进行优化。
模式可以很简单:短URL id的主键,以及长URL和可能的创建元数据字段。
{
"shortUrlId": "3ad32p9",
"longUrl": "https://example.com/very-long-url",
"creationDate": "2024-03-08T12:00:00Z",
"userId": "user123",
"clicks": 1023,
"metadata": {
"title": "Example Web Page",
"tags": ["example", "web", "url shortener"],
"expireDate": "2025-03-08T12:00:00Z"
},
"isActive": true
}
在这里,我们主要需要考虑数据库的读取次数。如果我们通常每秒有1000次写入,那么我们可以假设至少每秒有10到100000次读取。
在这种情况下,我们需要使用支持快速读取和写入的高性能数据库。这意味着我们需要使用NoSQL数据库(如MongoDB这样的文档存储、Cassandra这样的宽列存储或DynamoDB这样的键值存储),因为它们专门设计用于处理大量的扩展。
![null b7b9f2b5cfd9600f8d2727b973891132.png](https://img-blog.csdnimg.cn/img_convert/b7b9f2b5cfd9600f8d2727b973891132.png)
它不会是ACID兼容的,但我们不关心这一点,因为我们不会进行大量的JOIN或复杂的查询,我们不需要那些ACID规则和原子事务。
URL短链接服务
该系统的核心部分之一是URL短链接服务。该服务生成短URL,且不会在不同的长URL指向相同的短URL时引入冲突。
有多种方法可以实现这个服务;以下是其中一些:
•哈希:生成长URL的哈希,并使用其中的一部分作为标识符。然而,哈希可能导致冲突。•自增ID:使用数据库的自增ID并将其编码为一个短字符串。这确保了唯一性,但可能是可预测的。•自定义算法:设计一个自定义算法,用字符的混合来生成唯一ID,以确保唯一性和不可预测性。
例如,为了避免冲突,有一个非常简单的方法——我们可以生成
所有可能的7字符键,并将它们存储在数据库中作为键,其中键是生成的URL,值是布尔值;如果为true,则表示该URL已被使用,如果为false,则可以使用该URL创建新映射。
因此,每当用户请求生成一个键时,我们可以从这个数据库中找到一个当前未使用的URL,并将其映射到请求体中的长URL。
你认为我们在这种情况下会使用SQL还是NoSQL数据库?考虑一种场景:两个用户请求缩短他们的长URL,并且他们都被映射到这个数据库中的同一个键。
![null ec049786c8fb63a30ba8e3261e0f077a.png](https://img-blog.csdnimg.cn/img_convert/ec049786c8fb63a30ba8e3261e0f077a.png)
在这种情况下,URL将被映射到其中一个请求,另一个将被破坏。所以,我们将使用SQL,因为它具有ACID属性。我们可以为这里的每个会话创建一个事务,以在隔离中执行这些步骤,在这种情况下我们不会有这种问题。
高可用性和低延迟
我们的当前系统显然无法处理每秒1000个URL的流量。
![null d8822c6607974de089b7dbcea5d9a919.png](https://img-blog.csdnimg.cn/img_convert/d8822c6607974de089b7dbcea5d9a919.png)
缓存
为了使其更具可扩展性,我们首先需要一个缓存层(例如Redis)来缓存流行的URL,以便在内存中快速检索。
鉴于某些URL可能比其他URL访问频率更高,我们需要一种优先考虑频繁访问项的逐出策略。两种适合此场景的缓存逐出策略是:
•LRU逐出策略:首先删除最近最少访问的项目。对于URL短链接服务,这种策略非常有效,因为它确保缓存保持最新和最频繁访问的URL,这可以显著减少流行链接的访问时间。•或者基于TTL的逐出策略:为每个缓存条目分配一个固定的生存时间(TTL)。一旦条目的TTL过期,它将从缓存中移除。对于只在短时间内流行的URL,TTL策略对URL短链接服务很有用。
![null ec3a97a3ed8f57f3dba88a8afd1bb20a.png](https://img-blog.csdnimg.cn/img_convert/ec3a97a3ed8f57f3dba88a8afd1bb20a.png)
TTL还可以帮助我们自动刷新缓存内容,并可以与其他策略(如LRU)结合使用,以更有效地管理缓存。
数据库扩展:结合复制和分片
我们需要实现复制和分片策略,以确保数据库支持高可用性、容错性和可扩展性。
考虑到我们的7字符集有3.5T个唯一URL,我们可以使用基于键的分片将URL记录均匀分布在多个分片上。
假设我们将其分布在3个分片上,每个分片将存储大约1.16T个URL。这确保了随着URL数量的增长系统的可扩展性。
我们还可以在每个分片内实现主从复制,以确保高可用性和容错性。这种设置允许在节点故障时快速故障转移和恢复。
![null 000f6ddb0382262919137ae83c67ed7b.png](https://img-blog.csdnimg.cn/img_convert/000f6ddb0382262919137ae83c67ed7b.png)
另外,如果服务面向全球用户,我们可以考虑地理分片和复制,以最小化延迟并改善不同地区的用户体验。
这种组合允许服务处理大量URL缩短和重定向,并且几乎没有停机时间和快速响应时间。
![null 4cc11ab63cf98bfa9d7a9d3b2d615fd8.png](https://img-blog.csdnimg.cn/img_convert/4cc11ab63cf98bfa9d7a9d3b2d615fd8.png)
安全考虑
以下是我们服务的一些安全考虑:
•输入验证:我们必须对用户提交的每个URL进行消毒。我们必须检查有效的协议(HTTP、HTTPS等)并确保URL格式正确。这有助于防止注入攻击。•速率限制:我们可以通过限制单个源的请求次数来保护我们的服务免受DDoS攻击。可以考虑使用令牌桶算法。•监控和日志记录:需要一个强大的日志记录系统(如ELK堆栈)。它允许我们分析日志以查找瓶颈和可疑活动,并确保整体系统健康。•混淆:我们不希望轻易预测的短URL。为了阻止攻击者猜测有效链接,我们可以在生成算法中添加随机性。•链接到期:可选地,我们可以考虑允许用户为他们的短URL设置到期日期。这可以限制潜在恶意链接的生命周期。