短链接服务看似简单,无非是把一个长网址换成短码再跳转回去,但真正要做成一个扛得住大流量的服务,方案选型和细节处理都有讲究。核心问题只有两个:一是如何生成不重复的短码,二是如何在跳转时以极低的延迟查到对应的长网址。Redis凭借原子的自增操作和亚毫秒级的读写性能,恰好能同时解决这两个问题。本文从短码生成方案讲起,逐步展开存储设计、跳转优化和运维细节。

短码生成的三种主流方案对比
生成短码的思路主要有三种:自增ID转换进制、对长网址做哈希、以及号段预分配。三者的差别体现在唯一性保障、短码长度和实现复杂度上,需要根据业务量级来选择。
第一种是自增ID加62进制编码。利用Redis的INCR命令拿到一个全局递增的数字,再把数字转成由数字、大小写字母组成的62进制字符串。62的7次方约等于3.5万亿,也就是说7个字符的短码足够支撑绝大多数业务。这种方案的唯一性由INCR的原子性天然保证,不需要额外去重,是最省心的做法。缺点是短码是连续的,存在被恶意遍历的风险,可以在编码前对ID做一次可逆混淆,比如按固定规则打乱位序。
第二种是哈希算法方案。对长网址做MD5或MurmurHash计算,取其中若干位作为短码。优点是同一个长网址会得到相同的短码,天然去重。但哈希存在碰撞可能,需要配合Redis的SETNX命令判断该短码是否已被占用,碰撞后重算。如果业务里大量网址需要查重,还可以引入布隆过滤器先快速筛掉绝大部分重复请求,降低Redis压力。
第三种是号段预分配。应用服务器不每次都去请求Redis,而是一次性取走一段ID(比如1000个)缓存在本地内存中使用,用完再取下一段。这样把网络交互摊薄到千分之一,Redis的QPS压力大幅下降,适合创建短链的并发特别高的场景。代价是应用重启会浪费一小段未用完的号,好在号段本身成本极低。
基于自增ID方案的完整实现
综合来看,自增ID加62进制是最均衡的选择,下面给出关键实现。先定义62个字符的编码表,然后实现数字转62进制的函数:
import redis
# 62进制字符表
CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
BASE = len(CHARS)
def encode_base62(num):
"""将自增数字编码为62进制短码"""
if num == 0:
return CHARS[0]
result = []
while num > 0:
num, remainder = divmod(num, BASE)
result.append(CHARS[remainder])
return ''.join(reversed(result))
def obfuscate(num):
"""对ID做简单可逆混淆,避免短码连续可猜测"""
return (num * 1103515245 + 12345) % (2 ** 61 - 1)
def create_short_url(r, long_url):
# INCR是原子操作,并发下不会重复
seq = r.incr("short:url:counter")
code = encode_base62(obfuscate(seq))
# 用Hash存储映射,方便后续附加统计字段
r.hset(f"short:{code}", mapping={
"long_url": long_url,
"created_at": int(time.time())
})
return code存储结构上建议使用Hash而不是简单的String。String只能存长网址本身,而Hash可以在同一个key下附加创建时间、点击计数、过期时间等字段,后续做统计功能时不用改结构。短码到长网址的映射设置合理的过期时间也很重要,冷数据长期占用内存不划算,可以利用Redis的惰性删除配合定时任务做二次清理。
跳转链路的性能优化
跳转是这个服务里读压力最大的环节,一次跳转的本质就是一次GET short:code。优化读性能有几个层次可以叠加。
第一层是本地缓存。热门短链接的访问往往符合二八定律,在应用层加一个LRU本地缓存(比如容量10万条),命中时连Redis都不用访问,响应时间可以压到微秒级。本地缓存和Redis之间用短过期时间(例如60秒)保持最终一致性,即使短链被修改也能快速生效。
第二层是防穿透保护。恶意用户可能会拿不存在的短码疯狂请求,每次都打到Redis。解决办法是把“短码不存在”这个结果也缓存起来,写入一个特殊占位值并设置短TTL;或者在查询前用布隆过滤器拦截,不存在的短码直接返回404,一个几MB内存的布隆过滤器就能覆盖亿级短码的判重需求。
第三层是跳转方式的选择。301是永久重定向,浏览器会缓存结果,后续请求不再到达服务器,省流量但会丢失点击统计;302是临时重定向,每次请求都会回源,能记录访问日志。做营销分析类的短链服务通常选302,纯粹省带宽的场景选301。无论哪种,都要在响应头里带上Cache-Control控制浏览器行为,避免被无脑缓存。
// Java跳转伪代码示意
public String resolve(String code) {
String key = "short:" + code;
// 先查本地缓存
String longUrl = localCache.getIfPresent(key);
if (longUrl != null) {
return longUrl;
}
// 再查Redis
Map<Object, Object> fields = redisTemplate.opsForHash().entries(key);
if (fields.isEmpty()) {
// 缓存空值防止穿透
localCache.put(key, EMPTY_PLACEHOLDER);
return null;
}
longUrl = (String) fields.get("long_url");
localCache.put(key, longUrl);
return longUrl;
}高可用与容量规划
单点Redis一旦宕机,整个短链服务就不可用,因此生产环境至少要做主从加哨兵的部署。由于这个场景读多写少的特征非常明显,可以配置多个从节点分担读流量,主节点只负责INCR写操作。进一步还可以按短码首字母做分片,把不同前缀的key路由到不同Redis实例,容量和吞吐都能水平扩展。
内存方面可以粗略估算:一条短链映射包含短码、长网址和元数据,按平均500字节算,一亿条数据约50GB。如果长网址普遍较长,可以考虑对长网址做压缩存储,或者把冷数据下沉到MySQL,Redis只保留热点部分,通过访问时回源加载的方式维持一致性。回源操作要用SETNX加锁防止并发回源造成数据库压力突增。
最后别忘了监控。INCR的速率直接反映短链创建量,缓存命中率反映读链路健康度,Redis的内存碎片率和慢查询日志要定期查看。把这几个指标接入告警,短链接服务就能长期稳定运行了。