ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java短链接生成工具设计与实现:Base62编码、缓存与高并发源码解析

Java短链接生成工具设计与实现:Base62编码、缓存与高并发源码解析 简介这份资源是一套基于Java的短链接生成工具完整源码面向具备一定Java与前端基础的开发者用于解决长链接管理繁琐、访问数据难以追踪的问题。项目融合Java、Vue、JavaScript、CSS与HTML等技术覆盖后端服务与前端界面适合作为课程设计、毕业设计或二次开发的学习范本。压缩包共280个文件约1.44MB其中187个Java源文件构成核心业务逻辑19个Vue组件与11个JavaScript文件负责前端交互另有XML配置、YAML部署文件及PNG、SVG等界面素材目录结构清晰便于按模块阅读。工具实现短链接生成、访问量统计、访问详情记录与地区及设备分布图表并支持随时修改跳转目标、批量创建链接还预留了AB测试能力。目前已有329人学习下载读者可从中获取完整的工程分层思路、前后端协作方式与数据统计实现细节快速理解短链接系统的设计要点。1. 短链接生成工具到底解决什么问题从长 URL 到 6 位短码的完整链路做运营的朋友大概率遇到过这种场景一条带十几个查询参数的推广链接扔进短信里直接超长被截断扔进二维码里密度高到扫不出来扔进微博又占掉大半字数。短链接生成工具要干的事就是把https://example.com/activity/2024/summer?utm_sourcexxxutm_mediumyyychannelzzztrace_id123456这种几百字符的长 URL压缩成https://s.example.com/aB3dK9这样的短地址用户点开照样跳到原地址。用 Java 来做这件事核心链路其实就四步接收长链接、生成唯一短码、落库建立映射、访问时重定向。听起来简单但真正上线要处理的细节不少——短码怎么保证不重复、高并发下怎么扛住读多写少、过期链接怎么清理、恶意刷量怎么防。这篇笔记就围绕「基于 Java 的短链接生成工具设计与实现源码」这个方向把表结构、发号器、缓存、重定向、压测这几块拆开讲清楚适合想自己动手写一套短链服务、或者正在看开源短链源码但看不懂设计取舍的 Java 开发者。下面所有代码都是可跑的最小实现不是伪代码。2. 短码生成算法怎么选自增发号、哈希与 Base62 的取舍短码生成是整个工具的心脏选错了后面全是坑。常见做法有三类数据库自增 ID 转 Base62、哈希取模、预生成随机码池。我一般会先看业务量级——日增几千条和日增几百万条方案完全不一样。2.1 三种主流方案对比与选型依据先把三种方案的特性摆出来方便你对着自己的场景选。方案短码长度冲突概率是否可预测适用量级实现复杂度自增 ID Base626~7 位无冲突可预测递增中小规模低哈希取模MurmurHash6~8 位有冲突需处理不可预测任意中预生成随机码池固定 6 位无冲突不可预测大规模高自增 ID 转 Base62 是最省事的做法数据库主键自增把十进制 ID 转成 62 进制字符串。比如 ID 为 1000000转成 Base62 是4c92只有 4 位。这个方案天然无冲突因为 ID 本身唯一。缺点是短码连续可预测别人能顺着aB3dK8、aB3dK9遍历你的全部链接如果链接涉及隐私或不想被爬就得加干扰位。哈希方案用 MurmurHash 对长 URL 取 64 位哈希再取模映射到 62 进制空间。好处是同一个长 URL 每次生成的短码一致幂等适合去重场景。坏处是哈希空间有限链接量大了必然碰撞得靠布隆过滤器或数据库唯一索引兜底重试。预生成随机码池是大型短链服务的做法后台离线生成几百万个 6 位随机码存进池子用的时候直接取。优点是短码完全随机、不可预测、无冲突缺点是得维护一个码池服务架构复杂度上去了。提示如果只是内部系统或中小项目自增 ID Base62 足够用别一上来就上码池运维成本不划算。2.2 Base62 编码的 Java 实现与边界处理Base62 的字符集是0-9a-zA-Z共 62 个字符。编码逻辑就是不断对 62 取余余数查字符表商继续循环。下面是可直接用的工具类。public class Base62Util { // 62 个字符顺序固定解码时靠 indexOf 反查 private static final String CHARS 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; private static final int BASE CHARS.length(); // 十进制转 62 进制 public static String encode(long num) { if (num 0) return String.valueOf(CHARS.charAt(0)); StringBuilder sb new StringBuilder(); while (num 0) { int rem (int) (num % BASE); // 取余得到当前位 sb.append(CHARS.charAt(rem)); num / BASE; // 商继续下一轮 } return sb.reverse().toString(); // 反转才是正确顺序 } // 62 进制转十进制用于重定向时反查 public static long decode(String str) { long num 0; for (int i 0; i str.length(); i) { int idx CHARS.indexOf(str.charAt(i)); if (idx 0) throw new IllegalArgumentException(非法字符: str.charAt(i)); num num * BASE idx; } return num; } }逻辑说明encode里先取余拿到最低位字符再整除进入下一轮最后reverse才是人类可读顺序。decode是逆运算逐位累乘。参数上唯一要注意的是long类型——int最大约 21 亿转 Base62 只有 6 位但链接量一旦超过 21 亿就会溢出所以发号器返回类型必须是long。边界坑num为 0 时循环不执行会返回空串所以单独处理。另外字符表顺序一旦上线就不能改改了所有历史短码全部失效这是血泪经验——我见过有人把大小写顺序调了一下结果线上几万条链接集体 404。2.3 用数据库自增主键驱动短码生成最稳的落地方式是把发号交给数据库。建一张映射表主键自增插入长 URL 后拿到自增 ID再转 Base62 作为短码回写。CREATE TABLE t_short_link ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 自增主键短码来源, short_code VARCHAR(10) NOT NULL COMMENT Base62 短码, long_url VARCHAR(2048) NOT NULL COMMENT 原始长链接, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expire_time DATETIME DEFAULT NULL COMMENT 过期时间NULL 表示永久, PRIMARY KEY (id), UNIQUE KEY uk_short_code (short_code), KEY idx_expire (expire_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插入时先写long_url用getGeneratedKeys拿回自增 ID再encode成短码update回去。这里有个并发细节两步操作之间如果服务挂了会留下short_code为空的脏数据所以要么用事务包住要么加一个定时任务扫空短码补写。我一般选后者因为事务里做两次写会拉长持锁时间高并发下不划算。3. 高并发读写怎么做缓存、布隆过滤器与重定向性能短链服务是典型的读多写少——生成一次可能被访问几万次。所以性能优化的重心全在「读」这条路径上从短码到长 URL 的查询必须快最好不碰数据库。3.1 缓存穿透与布隆过滤器的接入如果直接查数据库恶意用户拿随机短码疯狂请求每次都是缓存未命中加数据库空查数据库很快被打穿这就是缓存穿透。解决办法是在缓存前面加一层布隆过滤器把所有已生成的短码放进去请求先过布隆判定「一定不存在」的直接返回 404不往下走。Component public class ShortLinkBloomFilter { private final BloomFilterString filter BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000, // 预估元素数量 0.001 // 允许误判率 0.1% ); public void add(String shortCode) { filter.put(shortCode); } // 返回 false 表示一定不存在可直接拒绝 public boolean mightExist(String shortCode) { return filter.mightContain(shortCode); } }参数说明1_000_000是预估要放进去的短码总数0.001是误判率。误判率越低底层位数组越大、内存占用越高。100 万元素、0.1% 误判率大约占 1.8MB 内存完全可接受。注意布隆过滤器只能判断「一定不存在」和「可能存在」不能判断「一定存在」所以它只是第一道闸后面还得有缓存和数据库兜底。注意布隆过滤器是内存态的服务重启就没了。生产环境要么启动时从数据库全量重建要么用 Redis 的 bitmap 自己实现一套持久化的。3.2 Redis 缓存短码映射与过期策略布隆过滤器放行后第二道是 Redis。短码到长 URL 的映射用 String 结构存key 是sl:{shortCode}value 是长 URL。Service public class ShortLinkService { Autowired private StringRedisTemplate redisTemplate; Autowired private ShortLinkMapper mapper; public String getLongUrl(String shortCode) { String cacheKey sl: shortCode; // 先查缓存 String longUrl redisTemplate.opsForValue().get(cacheKey); if (longUrl ! null) { return longUrl; } // 缓存未命中查库 ShortLink link mapper.selectByShortCode(shortCode); if (link null) { // 空值也缓存防止缓存穿透过期时间设短一点 redisTemplate.opsForValue().set(cacheKey, , 60, TimeUnit.SECONDS); return null; } // 判断是否过期 if (link.getExpireTime() ! null link.getExpireTime().before(new Date())) { return null; } // 回写缓存过期时间取链接剩余有效期和 24 小时的较小值 long ttl calcTtl(link); redisTemplate.opsForValue().set(cacheKey, link.getLongUrl(), ttl, TimeUnit.SECONDS); return link.getLongUrl(); } private long calcTtl(ShortLink link) { if (link.getExpireTime() null) return 86400L; long remain (link.getExpireTime().getTime() - System.currentTimeMillis()) / 1000; return Math.min(remain, 86400L); } }逻辑说明命中缓存直接返回未命中查库库里也没有就缓存一个空字符串 60 秒避免同一个不存在的短码反复打数据库。参数上空值缓存时间不能太长否则新生成的短码如果刚好撞上这个空值窗口会误判60 秒是经验值。正常链接的缓存 TTL 取「剩余有效期」和「24 小时」的较小值保证过期链接不会因为缓存而继续可访问。3.3 302 重定向的响应头与状态码选择拿到长 URL 后重定向用 302 还是 301是个容易被忽略但影响很大的选择。301 是永久重定向浏览器会缓存下次访问同一个短码不再请求你的服务器直接跳转。302 是临时重定向每次都会回源。RestController public class RedirectController { Autowired private ShortLinkService shortLinkService; GetMapping(/{shortCode}) public ResponseEntityVoid redirect(PathVariable String shortCode) { String longUrl shortLinkService.getLongUrl(shortCode); if (longUrl null) { return ResponseEntity.notFound().build(); } return ResponseEntity.status(HttpStatus.FOUND) // 302 .header(HttpHeaders.LOCATION, longUrl) .build(); } }选 302 的理由短链服务通常要统计点击量、做风控、支持链接失效如果用了 301浏览器缓存后你的服务器根本收不到请求统计和失效控制全部失效。代价是每次访问都回源服务器压力大一些但配合前面的 Redis 缓存这个压力完全扛得住。如果某个链接确定永久不变且不需要统计可以单独走 301但默认一律 302。4. 短链接工具避坑与排查五个上线后才会暴露的问题前面讲的都是「正常路径」但真正让人半夜爬起来的是那些边界情况。下面五条都是我或身边同事踩过的按「现象 → 原因 → 解决」写。4.1 短码大小写混淆导致 404现象用户反馈明明复制的是aB3dK9粘贴访问却提示不存在。原因有些渠道比如某些 IM 工具、邮件客户端会自动把 URL 里的字母转成小写aB3dK9变成ab3dk9而你的字符表里大小写是两个不同字符反查自然失败。解决要么短码只用小写字母加数字牺牲一部分空间6 位从 568 亿降到 21 亿要么在查询时做大小写不敏感的匹配。我一般选前者从源头避免因为大小写不敏感查询会让唯一索引失效性能下降明显。4.2 自增 ID 回写失败留下空短码现象数据库里出现short_code为空字符串的记录访问对应 ID 报错。原因插入长 URL 和回写短码是两步操作中间服务重启或抛异常第二步没执行。解决加一个补偿定时任务每分钟扫一次short_code 的记录重新生成短码回写。同时给short_code字段加唯一索引空串只能有一条第二条插入会失败能及时暴露问题。4.3 长 URL 超长被截断现象某些带大量参数的推广链接存进去后跳转地址不完整落地页 404。原因VARCHAR(2048)在某些字符集下不够用或者前端传参时被截断。解决字段类型改成TEXT或VARCHAR(4096)同时在入口做长度校验超过阈值直接拒绝并返回明确错误码别让它悄悄截断。另外 URL 里的中文参数要先做 URLEncode否则存进去就是乱码。4.4 缓存与数据库不一致现象链接在后台被删除了但用户还能访问过了好一会儿才失效。原因删除时只删了数据库没删 Redis 缓存缓存里的旧数据还在 TTL 内。解决删除操作必须「先删缓存再删数据库」或者用延迟双删——删完缓存、更新数据库后隔 500ms 再删一次缓存。更稳的做法是给缓存 key 设一个较短的 TTL 兜底即使漏删也会自动过期。4.5 恶意刷量打满带宽现象某个短码在几分钟内被请求几十万次服务器带宽跑满正常用户访问变慢。原因短链是公开的被人拿去刷了。解决在重定向入口加一层限流按 IP 或短码维度做滑动窗口计数超过阈值直接返回 429。用 Redis 的INCR加过期时间就能实现最简单的计数器成本低效果好。5. 压测验证与短码长度调优把工具跑成能上线的服务写完代码只是开始能不能上线得看压测数据。我一般用 JMeter 或 wrk 对重定向接口做压测重点看三个指标QPS、P99 延迟、缓存命中率。单机 4 核 8G配合 Redis 缓存重定向接口跑到 8000 QPS、P99 在 15ms 以内算合格。如果 QPS 上不去先看缓存命中率——低于 95% 说明大量请求穿透到了数据库得检查布隆过滤器和缓存 TTL 设置。短码长度也需要根据实际量级调。6 位 Base62 的空间是 62 的 6 次方约 568 亿看起来很大但如果你的业务日增百万链接几年下来也会逼近。我的习惯是留足余量预估三年总量乘以 10 倍安全系数再反推需要几位。比如三年预计 10 亿条乘 10 是 100 亿6 位够用如果预计 100 亿条就得考虑 7 位。别等到 ID 溢出才想起来加位那时候所有历史短码都得迁移代价极大。验证环节还有一个容易漏的点短码生成后要做一次「可解码性」自检。写个单元测试随机生成一万个短码逐个decode再encode确认结果一致。这个测试能挡住字符表被误改、编码逻辑被重构引入的 bug。Test public void testEncodeDecodeRoundTrip() { Random random new Random(); for (int i 0; i 10000; i) { long num Math.abs(random.nextLong() % 100_000_000_000L); String code Base62Util.encode(num); long decoded Base62Util.decode(code); assertEquals(短码往返不一致: num, num, decoded); } }最后说个我自己的习惯每次改完发号逻辑或字符表先跑这个往返测试再跑一遍全量历史短码的抽样解码确认老链接还能正常跳转。短链服务最怕的就是「新链接没问题老链接悄悄挂了」等用户投诉才发现那时候损失已经造成了。希望帮到你。本文还有配套的精品资源点击获取
返回列表