短链接系统听起来没什么复杂的——一个短码对应一条长链接,仅此而已。可一旦日生成量冲到百万级,事情就完全是另一回事了。活动运营正推得热火朝天,短链接却越生成越慢,数据库CPU飙到90%,用户点开短链要干等3秒才能跳转。排查一圈下来,根子往往出在存储层:映射表的数据量迈过千万级之后,传统的单表设计就成了绕不过去的坎。想在十亿级数据量下仍然保持毫秒级响应,常见的做法是用哈希分表把数据打散到一两百张物理表,再在前面架一层Redis缓存,拦掉80%左右的请求。

单表为什么撑不住?先看查询。映射表超过500万行之后,查询耗时会从20ms涨到200ms,背后是B+树索引的层级随着数据量加深,每次查询的磁盘IO次数跟着翻倍。写入同样不好受:插入操作会触发索引重建,一次插入可能锁住整张表0.5秒,放在高并发场景里,这几乎是灾难。
容量方面也有物理上限。MySQL单表一般建议控制在2000万行以内,真到了这个量级,加字段、建索引这类常规操作都会造成长时间锁表,想调整一下结构都成了奢望。更棘手的其实是并发。算一笔账:假如每天生成100万条记录,每条平均被访问5次,数据库一天就要扛100万次写入加500万次查询,而这些请求全部砸在同一张表上。就算配了主从复制,读请求可以分流到从库,主库的写入压力依然无处分摊。
出路是分表,而第一步是算清楚要分多少张。先估算未来一年的数据量:日均生成50万条,一年下来就是1.8亿。按单表800万行的最优容量算,至少需要23张表。再留出扩展余量,实际一般会直接上64张或128张——取2的幂次,后面扩容方便。表名按url_map_00到url_map_63这样的格式排下去即可。
表的数量定了,接下来的问题是:数据该写进哪张表?答案是哈希路由。对短码做哈希运算,再对分表数量取模。举个例子,短码aB3xY9算出哈希值1234567890,对64取模得到26,这条数据就进url_map_26表。这样做最大的好处是,同一个短码的所有读写永远落在同一张表里,不会出现跨表事务的麻烦。
表结构则要精简到极致。每张分表只保留四个字段:short_code(短码,CHAR(6),兼作主键)、long_url(长链接,VARCHAR(2048))、created_at和expired_at(创建与过期时间,都用INT存时间戳)。有一点要特别注意:别给long_url建索引。如果确实有反查长链接的需求,单独建一张辅助表来承担。

数据库前面还得挡一层缓存。用户点击短链接时,系统先查Redis:Key是短码,Value是长链接,命中就直接返回,没命中再回源数据库,查到后把结果写进缓存,过期时间设1小时。就靠这一层,能拦下80%以上的数据库请求。

真正落地的时候,有几个细节容易被忽略。短码字段建议用CHAR(6)而不是VARCHAR(6),固定长度字段在MySQL里存储更紧凑,查询性能能提升10%到15%。时间字段用INT存Unix时间戳,只占4个字节,而DATETIME要占8个。别小看这4字节的差距,放到亿级数据量下,就是几十GB的存储空间。
另外有三个常见的设计错误值得点名。第一,在长链接字段上建索引,写入性能直接下降60%以上;第二,拿自增ID当主键,分布式环境下必然产生冲突,正确的做法是让短码本身充当主键;第三,把访问统计塞进映射表,高并发下会产生大量行锁——统计数据应该单独建表,或者干脆写入日志系统异步处理。
扩容怎么办?推荐倍增策略,比如从64张扩到128张。具体做法是:先创建新表、修改路由规则,再启动数据迁移任务;迁移期间,读请求同时查新旧两张表,写请求直接写入新表,整个过程平滑过渡,业务无感。
最后聊聊什么时候该分表。日生成量在1万以下,单表完全够用,没必要过度设计;超过10万,或者预计半年内数据量会突破500万行,就该把分表方案提上日程了。至于中小规模的需求——比如月生成量10万以内——其实也不用自己造轮子,用快缩短网址这类在线工具就行,它提供API接口,支持批量生成,接进去就能用。
今すぐログイン