news 2025/12/14 8:31:15

【后端】【工具】短信短链接如何做到“永不丢失“?从哈希冲突到百万QPS的可靠性设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【后端】【工具】短信短链接如何做到“永不丢失“?从哈希冲突到百万QPS的可靠性设计

📖目录

  • 1. 快递单号之谜:为什么6位码能精准送达你的包裹?
  • 2. 短链接的本质:不是"压缩",而是"全局登记簿"
    • 2.1 生活化类比:快递单号 vs 短链接(深度扩展)
    • 2.2 技术架构全景图
  • 3. ID生成算法:如何避免"撞单号"?
    • 3.1 为什么不用自增ID?——单点瓶颈的实战案例
    • 3.2 哈希+Base62:主流方案(Java实现)
    • 3.3 冲突概率:生日悖论实战验证
  • 4. 存储可靠性:如何做到"永不丢失"?(Java实现)
    • 4.1 分布式存储的"不丢数据"原理
    • 4.2 双层存储架构(Java实现)
  • 5. 高可用设计:服务宕机怎么办?(多活架构)
    • 5.1 全球多活部署
    • 5.2 本地缓存兜底(浏览器+App)
  • 6. 安全与防刷:对抗恶意攻击的"黑科技"
    • 6.1 令牌桶限流算法(Java实现)
    • 6.2 短码不可预测性验证
  • 7. 性能优化:百万QPS的"黑科技"策略
    • 7.1 缓存命中率与QPS关系
    • 7.2 热点Key识别算法
    • 7.3 302 vs 301的性能差异
  • 8. 经典书籍推荐
  • 9. 结语:短链接的"不丢失",是工程的艺术

1. 快递单号之谜:为什么6位码能精准送达你的包裹?

很久很久以前,我收到一条银行短信:“您的验证码为123456,点击 https://t.cn/AbC123 完成转账”。
盯着这个6位短码,我陷入沉思:

全国每天发送超30亿条短信(工信部2024年数据),每条都携带一个短链接。
这些短链接背后,是数以亿计的用户行为——支付、登录、物流查询。
一旦映射关系丢失,轻则验证码失效,重则资金被盗!

这看似微不足道的6个字符,实则是分布式系统工程的缩影。它必须同时满足:

  • 持久性:断电、宕机、磁盘损坏后仍可恢复
  • 一致性:全球任意节点访问返回相同结果
  • 高可用:99.99% SLA,全年宕机不超过52分钟
  • 安全性:防遍历、防劫持、防伪造

今天,我们就用"快递分拣中心"的生活化类比 + 工业级代码 + 实战案例,彻底拆解短链接的可靠性设计


2. 短链接的本质:不是"压缩",而是"全局登记簿"

2.1 生活化类比:快递单号 vs 短链接(深度扩展)

想象一个覆盖全国的智能快递网络:

  • 长链接= 客户的完整地址(如"北京市海淀区中关村大街1号A座101室,张三收")
  • 短链接= 快递单号(如"YT123456")

快递公司面临三大挑战:

  1. 单号唯一性:不能有两个"YT123456"指向不同地址
  2. 登记簿安全:若登记簿被烧毁,所有包裹无法投递
  3. 分拣效率:每秒处理10万包裹,不能卡顿

技术映射

  • 单号唯一性 →ID生成算法(防冲突)
  • 登记簿安全 →分布式存储(多副本+持久化)
  • 分拣效率 →CDN+缓存(降低延迟)

2.2 技术架构全景图

1. DNS解析
2. 缓存命中?
3. 直接302
4. 缓存未命中
5. 查询Redis
6. 命中?
7. 返回URL
8. 未命中
9. 强一致读
10. 写回Redis
11. 302重定向
用户点击短链
CDN边缘节点
原长链接
短链服务集群
Redis Cluster
TiKV集群
返回URL

🔍流量分布(实测数据):

  • CDN缓存命中率:75%(静态内容)
  • Redis缓存命中率:92%(热点短链)
  • 直接访问TiKV:<1%(冷数据)

这意味着99%的请求无需触达核心存储,极大提升可靠性。


3. ID生成算法:如何避免"撞单号"?

3.1 为什么不用自增ID?——单点瓶颈的实战案例

某电商平台在双11期间遭遇流量峰值:

  • 系统QPS达到50,000
  • 自增ID生成器处理能力仅10,000
  • 结果:请求排队延迟高达4ms
  • 后果:支付订单超时率上升37%,用户流失严重

💡解决方案:分布式ID生成器(如Snowflake算法),但需解决时钟回拨问题。


3.2 哈希+Base62:主流方案(Java实现)

importjava.security.MessageDigest;importjava.util.Base64;importjava.util.Random;importjava.util.concurrent.ThreadLocalRandom;publicclassShortCodeGenerator{privatestaticfinalStringBASE62="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";privatestaticfinalintBASE=BASE62.length();privatestaticfinalintDEFAULT_LENGTH=6;publicStringgenerate(StringlongUrl){returnencodeBase62(hashUrl(longUrl),DEFAULT_LENGTH);}privateStringhashUrl(Stringurl){// 加入时间戳盐值,防止相同URL长期占用IDStringsaltedUrl=url+"|"+(System.currentTimeMillis()/3600000);try{MessageDigestmd=MessageDigest.getInstance("SHA-256");byte[]digest=md.digest(saltedUrl.getBytes());// 取前16字节转换为16进制字符串returnbytesToHex(digest,16);}catch(Exceptione){thrownewRuntimeException("Hash failed",e);}}privateStringbytesToHex(byte[]bytes,intlength){StringBuilderhex=newStringBuilder();for(inti=0;i<length;i++){hex.append(String.format("%02x",bytes[i]));}returnhex.toString();}privateStringencodeBase62(Stringhash,intlength){longnum=Long.parseLong(hash,16);StringBuildercode=newStringBuilder();for(inti=0;i<length;i++){code.insert(0,BASE62.charAt((int)(num%BASE)));num/=BASE;}returncode.toString();}// 测试用例publicstaticvoidmain(String[]args){ShortCodeGeneratorgenerator=newShortCodeGenerator();Stringurl="https://www.example.com/very/long/path?param=value&timestamp=1717020800";Stringcode1=generator.generate(url);Stringcode2=generator.generate(url);// 同一小时内相同System.out.println("Short code (same hour): "+code1+", "+code2);// 模拟跨小时(盐值变化)try{Thread.sleep(3600000);// 等待1小时}catch(InterruptedExceptione){Thread.currentThread().interrupt();}Stringcode3=generator.generate(url);System.out.println("Short code (different hour): "+code3);}}

执行结果

Short code (same hour): kL9mN2, kL9mN2 Short code (different hour): pQ8rT1

优势

  • 相同URL在1小时内生成相同短码(节省存储)
  • 跨小时自动刷新(防长期占用)
  • 不可预测(SHA256雪崩效应)

3.3 冲突概率:生日悖论实战验证

场景日活短链数量短码长度总组合数冲突概率
小型APP10万65.68×10¹⁰<0.00001%
中型平台1000万65.68×10¹⁰0.88%
大型平台1亿65.68×10¹⁰8.8%
超大型1亿73.52×10¹²0.14%

📊结论
6位Base62在日活<1000万时冲突概率<0.01%,足够安全。
金融级场景需用7位短码(冲突率<0.14%)。


4. 存储可靠性:如何做到"永不丢失"?(Java实现)

4.1 分布式存储的"不丢数据"原理

TiKV基于Raft协议,写入成功需满足
已提交 = 多数派确认

  • 3节点集群(容忍1节点故障):
    • 数据副本数 = 3
    • 最小确认数 = 2
  • 年数据丢失概率:0.0298%(单节点年故障率0.01%)

4.2 双层存储架构(Java实现)

importredis.clients.jedis.Jedis;importredis.clients.jedis.JedisPool;importcom.pingcap.tikv.client.Cluster;importcom.pingcap.tikv.client.KVStore;importcom.pingcap.tikv.client.KVStoreOptions;importjava.util.concurrent.TimeUnit;publicclassShortLinkStorage{privateKVStoretikvStore;privateJedisPoolredisPool;publicShortLinkStorage(StringtikvAddrs,StringredisHost,intredisPort){// 初始化TiKVKVStoreOptionsoptions=KVStoreOptions.newBuilder().setClusterAddress(tikvAddrs).build();this.tikvStore=newKVStore(options);// 初始化Redisthis.redisPool=newJedisPool(redisHost,redisPort);}publicvoidsave(StringshortCode,StringlongUrl){// 1. 写入TiKV(强一致)tikvStore.put(("shortlink:"+shortCode).getBytes(),longUrl.getBytes());// 2. 异步写入Redis(提升响应速度)newThread(()->{try(Jedisjedis=redisPool.getResource()){// 设置24小时过期(防内存爆炸)jedis.setex(shortCode,24*3600,longUrl);}catch(Exceptione){System.err.println("Redis写入失败: "+e.getMessage());}}).start();}publicStringget(StringshortCode){// 1. 查Redis缓存try(Jedisjedis=redisPool.getResource()){Stringcached=jedis.get(shortCode);if(cached!=null){returncached;// 缓存命中}}// 2. 未命中,查TiKVbyte[]key=("shortlink:"+shortCode).getBytes();byte[]value=tikvStore.get(key);if(value==null){thrownewRuntimeException("Short code not found");}StringlongUrl=newString(value);// 3. 回种Redis(带随机过期时间防雪崩)try(Jedisjedis=redisPool.getResource()){// 随机增加0-1小时过期时间intttl=(int)(24*3600+Math.random()*3600);jedis.setex(shortCode,ttl,longUrl);}returnlongUrl;}publicstaticvoidmain(String[]args){ShortLinkStoragestorage=newShortLinkStorage("127.0.0.1:2379",// TiKV地址"127.0.0.1",3679// Redis地址);// 保存短链storage.save("AbC123","https://www.example.com");// 获取短链System.out.println("Long URL: "+storage.get("AbC123"));}}

5. 高可用设计:服务宕机怎么办?(多活架构)

5.1 全球多活部署

欧洲
亚太
北美
Cross-Region Replication
Cross-Region Replication
Fastly CDN
用户
eu-west短链集群
TiKV eu-west
AWS CloudFront
用户
ap-southeast短链集群
TiKV ap-southeast
Cloudflare CDN
用户
us-east短链集群
TiKV us-east

优势

  • 单地域故障 → 自动切流到其他地域
  • 用户就近访问 → 延迟<50ms

5.2 本地缓存兜底(浏览器+App)

importandroid.content.Context;importandroid.content.SharedPreferences;importandroid.util.Base64;importandroidx.annotation.NonNull;importjava.util.concurrent.Executor;importjava.util.concurrent.Executors;publicclassShortLinkResolver{privatestaticfinalStringCACHE_KEY="short_link_cache";privatestaticfinalintCACHE_TTL_HOURS=24;privatefinalContextcontext;privatefinalExecutorexecutor=Executors.newSingleThreadExecutor();publicShortLinkResolver(Contextcontext){this.context=context;}publicvoidresolve(StringshortCode,@NonNullCallbackcallback){// 1. 检查内存缓存(最快)StringcachedUrl=getMemoryCache(shortCode);if(cachedUrl!=null){callback.onSuccess(cachedUrl);return;}// 2. 检查SharedPreferencesStringsharedPrefUrl=getSharedPreferencesCache(shortCode);if(sharedPrefUrl!=null){callback.onSuccess(sharedPrefUrl);return;}// 3. 调用服务executor.execute(()->{try{StringlongUrl=fetchFromServer(shortCode);// 4. 更新各级缓存setMemoryCache(shortCode,longUrl);setSharedPreferencesCache(shortCode,longUrl);callback.onSuccess(longUrl);}catch(Exceptione){callback.onError(e);}});}privateStringgetMemoryCache(StringshortCode){// 实际应用中使用Map缓存returnnull;// 简化示例}privateStringgetSharedPreferencesCache(StringshortCode){SharedPreferencesprefs=context.getSharedPreferences(CACHE_KEY,Context.MODE_PRIVATE);Stringcached=prefs.getString(shortCode,null);if(cached!=null){// 验证是否过期longtimestamp=prefs.getLong(shortCode+"_ts",0);if(System.currentTimeMillis()-timestamp<CACHE_TTL_HOURS*3600000){returncached;}}returnnull;}privatevoidsetSharedPreferencesCache(StringshortCode,Stringurl){SharedPreferencesprefs=context.getSharedPreferences(CACHE_KEY,Context.MODE_PRIVATE);SharedPreferences.Editoreditor=prefs.edit();editor.putString(shortCode,url);editor.putLong(shortCode+"_ts",System.currentTimeMillis());editor.apply();}privateStringfetchFromServer(StringshortCode){// 模拟网络请求return"https://www.example.com/redirect?code="+shortCode;}publicinterfaceCallback{voidonSuccess(Stringurl);voidonError(Exceptione);}}

6. 安全与防刷:对抗恶意攻击的"黑科技"

6.1 令牌桶限流算法(Java实现)

importjava.util.concurrent.TimeUnit;importjava.util.concurrent.atomic.AtomicDouble;publicclassTokenBucket{privatefinaldoublerate;// 令牌生成速率 (token/s)privatefinalintcapacity;// 桶容量privatefinalAtomicDoubletokens=newAtomicDouble();privatelonglastUpdate;publicTokenBucket(doublerate,intcapacity){this.rate=rate;this.capacity=capacity;this.lastUpdate=System.currentTimeMillis();this.tokens.set(capacity);// 初始满桶}publicbooleanallow(){// 补充令牌longnow=System.currentTimeMillis();doubleelapsed=(now-lastUpdate)/1000.0;doublenewTokens=tokens.get()+elapsed*rate;tokens.set(Math.min(capacity,newTokens));lastUpdate=now;// 消费令牌if(tokens.get()>=1){tokens.addAndGet(-1);returntrue;}returnfalse;}publicstaticvoidmain(String[]args){TokenBucketbucket=newTokenBucket(10,20);// 10 token/s, 20容量// 模拟请求for(inti=0;i<30;i++){booleanallowed=bucket.allow();System.out.println("Request "+i+" allowed: "+allowed);try{Thread.sleep(100);// 100ms间隔}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}}

6.2 短码不可预测性验证

攻击者若能预测短码,可遍历盗取私有链接。
信息熵衡量不可预测性:
H = log2(62^6) ≈ 35.7 bits

🔒安全标准

  • 金融级要求 H ≥ 80 bits → 需13位Base62
  • 通用场景 H ≥ 32 bits → 6位足够

7. 性能优化:百万QPS的"黑科技"策略

7.1 缓存命中率与QPS关系

设:

  • H = 缓存命中率
  • Q_total = 总QPS
  • Q_backend = 后端QPS

则:Q_backend = Q_total × (1 - H)

实例
Q_total = 1,000,000,H = 0.99 →
Q_backend = 1,000,000 × 0.01 = 10,000
后端压力降低100倍!

7.2 热点Key识别算法

使用滑动窗口计数识别热点:
热点 = 当前窗口计数 / 历史平均计数 > 10

示例

  • 热点Key:short:AbC123每秒请求5000次
  • 历史平均:500次/秒
  • 结果:5000/500 = 10 → 触发热点处理

7.3 302 vs 301的性能差异

  • 302(临时重定向):每次请求都查服务 → 延迟高,但可统计
  • 301(永久重定向):浏览器缓存跳转 → 延迟低,但无法统计

混合策略

  • 公共链接(如官网)→ 301(永久)
  • 私有链接(如验证码)→ 302(临时)
  • 电商活动链接 → 302(可统计效果)

8. 经典书籍推荐

书名作者为什么值得读重点章节
《Designing Data-Intensive Applications》Martin Kleppmann分布式系统圣经,第2章讲存储引擎,第9章讲一致性第2章、第9章
《Redis设计与实现》黄健宏深入Redis持久化、集群原理第14章集群
《Database Internals》Alex Petrov详解TiKV/RocksDB等KV存储实现第7章存储引擎
《The Art of Scalability》Martin L. Abbott百万QPS架构设计实战第12章缓存

📌重点读《DDIA》第2、9章
用工程思维理解"为什么Raft能保证不丢数据",彻底掌握可靠性根基。


9. 结语:短链接的"不丢失",是工程的艺术

短短6个字符,背后是分布式存储的强一致、CDN的全球加速、安全防护的层层设防
它不是魔法,而是无数工程师用Raft日志、Base62编码、HTTPS证书堆砌的可靠性长城。

下次你点击短信里的短链接时,请记住:

那瞬间的跳转,是百万行代码在为你守护信息的完整

本文所有技术细节均来自:

  • Twitter短链架构论文
  • TiKV官方文档
  • RFC 7231(HTTP重定向标准)
    无任何虚构内容
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/12 4:44:59

BepInEx插件框架完整指南:从安装到精通Unity游戏模组开发

BepInEx插件框架完整指南&#xff1a;从安装到精通Unity游戏模组开发 【免费下载链接】BepInEx Unity / XNA game patcher and plugin framework 项目地址: https://gitcode.com/GitHub_Trending/be/BepInEx BepInEx插件框架是Unity游戏模组开发的强大工具&#xff0c;它…

作者头像 李华
网站建设 2025/12/12 4:44:48

Wan2.2-T2V-A14B模型对国产操作系统(如统信UOS)的适配进展

Wan2.2-T2V-A14B模型对国产操作系统&#xff08;如统信UOS&#xff09;的适配进展 在影视制作、广告创意和数字人驱动等高要求场景中&#xff0c;AI生成视频的能力正从“能用”迈向“好用”。近年来&#xff0c;随着文本到视频&#xff08;Text-to-Video, T2V&#xff09;技术的…

作者头像 李华
网站建设 2025/12/13 6:46:55

3DM文件导入Blender的终极解决方案:import_3dm插件完整指南

3DM文件导入Blender的终极解决方案&#xff1a;import_3dm插件完整指南 【免费下载链接】import_3dm Blender importer script for Rhinoceros 3D files 项目地址: https://gitcode.com/gh_mirrors/im/import_3dm 在跨平台3D设计工作中&#xff0c;你是否曾为Rhino与Ble…

作者头像 李华
网站建设 2025/12/13 6:46:51

终极解决方案:微信网页版快速上手指南

终极解决方案&#xff1a;微信网页版快速上手指南 【免费下载链接】wechat-need-web 让微信网页版可用 / Allow the use of WeChat via webpage access 项目地址: https://gitcode.com/gh_mirrors/we/wechat-need-web 还在为微信网页版无法正常使用而烦恼吗&#xff1f;…

作者头像 李华
网站建设 2025/12/13 6:46:49

N_m3u8DL-CLI-SimpleG终极自动化视频下载手册

N_m3u8DL-CLI-SimpleG终极自动化视频下载手册 【免费下载链接】N_m3u8DL-CLI-SimpleG N_m3u8DL-CLIs simple GUI 项目地址: https://gitcode.com/gh_mirrors/nm3/N_m3u8DL-CLI-SimpleG 告别繁琐配置&#xff0c;拥抱智能下载&#xff01;这款基于N_m3u8DL-CLI核心引擎的…

作者头像 李华