应使用固定长度短码(如6位base62)作主键,原始URL建唯一哈希索引防重;高并发下预生成短码或用分布式ID+base62编码避免冲突;跳转需多级缓存(Redis+本地内存+DB),禁用MySQL直出302。

短链接映射表该用什么主键
直接用原始 URL 做主键?别试。长度超长、重复率低、写入慢,INSERT 和 SELECT 都会卡在 B+ 树索引的分裂和遍历上。
实际做法是:生成固定长度的短码(如 6 位 base62 字符串),作为主键;同时对原始 URL 建唯一索引防重复。短码可预生成或用雪花 ID + 编码,避免每次 SELECT ... FOR UPDATE 查重再插入。
-
short_code设为VARCHAR(10)主键,不加自增,启用utf8mb4_bin排序规则保证大小写敏感 -
original_url加UNIQUE INDEX,但注意 MySQL 对长文本索引有前缀限制,建议先SHA2(original_url, 256)存哈希值再建唯一索引 - 不推荐用
MD5或SHA1原值做主键——虽然定长,但十六进制字符串比较效率不如二进制,且无法直接用于跳转路径
哈希索引只在 MEMORY 引擎里真生效
你建了 INDEX idx_hash USING HASH (short_code),但在 InnoDB 表里,这只是个“假哈希”——MySQL 会默默转成 B+ 树索引,EXPLAIN 看执行计划仍是 type: ref,不是哈希查找的 type: const。
真正能用上哈希索引的场景,只有 MEMORY 表,且必须满足:等值查询、无范围条件、字段类型固定(如 VARCHAR 要设合理长度,太大会退化)。
- 短链跳转这种纯
SELECT * FROM t WHERE short_code = 'aB3xK9'场景,MEMORY表 + 哈希索引响应可压到 0.1ms 内 - 但
MEMORY表数据重启即丢,得配合持久化表双写,或用 Redis 做第一层缓存,MySQL 仅作备份存储 - InnoDB 的“哈希索引”只是自适应哈希索引(AHI),由引擎自动构建,不能手动指定,也不保证存在——
SHOW ENGINE INNODB STATUS里才能看到它是否生效
高并发下生成短码怎么避免冲突
靠 INSERT IGNORE 重试?在每秒几千 QPS 下,失败率飙升,CPU 全耗在重试和锁等待上。
更稳的做法是:预生成一批短码,写入临时表或 Redis list,服务取用时原子出队;或者用带版本号的分布式 ID 生成器(如 TinyID),再经 base62 编码,天然无冲突。
- 不要用
RAND()或UUID_SHORT()直接截取——前者分布不均易碰撞,后者时间戳部分在单机高并发下可能重复 - 如果坚持 DB 内生成,至少用
SELECT @code := LPAD(CONV(FLOOR(RAND() * POW(62,6)), 10, 62), 6, '0')加循环校验,但务必限制最大尝试次数(如 5 次),超时走降级逻辑 - 上线前压测时重点看
innodb_row_lock_waits和Handler_read_rnd_next,这两个指标飙升说明短码生成逻辑正在锁表
跳转时要不要查原 URL 再 302
要,但不能每次都查 MySQL。用户访问 /aB3xK9,最差路径是:Nginx → PHP → 连 MySQL → SELECT original_url FROM links WHERE short_code = 'aB3xK9' → 302。DB 成瓶颈是分分钟的事。
真实线上方案一定是多级缓存:Nginx 用 lua-resty-redis 查 Redis;没命中再查本地内存(如 PHP 的 APCu);最后才打 DB,并异步回填缓存。
- Redis key 建议用
short:{$code},value 存原 URL,过期时间设 7 天,比业务 TTL 略长即可 - 千万别把
302逻辑写在 MySQL 里(比如用SELECT CONCAT('Location: ', original_url)),HTTP 头必须由应用层控制,否则没法加缓存头、UA 判断、地域跳转等扩展 - 注意 Redis 和 MySQL 数据一致性:短链删除/更新时,要
DEL short:{$code}+UPDATEDB,且用 pipeline 或事务包裹,避免只删缓存没改 DB 导致脏读
短链系统看着简单,真正卡点从来不在算法,而在缓存穿透、码冲突、主从延迟导致的跳转错向——这些地方没日志、难复现,但一出就是大面积 404。










