如何设计短链接系统
短链接平台是一种将较长的网址转换为较短形式的在线服务工具,主要用于分享和流量采集分析,具有较高的时效性。一个短链接系统核心架构有两个功能:短链接生成、短链接跳转,是一个典型的读多写少的系统。
短链接生成
短链接一般6个字符以下,使用Base62编码(数字加字母大小写),本质是是一个UID,用于单向映射一个长连接,所以短链接的生成算法,本质是一个32位UID的生成算法。
考虑分布式系统下的UID生成方案,主流有两种:Hash摘要或分布式ID。接下来对比说明两种算法:
对比维度 | Hash摘要算法 | 分布式ID生成 |
---|---|---|
运维成本 | 节点本地运算,没有网络通信开销。 | 需要额外运维一个分布式ID生成中间服务,并且要求高性能和高可用。 |
ID枯竭 | 均匀的随机性是Hash算法的基本特征,上限为2^32,可视为不会枯竭。 | 分布式ID常常用于永久存储的资源的ID生成,保证ID整体的递增性和不会重复生成。这两种特征导致可用ID会越来越少,理论上会存在ID枯竭,实际上很少见。 |
ID冲突 | 取决于Hash算法的冲突概率,理论上存在冲突。如果一个用户对同一个长连接生成多个短链接,直接的Hash摘要会存在严重的冲突问题,需要额外解决。 | 成熟的方案不存在ID冲突。 |
短链接一般在营销活动中使用,常见于短信营销和群聊转发,具有极高的时效性,活动过期即失效。基于这个特征,更倾向于使用Hash摘要算法生成短链接,节省运营开销,且均匀随机性和Hash冲突一定程度上保证了对ID资源的利用率。
使用Hash算法生成短链接的基本流程为:上传原始链接,加上用户信息、时间戳或者随机数,使用Hash算法生成Base62短链接,判断是否存在ID冲突,有ID冲突则重新生成,设置重试次数。
坑点:需要注意Hash算法的选择和计算因子的选择,由于一个长链接可能会生成多个短链接(比如一个活动链接可能需要生成多个短链接用于不同的营销渠道),所以Hash计算时需要加上一些随机变量用于避免哈希冲突。
当然,对于财大气粗的系统,分布式ID是一个更方便的方案,首先不需要考虑ID冲突的问题,其次分布式ID在部分情况下也可以作为订单号进行唯一标识和记录。
短链接存储
短链接发布表设计如下:
CREATE TABLE short_url (
id BIGINT AUTO_INCREMENT COMMENT '主键ID',
short_code VARCHAR(6) NOT NULL COMMENT '短链接编码',
original_url TEXT NOT NULL COMMENT '原始链接',
user_id INT NOT NULL COMMENT '创建用户',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
expire_time DATETIME NOT NULL COMMENT '过期时间',
PRIMARY KEY (id),
UNIQUE KEY unique_short_code (short_code),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短链接映射表';
用于发布有效的短链接,设置定时任务对表中过期的数据进行清除。
坑点:MySQL的DELETE其实是逻辑删除,所以隔一段时间需要Optimize一下。
架构优化
基础的功能其实已经实现完成,但对于这样一个典型的有高并发读要求的系统,性能瓶颈在数据库上是不合适的,所以需要加入一些中间件优化。对于高并发读的要求,我们引入Redis作为缓存层,缓存有效的短链接。
Redis缓存采用冷启动的缓存方案:生成的短链接不会第一时间缓存入Redis,而是在第一次访问的时候,缓存入Redis。支持提前预热,也可以人工进行预热。
需要解决的问题:
-
缓存击穿:热点数据大量访问的时候,由于是冷启动或者缓存已过期,大量请求击穿Redis到达数据库,导致数据压力上升。解决方案:使用分布式锁加 Double Check 的方案,首先以短链接为分布式锁的Key枪锁,抢到后再次查询缓存中是否有记录,没有记录再读数据库,再写缓存。
-
缓存穿透:无效短链接会不断直接穿过Redis访问到数据库,导致数据库压力上升。解决方案:采用布隆过滤器(或者布谷鸟过滤器)加缓存空值的方案。首先在短链接生成时将短链接存入布隆过滤器,当一个没有缓存记录的空链接请求到达时,先查询布隆过滤器,没有记录直接返回(一级缓存);如果布隆过滤器中有记录,再查询空值缓存,有记录则直接返回(二级缓存);最后查询数据库,分布式锁加 Double Check 写入空值缓存记录。
-
缓存雪崩:大量缓存记录同时过期,导致Redis性能下降。解决方案:基于实践经验,短链接80%的访问都集中在前20%的时间,所以在缓存时采用随机过期时间,缓存中的记录过期时间随机为短链接有效时间的20%~25%。
-
空值覆盖:新生成的短链接刚好在已经存在空值缓存中,导致新生成的短链接无法访问。解决方案:由于这个概率太低了,去特别做优化不太合理,设计为对空值缓存的过期时间限制在1~3分钟左右,允许略微降低用户的体验。
-
布隆过滤器无法删除记录:虽然在系统中短链接过期,但在布隆过滤器中已过期的记录无法删除,严重影响过滤器性能。解决方案:可以换用布谷鸟过滤器。如果要使用Redis原生的布隆过滤器,可以采用新旧过滤器循环的机制:如果短链接的最长过期时间为15天,则以15天为周期,记两个过滤器为BF-0和BF-1,当BF-0使用超过15天时,则删除前一个旧过滤器BF-1,将BF-0设置为BF-1,并新增过滤器BF-0。写入时采用双写,判断以BF-1为标准。保证所有有效短链接都被记录。
在上述架构下的短链接读写流程为:
短链接生成:原始链接生成短链接,短链接在布隆过滤器中判断(需要调整布隆过滤器的误判率,如果误判率太高了容易浪费性能)是否存在,不存在则写入数据库,存在则进行重试。如果布隆过滤器中不存在,而写入数据库失败,抛出异常。
短链接跳转:短链接跳转请求到达,首先查询Redis中缓存记录用于快速响应。没有记录则执行空值查询逻辑,最后执行写缓存逻辑。