系统设计:设计URL短链接工具

90194c74e4877cce86f07dd3d02885ad.png
1*xOHz3T_iSShM2rRIT2bljA.png

这是一个系统设计问题,要求从头开始设计一个类似于TinyURL或Bitly的URL短链接工具。我们将涵盖从设计需求、架构和组件设计到高性能扩展和安全最佳实践的各个方面。

定义范围:功能性和非功能性需求

首先,我们需要定义该系统的功能性和非功能性需求。

我们有两个功能性需求:

1.给定一个长URL时,我们必须创建一个短URL2.给定一个短URL时,我们必须将用户重定向到长URL。

8efb3ead770e177093e48a6cd3edfc6d.png
1*jAM1f_jvXWxmPVRjiweF0g.png

该服务的非功能性需求是优先考虑低延迟(快速响应)和高可用性(始终在线)。

f2675d737f176b25b5f647c73a9c50d0.png
1*jNP-rIMTYsFG6wq8Hu4cnA.png

明确业务问题

以下是一些我们可能需要明确的问题,以确保我们对系统的规模有一致的理解:

使用情况:估计我们每秒需要创建多少个URL(假设是1000个)。•字符:我们可以使用数字和字母(字母数字)还是其他符号?(我们假设使用字母数字字符)。•唯一性:每次生成的短URL是否唯一,即使多个用户提交相同的长URL?(在这个设计中,我们假设是唯一的)。

估算:数据计算

有了这些信息,我们需要计算缩短后的URL应该有多长。当然,我们希望它尽可能短,但我们需要考虑到每年的URL创建数量。

872517b32c02071d4621c99d616a8b47.png
1*KSImBkGNZ7RzwWQRZ22uNA.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个字符)•…等等。

db38d26e128babc93ca153cc03e97e7e.png
1*v92u6EyCjrSdC2G9dfdkzQ.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的数据。

23157186ab03bf81acb03cd0e8fd8fb2.png
1*mMbzPSeZtYyVyjcmi2rqVw.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" }

1c045a4f629571202fb52a6c402e59d8.png
1*wNufw9wtsv-tV0G20eDuAA.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这样的键值存储),因为它们专门设计用于处理大量的扩展。

b7b9f2b5cfd9600f8d2727b973891132.png
1*qDll9-tFr2I2OxhWO3wSHg.png

它不会是ACID兼容的,但我们不关心这一点,因为我们不会进行大量的JOIN或复杂的查询,我们不需要那些ACID规则和原子事务。

URL短链接服务

该系统的核心部分之一是URL短链接服务。该服务生成短URL,且不会在不同的长URL指向相同的短URL时引入冲突。

有多种方法可以实现这个服务;以下是其中一些:

•哈希:生成长URL的哈希,并使用其中的一部分作为标识符。然而,哈希可能导致冲突。•自增ID:使用数据库的自增ID并将其编码为一个短字符串。这确保了唯一性,但可能是可预测的。•自定义算法:设计一个自定义算法,用字符的混合来生成唯一ID,以确保唯一性和不可预测性。

例如,为了避免冲突,有一个非常简单的方法——我们可以生成

所有可能的7字符键,并将它们存储在数据库中作为键,其中键是生成的URL,值是布尔值;如果为true,则表示该URL已被使用,如果为false,则可以使用该URL创建新映射。

因此,每当用户请求生成一个键时,我们可以从这个数据库中找到一个当前未使用的URL,并将其映射到请求体中的长URL。

你认为我们在这种情况下会使用SQL还是NoSQL数据库?考虑一种场景:两个用户请求缩短他们的长URL,并且他们都被映射到这个数据库中的同一个键。

ec049786c8fb63a30ba8e3261e0f077a.png
1*b0c8CnuESVxCE2-Ux7QPqA.png

在这种情况下,URL将被映射到其中一个请求,另一个将被破坏。所以,我们将使用SQL,因为它具有ACID属性。我们可以为这里的每个会话创建一个事务,以在隔离中执行这些步骤,在这种情况下我们不会有这种问题。

高可用性和低延迟

我们的当前系统显然无法处理每秒1000个URL的流量。

d8822c6607974de089b7dbcea5d9a919.png
1*eN5OY1eSi9SnGHqgVgF9bg.png

缓存

为了使其更具可扩展性,我们首先需要一个缓存层(例如Redis)来缓存流行的URL,以便在内存中快速检索。

鉴于某些URL可能比其他URL访问频率更高,我们需要一种优先考虑频繁访问项的逐出策略。两种适合此场景的缓存逐出策略是:

LRU逐出策略:首先删除最近最少访问的项目。对于URL短链接服务,这种策略非常有效,因为它确保缓存保持最新和最频繁访问的URL,这可以显著减少流行链接的访问时间。•或者基于TTL的逐出策略:为每个缓存条目分配一个固定的生存时间(TTL)。一旦条目的TTL过期,它将从缓存中移除。对于只在短时间内流行的URL,TTL策略对URL短链接服务很有用。

ec3a97a3ed8f57f3dba88a8afd1bb20a.png
1*oDV7pndeZqmTmRrthz5a3Q.png

TTL还可以帮助我们自动刷新缓存内容,并可以与其他策略(如LRU)结合使用,以更有效地管理缓存。

数据库扩展:结合复制和分片

我们需要实现复制和分片策略,以确保数据库支持高可用性、容错性和可扩展性。

考虑到我们的7字符集有3.5T个唯一URL,我们可以使用基于键的分片将URL记录均匀分布在多个分片上。

假设我们将其分布在3个分片上,每个分片将存储大约1.16T个URL。这确保了随着URL数量的增长系统的可扩展性。

我们还可以在每个分片内实现主从复制,以确保高可用性和容错性。这种设置允许在节点故障时快速故障转移和恢复。

000f6ddb0382262919137ae83c67ed7b.png
1*68H1bEHS3eYotv9dBy9hmg.png

另外,如果服务面向全球用户,我们可以考虑地理分片和复制,以最小化延迟并改善不同地区的用户体验。

这种组合允许服务处理大量URL缩短和重定向,并且几乎没有停机时间和快速响应时间。

4cc11ab63cf98bfa9d7a9d3b2d615fd8.png
1*XHmu-wQLQwyKAHztn3zIeA.png

安全考虑

以下是我们服务的一些安全考虑:

输入验证:我们必须对用户提交的每个URL进行消毒。我们必须检查有效的协议(HTTP、HTTPS等)并确保URL格式正确。这有助于防止注入攻击。•速率限制:我们可以通过限制单个源的请求次数来保护我们的服务免受DDoS攻击。可以考虑使用令牌桶算法。•监控和日志记录:需要一个强大的日志记录系统(如ELK堆栈)。它允许我们分析日志以查找瓶颈和可疑活动,并确保整体系统健康。•混淆:我们不希望轻易预测的短URL。为了阻止攻击者猜测有效链接,我们可以在生成算法中添加随机性。•链接到期:可选地,我们可以考虑允许用户为他们的短URL设置到期日期。这可以限制潜在恶意链接的生命周期。


  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小技术君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值