news 2026/5/8 8:58:03

【后端】【工具】短信短链接如何做到“永不丢失“?从哈希冲突到百万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进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 4:43:34

从CAD插件到原生平台:工程AI的演进路径与智能协同新范式

​摘要&#xff1a;随着工程师AI助手的普及&#xff0c;电力工程师面对繁琐的报告撰写、规范查询和图纸绘制等痛点正被逐一击破。本文通过对比传统CAD插件与良策金宝的六大核心功能&#xff0c;展示如何通过原生平台提升工作效率、保障合规性&#xff0c;并实现知识沉淀。一、插…

作者头像 李华
网站建设 2026/5/8 8:17:24

Mermaid.js流程图布局算法终极优化指南

Mermaid.js流程图布局算法终极优化指南 【免费下载链接】mermaid 项目地址: https://gitcode.com/gh_mirrors/mer/mermaid 作为一名技术伙伴&#xff0c;你是否在使用Mermaid.js绘制复杂流程图时遭遇过这样的困境&#xff1a;节点位置混乱、连线交叉严重、手动调整耗费…

作者头像 李华
网站建设 2026/5/6 6:17:20

Blender PSK/PSA动画导入实战指南:从困惑到精通

Blender PSK/PSA动画导入实战指南&#xff1a;从困惑到精通 【免费下载链接】io_scene_psk_psa A Blender plugin for importing and exporting Unreal PSK and PSA files 项目地址: https://gitcode.com/gh_mirrors/io/io_scene_psk_psa 当你满怀期待地将精心制作的PSK…

作者头像 李华
网站建设 2026/5/2 17:30:17

曾经的王,SpringBoot倒下了!!!

马上就要2026年了&#xff0c;想跟大家说点儿心里话。当下Java 行业正经历“结构性洗牌”&#xff1a;3年岗位缩减40%&#xff0c;AI替代30%基础编码&#xff0c;1:120的竞争比让求职难上加难&#xff0c;未来3年更有40万开发者面临淘汰。最近筛简历&#xff0c;清一色的Spring…

作者头像 李华
网站建设 2026/5/1 2:33:54

Wan2.2-T2V-A14B在航天员训练模拟视频中的失重状态呈现

Wan2.2-T2V-A14B在航天员训练模拟视频中的失重状态呈现 在空间站绕地球高速飞行的轨道上&#xff0c;航天员看似“漂浮”于舱内&#xff0c;实则正以每秒近8公里的速度自由落体——这种持续的微重力环境&#xff0c;是地面难以复现却又至关重要的训练场景。传统依赖水槽模拟或…

作者头像 李华