公司的业务中有用到过短链接服务,那为什么要用短链接服务呢?
业务中导购要把商品链接发到朋友圈,客户点击导购的链接业绩是要计算到导购身上的,怎么算到导购身上的?靠的是商品链接后边拼接的很多参数,而且业务中的店铺,是连锁体系,又有进店逻辑等参数,导致这个商品链接很长很长;形如
https://detail.tmall.com/item.htm?id=588065026246&ali_refid=a3_420434_1006:1200600091:N:LO74Bc2EWLnfvdjOI7AADg==:9c1ecb74e4281f85a3d6f443db4a7f28&ali_trackid=1_9c1ecb74e4281f85a3d6f443db4a7f28&spm=a230r.1.1957635.2
这个是我从天猫的商品链接中cv的,我们的业务比这个还要长
经过短链转换,链接形如
http://short.youzan.com/avf25a
这个发出来就干净利落,简洁
官方的说法是
URL 缩短用于优化跨设备链接、跟踪单个链接以分析受众和活动绩效以及隐藏附属的原始 URL 等
找到了一个百度的短链接服务(不是广告。。。) 大部分公司是自己内部有短链接系统,百度这种已经拿出来商业化了,
看了下功能,有些是我们用不上的,
我们看下设计一个我们公司内部的短链接都需要哪些功能
系统的要求和目标
功能性要求
- 给我一个URL,我换你一个更短且唯一的url;
- 用户访问我们的短url时,重定向到原始url;
- 短链接应该有一个过期时间,用户可以指定; 4.同一个url,不同的人,或指定的过期时间不同,生成的短地址是不同的;
非功能要求
1 高可用,高性能; 2 url不能被猜测出来,(无序);
##非必需功能 可视化大盘,大数据闭环;访问分析,流量分析
容量估计
我们的系统会读取繁重;与新的 URL 缩短相比,会有很多重定向请求。让我们假设读写之间的比率为 100:1。
流量估算: 如果我们假设我们每个月会有 500M 的新 URL 缩短,我们可以预期在同一时间有 (100 * 500M => 50B) 重定向。我们系统的每秒查询数 (QPS) 是多少?
每秒新 URL 缩短:
5 亿 / (30 天 * 24 小时 * 3600 秒) ~= 200 URLs/s
每秒 URL 重定向:
500 亿 / (30 天 * 24 小时 * 3600 秒) ~= 19K/s
存储估计: 因为我们预计每个月会有 5 亿个新 URL,如果我们将这些对象保留 5 年;我们将存储的对象总数将是 300 亿个。
5亿5年12个月=300亿
假设我们存储的每个对象可以是 500 字节(只是一个大概,我们稍后会深入研究);我们需要 15TB 的总存储空间:
300 亿 * 500 字节 = 15 TB 带宽估计: 对于写入请求,由于我们预计每秒有 200 个新 URL,因此我们服务的总传入数据将是每秒 100KB。
200 * 500 字节 = 100 KB/s
对于读取请求,由于我们预计每秒有大约 19K URL 重定向,因此我们服务的总传出数据将是每秒 9MB。
19K * 500 字节 ~= 9 MB/s
内存估计: 如果我们想缓存一些经常访问的热门 URL,我们需要多少内存来存储它们?如果我们遵循 80-20 规则,即 20% 的 URL 产生 80% 的流量,我们希望缓存这 20% 的热门 URL。
由于我们每秒有 19K 请求,我们每天将收到 17 亿个请求。
19K * 3600 秒 * 24 小时 ~= 17 亿
要缓存 20% 的这些请求,我们需要 170GB 的内存。
0.2 * 17 亿 * 500 字节 ~= 170GB
思考: 不一定要这么多内存,通常来讲链接在生成之后的几天会被访问的几率大,生成时间久的访问概率小,可以简单采用lru策略就好了;
高水平估计: 假设每月有 5 亿个新 URL 和 100:1 的读写比率,以下是我们服务的高级估计摘要:
| 标题 | |
|---|---|
| 新网址 | 200/s |
| 重定向 | 19k/s |
| 传入数据 | 100k/s |
| 传出数据 | 9MB/s |
| 存储5年 | 15TB |
| 缓存 | 170GB |
系统api
public boolean creatURL(String apiKey,String originUrl, Data expireDate,String userName);
apiKey:注册账号的API开发者密钥。除其他外,这将用于根据分配的配额限制用户; originUrl(字符串):要缩短的原始 URL; userName(字符串):用于编码的可选用户名; expireDate (Date): 缩短 URL 的可选过期日期;
apiKey,是我们分配给业务方使用的key,可以做鉴权,限流,接入审批
算法
我们可以计算一个唯一的哈希(例如,MD5要么SHA256等)的给定 URL。然后可以对散列进行编码以进行显示。这种编码可以是 base36 ([az ,0-9]) 或 base62 ([AZ, az, 0-9]),如果我们添加 '-' 和 '.',我们可以使用 base64 编码。一个合理的问题是;短键的长度应该是多少?6、8 还是 10 个字符?
使用 base64 编码,一个 6 字母长的密钥将产生 64^6 ~= 687 亿个可能的字符串
使用 base64 编码,一个 8 字母长的密钥将产生 64^8 ~= 281 万亿个可能的字符串
对于 68.7B 个唯一字符串,假设我们的系统使用六个字母键就足够了。
如果我们使用 MD5 算法作为我们的散列函数,它将产生一个 128 位的散列值。经过base64编码后,我们会得到一个超过20个字符的字符串,那么我们将如何选择我们的密钥呢?我们可以将前 6 个(或 8 个)字母作为键。不过,这可能会导致密钥重复,我们可以在此基础上从编码字符串中选择一些其他字符或交换一些字符。
我们的解决方案有哪些不同的问题? 我们的编码方案存在以下几个问题:
- 如果多个用户输入相同的 URL,他们可以获得相同的缩短 URL,这是不可接受的。
- 如果 URL 的某些部分是 URL 编码的怎么办?例如,www.education.io/distributed…, 和www.education.io/distributed…除了 URL 编码之外,其他都是相同的。
问题的解决方法: 我们可以将一个递增的序列号附加到每个输入 URL 以使其唯一,然后生成它的哈希。不过,我们不需要将此序列号存储在数据库中。这种方法的可能问题是这个序列号有多大,它会溢出吗?附加增加的序列号也会影响服务的性能。
另一种解决方案可能是将用户 ID(应该是唯一的)附加到输入 URL。但是,如果用户尚未登录,我们可以要求用户选择唯一性密钥。即使在这之后,如果我们有冲突,我们也必须不断生成一个密钥,直到我们得到一个唯一的。
我们可以有一个独立的密钥生成服务 (KGS),它预先生成随机的六个字母字符串并将它们存储在数据库中(我们称之为 key-DB)。每当我们想缩短一个 URL 时,我们将只取一个已经生成的密钥并使用它。这种方法将使事情变得非常简单和快速,因为我们不会对 URL 进行编码或担心重复或冲突。KGS 将确保插入到 key-DB 中的所有密钥都是唯一的。
并发会导致问题吗? 一旦使用了密钥,就应该在数据库中对其进行标记,以免再次使用它。如果有多个服务器同时读取密钥,我们可能会遇到两个或更多服务器尝试从数据库中读取相同密钥的情况。我们如何解决这个并发问题?
服务器可以使用 KGS 读取/标记数据库中的密钥。KGS 可以使用两张表来存储密钥,一张用于存储尚未使用的密钥,一张用于存储所有已使用的密钥。一旦 KGS 将密钥提供给其中一台服务器,它就可以将它们移动到已使用的密钥表中。KGS 可以始终将一些密钥保存在内存中,以便在服务器需要它们时,它可以快速提供它们。为简单起见,一旦 KGS 在内存中加载了一些键,它就可以将它们移动到已使用的键表中。这样我们可以确保每个服务器都获得唯一的密钥。如果 KGS 在将所有加载的密钥分配给某个服务器之前死了,我们将浪费这些密钥,考虑到我们拥有大量密钥,我们可以忽略它们。KGS 还必须确保不要将相同的密钥提供给多个服务器。 另一种方案是维护一个指针查询kgs使用到的位置,每次加锁读到内存中,更改指针,一次多读一些到内存,比如1000个; 读到内存中后使用起来也很麻烦,并发生成短链接时还是要加锁,容易成为瓶颈,我们用的就是加锁的方案,不太好,可以优化到放redis,list模仿队列,使用时从队列里边拉key,就不会有并发问题了
密钥数据库的大小是多少? 使用 base64 编码,我们可以生成 68.7B 的唯一六字母密钥。如果我们需要一个字节来存储一个字母数字字符,我们可以将所有这些键存储在:
6(每个键的字符)* 68.7B(唯一键)=> 412 GB。
KGS 不是单点故障吗? 是的。为了解决这个问题,我们可以有一个 KGS 的备用副本,当主服务器死机时,它可以接管生成和提供密钥。
每个应用服务器都可以缓存来自 key-DB 的一些键吗? 是的,这肯定可以加快速度。尽管在这种情况下,如果应用服务器在使用完所有密钥之前就死了,我们最终会丢失这些密钥。这是可以接受的,因为我们有 68B 个唯一的六个字母键。
我们将如何执行密钥查找? 我们可以在我们的数据库或键值存储中查找键以获取完整的 URL。如果存在,则向浏览器发出“HTTP 302 重定向”状态,将存储的 URL 传递到请求的“位置”字段中。如果我们的系统中不存在该密钥,请发出“HTTP 404 Not Found”状态,或将用户重定向回主页。
我们应该对自定义别名施加大小限制吗? 由于我们的服务支持自定义别名,用户可以选择他们喜欢的任何“键”,但提供自定义别名不是强制性的。但是,对自定义别名施加大小限制是合理的(并且通常是可取的),以便我们拥有一致的 URL 数据库。假设用户可以指定最多 16 个字符长的客户密钥(如上面的数据库架构中所反映的)。
7. 数据分区和复制
为了扩展我们的数据库,我们需要对其进行分区,以便它可以存储有关数十亿 URL 的信息。我们需要提出一个分区方案,将我们的数据划分并存储到不同的数据库服务器。
一种。基于范围的分区: 我们可以根据 URL 的第一个字母或哈希键将 URL 存储在单独的分区中。因此,我们将所有以字母“A”开头的 URL 保存在一个分区中,将那些以字母“B”开头的 URL 保存到另一个分区中,依此类推。这种方法称为基于范围的分区。我们甚至可以将某些不太频繁出现的字母组合到一个数据库分区中。我们应该静态地提出这种分区方案,以便我们始终可以以可预测的方式存储/查找文件。例如,这种方法的主要问题是它可能导致服务器不平衡;如果我们决定将所有以字母“E”开头的 URL 放入一个 DB 分区,但后来我们意识到我们有太多以字母“E”开头的 URL,我们无法将它们放入一个 DB 分区。
基于哈希的分区: 在这个方案中,我们获取我们正在存储的对象的哈希值,并根据这个哈希值计算出这个对象应该去的 DB 分区。在我们的例子中,我们可以使用“key”或实际 URL 的哈希值来确定存储文件的分区。我们的散列函数会将 URL 随机分布到不同的分区中,例如,我们的散列函数总是可以将任何键映射到 [1…256] 之间的数字,而这个数字将代表存储对象的分区。 这种方法仍然会导致分区过载,可以通过使用来解决一致的哈希. 8. 缓存
我们可以缓存经常访问的 URL。我们可以使用一些现成的解决方案,比如 Memcache,它可以存储完整的 URL 及其各自的哈希值。应用服务器在访问后端存储之前,可以快速检查缓存是否具有所需的 URL。
我们应该有多少缓存? 我们可以从每天 20% 的流量开始,根据客户的使用模式,我们可以调整我们需要多少缓存服务器。如上所述,我们需要 170GB 内存来缓存 20% 的日常流量,因为现代服务器可以有 256GB 内存,我们可以轻松地将所有缓存放入一台机器,或者我们可以选择使用几个较小的服务器来存储所有这些热门网址。
哪种缓存驱逐策略最适合我们的需求? 当缓存已满,并且我们想用更新/更热的 URL 替换链接时,我们将如何选择?最近最少使用 (LRU) 可能是我们系统的合理策略。根据此政策,我们会首先丢弃最近最少使用的 URL。我们可以使用一个链接哈希图或类似的数据结构来存储我们的 URL 和哈希,它还将跟踪最近访问的 URL。
为了进一步提高效率,我们可以复制我们的缓存服务器以在它们之间分配负载。
如何更新每个缓存副本? 每当缓存未命中时,我们的服务器就会访问后端数据库。每当发生这种情况时,我们都可以更新缓存并将新条目传递给所有缓存副本。每个副本都可以通过添加新条目来更新其缓存。如果副本已经有该条目,它可以简单地忽略它。
9. 负载均衡器( LB )
我们可以在系统的三个地方添加负载均衡层:
- 客户端和应用服务器之间
- 应用服务器和数据库服务器之间
- 应用服务器和缓存服务器之间
最初,可以采用简单的循环方法;在后端服务器之间平均分配传入请求。这个 LB 实现简单,不会引入任何开销。这种方法的另一个好处是,如果服务器死了,LB 将把它从轮换中取出,并停止向它发送任何流量。Round Robin LB 的一个问题是,它不会考虑服务器负载。如果服务器过载或速度慢,LB 将不会停止向该服务器发送新请求。为了解决这个问题,可以放置一个更智能的 LB 解决方案,它会定期向后端服务器查询其负载并据此调整流量。
清理数据
引入redis的懒惰删除的思想 查询时发现过期了再删除 另外加个定时任务扫描删除 用户能否创建私有 URL 或允许一组特定用户访问 URL?
权限
我们可以在数据库中存储每个 URL 的权限级别(公共/私有)。我们还可以创建一个单独的表来存储有权查看特定 URL 的 UserID。如果用户没有权限并尝试访问 URL,我们可以发回错误 (HTTP 401)。鉴于此,我们将数据存储在像 Cassandra 这样的 NoSQL 宽列数据库中,表存储权限的键是“哈希”(或 KGS 生成的“键”),列将存储这些用户的用户 ID有权查看 URL 的用户。