摘要
URL 縮短器是一個簡單卻強大的工具,它能將長 URL 轉化為更易管理的短版本。今天,我將引導你完成在 .NET 中創(chuàng)建你自己的 URL 縮短器的設計、實現(xiàn)和考慮事項。
一個 URL 縮短器 是一個簡單卻強大的工具,它能將長 URL 轉化為更易管理的短版本。這在需要在字符限制的平臺上分享鏈接或通過減少內(nèi)容雜亂以改善用戶體驗方面尤為有用。兩個流行的 URL 縮短器是 Bitly[1] 和 TinyURL[2]。設計一個 URL 縮短器是一個有趣的挑戰(zhàn),有很多有趣的問題需要解決。
但你如何在 .NET 中構建一個 URL 縮短器呢?
URL 縮短器有兩個核心功能:
• 為給定的 URL 生成唯一的代碼
• 將訪問短鏈接的用戶重定向到原始 URL
今天,我將引導你完成創(chuàng)建你自己的 URL 縮短器的設計、實現(xiàn)和考慮事項。
URL 縮短器系統(tǒng)設計
以下是我們 URL 縮短器的高級系統(tǒng)設計。我們想要暴露兩個端點。一個用于縮短長 URL,另一個根據(jù)縮短的 URL 重定向用戶。在這個示例中,縮短的 URLs 存儲在 PostgreSQL[3] 數(shù)據(jù)庫中。我們可以向系統(tǒng)中引入像 Redis[4] 這樣的分布式緩存來提高讀取性能。
我們首先需要確保有大量的短 URL。我們將為每個長 URL 分配一個唯一代碼,并使用它來生成縮短的 URL。唯一代碼的長度和字符集決定了系統(tǒng)可以生成多少個短 URL。我們將在實現(xiàn)唯一代碼生成時更詳細地討論這一點。
我們將使用隨機代碼生成策略。它易于實現(xiàn),并且碰撞率可接受地低。我們做出的權衡是增加了延遲,但我們還會探索其他選項。
數(shù)據(jù)模型
讓我們開始考慮我們將在數(shù)據(jù)庫中存儲什么。我們的數(shù)據(jù)模型很簡單。我們有一個代表系統(tǒng)中存儲的 URL 的 ShortenedUrl
類:
public class ShortenedUrl
{
public Guid Id { get; set; }
public string LongUrl { get; set; } = string.Empty;
public string ShortUrl { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public DateTime CreatedOnUtc { get; set; }
}
這個類包括原始 URL(LongUrl
)、縮短的 URL(ShortUrl
)和表示縮短的 URL 的唯一代碼(Code
)。Id
和 CreatedOnUtc
字段用于數(shù)據(jù)庫和跟蹤目的。用戶將向我們的系統(tǒng)發(fā)送唯一的 Code
,我們的系統(tǒng)將嘗試找到匹配的 LongUrl
并重定向他們。
此外,我們還將定義一個負責配置我們的實體和設置數(shù)據(jù)庫上下文的 EF ApplicationDbContext
類。我在這里做兩件事來提高性能:
唯一索引可保護我們免受并發(fā)沖突,因此我們將永遠不會在數(shù)據(jù)庫中持久化重復的 Code
值。為這個列設置最大長度節(jié)省了存儲空間,并且是在某些數(shù)據(jù)庫中為字符串列編 索引的要求。
請注意,一些數(shù)據(jù)庫以不區(qū)分大小寫的方式處理字符串。這大大減少了可用的短 URL 數(shù)量。你要配置數(shù)據(jù)庫以區(qū)分大小寫的方式處理唯一代碼。
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<ShortenedUrl> ShortenedUrls { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ShortenedUrl>(builder =>
{
builder
.Property(shortenedUrl => shortenedUrl.Code)
.HasMaxLength(ShortLinkSettings.Length);
builder
.HasIndex(shortenedUrl => shortenedUrl.Code)
.IsUnique();
});
}
}
唯一代碼生成
我們 URL 縮短器最重要的部分是為每個 URL 生成一個唯一代碼。有幾種不同的算法可以實現(xiàn)這一點。我們希望在所有可能的值中均勻分布唯一代碼。這有助于減少潛在的沖突。
我將實現(xiàn)一個具有預定義字母表的隨機、唯一代碼生成器。實現(xiàn)簡單,碰撞機會相對較低。盡管如此,還有比這更高效的解決方案,但我們稍后再討論。
讓我們定義一個 ShortLinkSettings
類,其中包含兩個常量。一個是用于定義我們將生成的未經(jīng)驗證的代碼的長度。另一個常量是我們用來生成隨機代碼的字母表。
public static class ShortLinkSettings
{
public const int Length = 7;
public const string Alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
}
字母表有 62
個字符,這為我們提供了 62^7
種可能的唯一代碼組合。
如果你想知道,這是 3,521,614,606,208
種組合。
詳細說來是:三萬五千二百一十四億六千一百四十六萬零六百零八。
這些是相當多的唯一代碼,對我們的 URL 縮短器來說已經(jīng)足夠了。
現(xiàn)在,讓我們實現(xiàn)我們的 UrlShorteningService
,它負責生成唯一代碼。此服務生成使用我們預定義的字母表指定長度的隨機字符串。它還將校驗數(shù)據(jù)庫以確保唯一性。
public class UrlShorteningService(ApplicationDbContext dbContext)
{
private readonly Random _random = new();
public async Task<string> GenerateUniqueCode()
{
var codeChars = new char[ShortLinkSettings.Length];
const int maxValue = ShortLinkSettings.Alphabet.Length;
while (true)
{
for (var i = 0; i < ShortLinkSettings.Length; i++)
{
var randomIndex = _random.Next(maxValue);
codeChars[i] = ShortLinkSettings.Alphabet[randomIndex];
}
var code = new string(codeChars);
if (!await dbContext.ShortenedUrls.AnyAsync(s => s.Code == code))
{
return code;
}
}
}
}
缺點和改進點
這種實現(xiàn)的缺點是增加了延遲,因為我們正在生成的每個代碼都需要與數(shù)據(jù)庫進行檢查。一個改進點可以是提前在數(shù)據(jù)庫中生成唯一代碼。
另一個改進點可以是使用固定數(shù)量的迭代而不是無限循環(huán)。在連續(xù)多次發(fā)生碰撞的情況下,當前實現(xiàn)會繼續(xù)執(zhí)行,直到找到一個唯一值??紤]在連續(xù)幾次碰撞后拋出異常而不是繼續(xù)執(zhí)行。
URL 縮短
現(xiàn)在我們的核心業(yè)務邏輯已經(jīng)準備好,我們可以暴露一個端點來縮短 URL。我們可以使用一個簡單的 Minimal API 端點。
這個端點接受一個 URL,驗證它,然后使用 UrlShorteningService
創(chuàng)建一個縮短的 URL,然后將其保存到數(shù)據(jù)庫中。我們將完整的縮短 URL 返回給客戶端。
public record ShortenUrlRequest(string Url);
app.MapPost("shorten", async (
ShortenUrlRequest request,
UrlShorteningService urlShorteningService,
ApplicationDbContext dbContext,
HttpContext httpContext) =>
{
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out _))
{
return Results.BadRequest("The specified URL is invalid.");
}
var code = await urlShorteningService.GenerateUniqueCode();
var request = httpContext.Request;
var shortenedUrl = new ShortenedUrl
{
Id = Guid.NewGuid(),
LongUrl = request.Url,
Code = code,
ShortUrl = $"{request.Scheme}://{request.Host}/{code}",
CreatedOnUtc = DateTime.UtcNow
};
dbContext.ShortenedUrls.Add(shortenedUrl);
await dbContext.SaveChangesAsync();
return Results.Ok(shortenedUrl.ShortUrl);
});
這里有一個小的競態(tài)條件[5],因為我們首先生成一個唯一代碼,然后將其插入數(shù)據(jù)庫。一個并發(fā)請求可能會生成同一個唯一代碼,并在我們完成交易之前將其插入數(shù)據(jù)庫。然而,發(fā)生這種情況的可能性很低,所以我決定不處理這種情況。
記住,數(shù)據(jù)庫中的唯一索引仍然保護我們免受重復值的影響。
URL 重定向
URL 縮短器的第二個用例是訪問縮短的 URL 時進行重定向。
我們將為此功能暴露另一個 Minimal API 端點。該端點將接受一個唯一代碼,找到相應的縮短 URL,然后將用戶重定向到原始長 URL。在檢查數(shù)據(jù)庫中是否有縮短的 URL 之前,你可以為指定的代碼實現(xiàn)額外的驗證。
app.MapGet("{code}", async (string code, ApplicationDbContext dbContext) =>
{
var shortenedUrl = await dbContext
.ShortenedUrls
.SingleOrDefaultAsync(s => s.Code == code);
if (shortenedUrl is null)
{
return Results.NotFound();
}
return Results.Redirect(shortenedUrl.LongUrl);
});
此端點在數(shù)據(jù)庫中查找代碼,如果找到,將用戶重定向到原始的長 URL。根據(jù) HTTP 標準,響應將有一個 302(已找到)狀態(tài)碼[6]。
URL 縮短器改進點
雖然我們基本的 URL 縮短器是功能性的,但有幾個領域可以改進:
• 緩存:實現(xiàn)緩存以減少頻繁訪問的 URL 對數(shù)據(jù)庫的負載。
• 水平擴展:設計系統(tǒng)以水平擴展以處理增加的負載。
• 數(shù)據(jù)分片:實現(xiàn)數(shù)據(jù)分片以將數(shù)據(jù)分布到多個數(shù)據(jù)庫。
• 分析:引入分析以跟蹤 URL 使用情況并向用戶展示報告。
• 用戶賬號:允許用戶創(chuàng)建賬號來管理他們的 URL。
我們已經(jīng)介紹了使用 .NET 構建 URL 縮短器的關鍵組件。你可以進一步實現(xiàn)改進點以得到更健壯的解決方案。
如果你想看我從頭開始構建它,這里有一個 YouTube 視頻教程。[7]
引用鏈接
[1]
Bitly: https://bitly.com/
[2]
TinyURL: https://tinyurl.com/app
[3]
PostgreSQL: https://www.postgresql.org/
[4]
Redis: https://redis.io/
[5]
競態(tài)條件: https://www.milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking
[6]
302(已找到)狀態(tài)碼: https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.3
[7]
YouTube 視頻教程。: https://youtu.be/2UoA_PoEvuA
該文章在 2024/4/9 22:25:54 編輯過