news 2026/10/7 6:33:01

Java短链接生成工具实战:Base62编码、Redis缓存与布隆过滤器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java短链接生成工具实战:Base62编码、Redis缓存与布隆过滤器

简介:这是一套基于Java实现的短链接生成工具完整源码,面向具备一定Java与前端基础的开发者,可用于学习链接管理、访问统计与AB测试等典型业务场景。项目融合Java、Vue、JavaScript、CSS与HTML等技术栈,核心能力包括将原始网页链接压缩为简洁短链、记录每次访问的详细信息并生成地区分布与设备信息等统计图表,同时支持随时修改跳转目标、批量创建短链,兼顾管理效率与灵活性。资源包共280个文件,约1.44MB,其中187个Java源文件构成后端主体,19个Vue组件与11个JavaScript文件负责前端交互,另有XML、YAML配置及PNG、SVG等静态资源,目录结构清晰,便于按模块阅读与二次开发。目前已有329人学习下载,适合希望掌握短链系统设计思路、统计埋点与前后端协作方式的读者参考借鉴。

1. 短链接生成工具:从长网址到短码,Java 方案到底怎么落地

一条带 UTM 参数的电商活动链接动辄两三百个字符,丢进短信里直接截断,贴在海报上得用放大镜看。短链接生成工具要解决的就是这件事:把任意长网址压缩成https://域名/abc123这种短码,用户点击后 302 跳转到原始地址。听起来简单,但真正上线要考虑的东西不少——短码怎么生成才不撞车、并发写入怎么保证唯一、跳转用 301 还是 302、缓存怎么扛住热点链接、数据量大到千万级之后索引还走不走得动。这套基于 Java 的实现方案,核心就是围绕「发号 → 映射 → 跳转 → 统计」这条链路来设计。适合有 Java 基础、想做一个能真正跑起来的后端小项目的开发者,也适合拿它来练手 Spring Boot、Redis、分库分表这些常见技术栈的组合使用。下面从表结构设计开始,一步步把可运行的代码和踩过的坑讲清楚。

2. 短码生成算法选型:自增发号、哈希与 Base62 的取舍

短链接工具最核心的问题就是:给你一个长网址,怎么产出一个短码?这个短码要满足三个条件——全局唯一、尽量短、生成速度够快。常见的做法有三类,各有各的适用场景,选错了后面改起来很痛苦。

2.1 三种主流短码生成策略对比

自增 ID + Base62 编码是最稳妥的方案。数据库维护一个自增主键,拿到 ID 之后转成 62 进制字符串。比如 ID 为 1000,Base62 编码后是gi,只有两位。这种方案的好处是绝对不会重复,因为自增 ID 本身就不重复;短码长度随 ID 增长而缓慢增长,6 位 Base62 能表示 568 亿个组合,够用很久。缺点是短码可被枚举——别人从a开始递增就能遍历你的所有链接。如果业务对这一点敏感,可以在 Base62 之前做一次混淆,比如把 ID 和一个固定大质数做异或。

哈希取模是另一种思路。对长网址做 MD5 或 MurmurHash,取哈希值的前 N 位作为短码。问题是哈希必然存在碰撞,两个不同的长网址可能映射到同一个短码。解决碰撞的方式是检测到冲突后加盐重新哈希,但这样每次生成都要查一次库,并发高了性能会受影响。

预生成短码池适合对写入性能要求极高的场景。后台批量生成一批短码存进队列,前端请求来了直接从队列里取,取完再补。好处是写入路径极短,坏处是要维护一个额外的短码池服务,复杂度上去了。

方案唯一性保证短码长度写入性能可枚举风险适用场景
自增ID+Base62强保证4-7位高有大多数业务
哈希取模需冲突检测6-8位中低对不可枚举有要求
预生成池强保证可控极高有超高并发写入

我一般会选自增 ID + Base62,再叠加一层轻量混淆。理由很简单:唯一性由数据库保证,不需要在应用层做冲突检测,代码路径最短,出问题的概率最低。

2.2 Base62 编码的 Java 实现

Base62 就是用0-9、a-z、A-Z共 62 个字符来表示数字。核心逻辑是不断对 62 取余,余数查字符表,商继续下一轮,直到商为 0。

public class Base62Encoder { // 62个字符,顺序可以自定义,打乱后短码更不可预测 private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; private static final int BASE = CHARS.length(); /** * 将十进制数字编码为 Base62 字符串 * @param num 自增ID,必须为非负数 * @return Base62 短码 */ public static String encode(long num) { if (num < 0) { throw new IllegalArgumentException("num must be non-negative"); } if (num == 0) { return String.valueOf(CHARS.charAt(0)); } StringBuilder sb = new StringBuilder(); while (num > 0) { int remainder = (int) (num % BASE); sb.append(CHARS.charAt(remainder)); num /= BASE; } // 反转得到正确顺序 return sb.reverse().toString(); } /** * 将 Base62 字符串解码回十进制数字 * @param code 短码 * @return 原始数字 */ public static long decode(String code) { long result = 0; for (int i = 0; i < code.length(); i++) { int digit = CHARS.indexOf(code.charAt(i)); if (digit < 0) { throw new IllegalArgumentException("invalid char: " + code.charAt(i)); } result = result * BASE + digit; } return result; } }

这段代码有两个关键点。第一,CHARS的顺序可以打乱,打乱后即使别人知道你是自增 ID,也没法直接猜出短码序列。第二,encode里先 append 再 reverse,是因为取余得到的字符是从低位到高位的,必须反转才是正确顺序。decode方法在排查问题时很有用——拿到一个短码,反解出 ID,就能直接去数据库定位记录。

2.3 混淆策略:让短码不可枚举

如果直接用自增 ID 做 Base62,短码就是1、2、3……这种顺序。别人写个循环就能遍历你所有链接。常见的混淆方式有两种:

一种是异或混淆。选一个足够大的质数作为密钥,把 ID 和密钥做异或后再 Base62。这样短码看起来就是乱序的,但解码时再异或一次就能还原。注意异或密钥要足够大,否则短码长度会不够。

另一种是Feistel 网络。把 ID 拆成两半,做多轮可逆变换,每轮用不同的轮函数。这种方式更安全,但实现复杂度也更高。对于大多数业务场景,异或混淆已经够用了。

// 异或混淆示例,密钥取一个较大的质数 private static final long XOR_KEY = 0x5DEECE66DL; public static String encodeWithObfuscation(long id) { long obfuscated = id ^ XOR_KEY; return Base62Encoder.encode(obfuscated); } public static long decodeWithObfuscation(String code) { long obfuscated = Base62Encoder.decode(code); return obfuscated ^ XOR_KEY; }

注意:异或混淆只能防住「顺手遍历」,不能防住有心人。如果业务对安全性要求高,应该在短码之外再加一层签名校验,或者直接用随机短码 + 唯一索引冲突重试的方案。

3. 存储层设计:MySQL 表结构与 Redis 缓存的配合

短链接系统的读写比极度倾斜——写入一次,可能被点击几万次。所以存储层要分两块:MySQL 负责持久化和唯一性保证,Redis 负责扛住高频读取。

3.1 MySQL 表结构设计与索引选择

核心表就一张,字段不多,但每个字段的类型和索引都要想清楚。

CREATE TABLE `short_link` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键', `short_code` VARCHAR(10) NOT NULL COMMENT 'Base62短码', `original_url` VARCHAR(2048) NOT NULL COMMENT '原始长网址', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `expire_at` DATETIME DEFAULT NULL COMMENT '过期时间,NULL表示永不过期', `click_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '点击次数', PRIMARY KEY (`id`), UNIQUE KEY `uk_short_code` (`short_code`), KEY `idx_expire_at` (`expire_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短链接映射表';

几个设计决策值得展开说。original_url用VARCHAR(2048)而不是TEXT,是因为 VARCHAR 在 InnoDB 里可以走覆盖索引,而且 2048 字节足够覆盖绝大多数 URL 长度。short_code加了唯一索引,这是防止短码冲突的最后一道防线——即使应用层出了问题,数据库也不会让重复短码写进去。expire_at加普通索引是为了定时清理过期链接时能快速定位。

click_count字段放在主表里其实有争议。如果点击量很大,每次跳转都更新这个字段会造成频繁的行锁竞争。更常见的做法是把点击事件写到 Redis 或消息队列,异步批量更新。如果只是做个 demo,直接更新问题不大;如果要上线,建议拆出去。

3.2 Redis 缓存策略与过期时间设置

跳转接口的 QPS 可能是写入的几百倍,每次跳转都查 MySQL 肯定扛不住。Redis 缓存是必须的,但缓存策略有几个细节要注意。

缓存 Key 用short_link:{short_code},Value 存原始 URL。过期时间设置有两种思路:如果链接本身有过期时间,Redis 的 TTL 就设成和链接过期时间一致;如果链接永不过期,Redis TTL 可以设成 24 小时,配合懒加载——缓存没命中就查 MySQL 并回写。

@Service public class ShortLinkService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private ShortLinkMapper shortLinkMapper; private static final String CACHE_KEY_PREFIX = "short_link:"; private static final long CACHE_TTL_HOURS = 24; /** * 根据短码获取原始URL,优先走Redis缓存 */ public String getOriginalUrl(String shortCode) { String cacheKey = CACHE_KEY_PREFIX + shortCode; // 1. 先查Redis String cachedUrl = redisTemplate.opsForValue().get(cacheKey); if (cachedUrl != null) { return cachedUrl; } // 2. 缓存未命中,查MySQL ShortLink link = shortLinkMapper.selectByShortCode(shortCode); if (link == null) { return null; } // 3. 检查是否过期 if (link.getExpireAt() != null && link.getExpireAt().before(new Date())) { return null; } // 4. 回写Redis redisTemplate.opsForValue().set( cacheKey, link.getOriginalUrl(), CACHE_TTL_HOURS, TimeUnit.HOURS); return link.getOriginalUrl(); } }

这段代码里有个容易忽略的点:缓存穿透。如果有人拿一个不存在的短码疯狂请求,每次都会穿透 Redis 打到 MySQL。解决办法是在 Redis 里缓存一个空值,TTL 设短一点,比如 5 分钟。这样短时间内重复请求同一个不存在的短码,就不会反复查库了。

另一个问题是缓存雪崩。如果大量链接的缓存同时过期,请求会瞬间全打到 MySQL。解决办法是在 TTL 上加一个随机偏移量,比如24小时 + random(0, 3600)秒,让过期时间分散开。

3.3 分库分表的时机判断

单表数据量在 500 万以内,MySQL 的 B+ 树索引还能保持不错的查询性能。超过这个量级,就需要考虑分库分表了。短链接表的分片键很自然就是short_code,因为查询几乎都是按短码查。常见的分片算法是对短码做哈希取模,或者用一致性哈希。

但分库分表会带来一个麻烦:自增 ID 不再全局唯一。这时候要么用分布式 ID 生成器(雪花算法等),要么每个分片独立自增但加一个分片前缀。我一般会建议:如果单表还没到 500 万行,先别急着分。过早分库分表带来的复杂度,往往比性能收益更大。

4. 跳转服务实现:Spring Boot 接口与 301/302 的选择

存储层搞定之后,跳转服务就是对外暴露的入口。这部分逻辑不复杂,但 HTTP 状态码的选择、参数校验、防刷策略这些细节决定了服务的质量。

4.1 跳转接口的完整实现

@RestController public class RedirectController { @Autowired private ShortLinkService shortLinkService; /** * 短链接跳转接口 * @param shortCode 路径中的短码 * @return 302重定向到原始URL */ @GetMapping("/{shortCode}") public ResponseEntity<Void> redirect( @PathVariable String shortCode, HttpServletRequest request) { // 1. 参数校验:短码只允许字母和数字,长度限制 if (shortCode == null || !shortCode.matches("^[0-9a-zA-Z]{1,10}$")) { return ResponseEntity.badRequest().build(); } // 2. 查询原始URL String originalUrl = shortLinkService.getOriginalUrl(shortCode); if (originalUrl == null) { return ResponseEntity.notFound().build(); } // 3. 异步记录点击事件 shortLinkService.recordClick(shortCode, request); // 4. 302临时重定向 HttpHeaders headers = new HttpHeaders(); headers.setLocation(URI.create(originalUrl)); return new ResponseEntity<>(headers, HttpStatus.FOUND); } }

参数校验这一步不能省。短码只允许字母和数字,长度限制在 10 位以内,这样可以直接挡掉一批恶意请求。recordClick方法建议做成异步的,用@Async注解或者丢到线程池里执行,不要阻塞跳转主流程。

4.2 301 与 302 的取舍

这是短链接系统里最常被讨论的问题。301 是永久重定向,浏览器会缓存这个映射关系,下次再访问同一个短码就直接跳转,不再请求你的服务器。302 是临时重定向,每次都会请求服务器。

选 301 的好处是省服务器资源,坏处是你丢失了后续的点击数据——浏览器不请求了,你就统计不到。选 302 的好处是每次跳转都经过服务器,点击统计准确,而且你可以随时修改短码指向的原始 URL。坏处是服务器压力大。

对于短链接服务,我一般选302。原因很直接:短链接的核心价值之一就是可统计、可修改。如果用了 301,用户浏览器缓存了跳转关系,你后面想改目标地址都改不了,点击数据也丢了。除非是纯公益的、不需要统计的短链接服务,才考虑 301。

4.3 防刷与限流

短链接跳转接口是公开的,很容易被刷。常见的防护手段有两层:IP 限流和短码黑名单。

IP 限流可以用 Redis 做滑动窗口。每个 IP 每分钟最多请求 60 次跳转,超过就返回 429。实现方式是用INCR命令配合EXPIRE,或者用 Redis 的 Sorted Set 做精确的滑动窗口。

/** * 基于Redis的简单IP限流 * @return true表示允许通过,false表示被限流 */ public boolean allowRequest(String ip) { String key = "rate_limit:" + ip; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { // 第一次请求,设置过期时间 redisTemplate.expire(key, 1, TimeUnit.MINUTES); } return count <= 60; }

这段代码有个小问题:increment和expire不是原子操作。如果第一次increment之后、expire之前服务挂了,这个 key 就永远不会过期。更稳妥的做法是用 Lua 脚本把两个命令包在一起执行。

短码黑名单则是针对已经确认的恶意链接,把短码加入 Redis Set,跳转前先检查一下。这个功能在内容审核场景下很有用。

5. 避坑与排查:短链接系统上线后最容易翻车的 5 个点

前面把主流程讲完了,但真正上线之后,出问题的地方往往不在主流程,而在一些边角细节上。下面这 5 个坑都是我或者身边同事实际踩过的,按「现象 → 原因 → 解决」整理出来。

5.1 短码冲突导致写入失败

现象:高并发写入时,偶尔出现Duplicate entry for key 'uk_short_code'异常,用户看到 500 错误。

原因:如果短码生成逻辑不是严格依赖数据库自增 ID,而是用了哈希或者随机数,并发场景下就可能生成相同的短码。即使概率很低,量大了也会撞上。

解决:最彻底的办法是用数据库自增 ID 做 Base62,从根上杜绝冲突。如果必须用随机短码,写入时捕获唯一索引冲突异常,重新生成一次再试,重试 3 次还失败就返回错误。不要用「先查再插」的方式,并发下查和插之间有窗口期,照样会冲突。

5.2 Redis 缓存与 MySQL 数据不一致

现象:修改了某个短链接的目标 URL,但跳转还是跳到旧地址,过了好一会儿才更新。

原因:更新 MySQL 之后没有删除 Redis 缓存,或者删除失败了。缓存里还是旧数据,直到 TTL 过期才回源。

解决:更新流程必须是「先更新 MySQL,再删除 Redis」,而不是「先删 Redis 再更新 MySQL」。删除失败时要重试,或者把删除操作丢到消息队列里保证最终一致。更简单的做法是给缓存设一个较短的 TTL,比如 5 分钟,即使删除失败,最多 5 分钟后也会自动纠正。

5.3 长网址超长导致插入失败

现象:某些带大量参数的 URL 插入 MySQL 时报Data too long for column 'original_url'。

原因:VARCHAR(2048)看起来够用,但有些电商平台的 URL 带一堆追踪参数,轻松超过 2048 字符。

解决:两个方案。一是把字段改成TEXT类型,但会失去覆盖索引的能力。二是插入前先判断长度,超过阈值就拒绝并返回明确错误。我一般选后者,因为超长 URL 本身就不适合做短链接——短链接的价值在于简洁,原始 URL 太长说明它本身就需要被清理。

5.4 过期链接清理任务锁表

现象:定时清理任务执行时,数据库 CPU 飙升,正常跳转请求变慢。

原因:清理任务用DELETE FROM short_link WHERE expire_at < NOW()一次性删除大量数据,长事务锁住了大量行。

解决:分批删除,每次删 1000 行,删完 sleep 100 毫秒再删下一批。或者用LIMIT子句控制单次删除量。另外,清理任务尽量放在低峰期执行,比如凌晨 3 点。

5.5 短码大小写敏感导致 404

现象:用户手动输入短码时,把abc输成了ABC,结果 404。

原因:Base62 的字符集里同时包含大小写字母,abc和ABC是两个不同的短码。

解决:如果业务允许,可以在生成短码时只用小写字母和数字,把字符集从 62 个缩减到 36 个。代价是同样长度能表示的组合数变少了,但 6 位 36 进制仍然有 21 亿种组合,够用。另一种方案是在跳转接口里对短码做大小写不敏感处理,但这样需要额外维护一个映射关系,复杂度更高。

6. 进阶技巧:用布隆过滤器挡住无效短码请求

前面在缓存策略里提到了缓存穿透的问题——大量请求不存在的短码,每次都打到 MySQL。用 Redis 缓存空值能缓解,但如果攻击者每次都用不同的随机短码,空值缓存也会被撑爆。这时候布隆过滤器就是更优雅的方案。

布隆过滤器的原理不复杂:用一个位数组和多个哈希函数,把存在的短码映射到若干个位上。查询时如果所有对应的位都是 1,说明短码可能存在;如果有任何一位是 0,说明短码一定不存在。注意「可能存在」这个词——布隆过滤器有假阳性,但没有假阴性。也就是说,它说「不存在」的时候一定准,说「存在」的时候可能误判。

在短链接系统里,这个特性刚好够用:请求进来先过布隆过滤器,如果它说不存在,直接返回 404,连 Redis 都不用查。如果它说存在,再走正常的缓存和数据库查询流程。即使有少量假阳性,也只是多查一次 Redis,不影响正确性。

@Component public class ShortCodeBloomFilter { private final BloomFilter<String> bloomFilter; // 预计插入1000万个短码,误判率设为0.01% public ShortCodeBloomFilter() { this.bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.defaultCharset()), 10_000_000, 0.0001 ); } /** * 新增短码时加入布隆过滤器 */ public void add(String shortCode) { bloomFilter.put(shortCode); } /** * 判断短码是否可能存在 * @return false表示一定不存在,true表示可能存在 */ public boolean mightExist(String shortCode) { return bloomFilter.mightContain(shortCode); } }

这里用的是 Guava 的BloomFilter,参数10_000_000是预计插入的元素数量,0.0001是期望的误判率。误判率越低,需要的位数组越大,内存占用越高。1000 万元素、0.01% 误判率的情况下,大约需要 24 MB 内存,完全可以接受。

有一个关键点要注意:布隆过滤器不支持删除。如果短链接过期了,你没法把它从布隆过滤器里移除。所以布隆过滤器只适合做「新增时加入」,不适合做「删除时移除」。过期链接的处理还是得靠 Redis 和 MySQL 的过期机制。如果业务中删除操作很频繁,可以考虑用布谷鸟过滤器,它支持删除,但实现复杂度更高。

另一个实践中的问题是布隆过滤器的预热。服务重启后,布隆过滤器是空的,所有请求都会穿透到 Redis 和 MySQL。解决办法是启动时从 MySQL 批量加载所有短码到布隆过滤器。如果数据量太大加载太慢,可以只加载最近 N 天的短码,更早的短码走正常的缓存查询流程。

验证布隆过滤器是否生效,可以写一个简单的测试:先加入一个短码,然后查询一个明显不存在的短码,看是否直接返回 404 而没有查库。如果日志里没有对应的 SQL 查询,说明布隆过滤器起作用了。

我自己的习惯是,任何对外暴露的查询接口,只要数据量超过百万级,都会先加一层布隆过滤器。它就像一道免费的保安,虽然偶尔会拦错人,但能挡住绝大多数无效请求。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 6:32:59

金融信贷AI智能体搭建实战:基于华为云AgentArts的RAG与工作流编排经验

金融信贷这个场景&#xff0c;做AI智能体跟做通用问答完全是两码事。通用场景下模型答错一句话&#xff0c;用户顶多觉得"这AI不太聪明"&#xff1b;但在信贷审批、贷后管理、合规质检这些环节里&#xff0c;一次错误的判断可能直接对应一笔坏账或者一次监管问责。我…

作者头像 李华
网站建设 2026/10/7 6:32:06

用 WorkBuddy 和开源 Hypit 搭建 AI 短视频爆款复刻工作流

最近这一两个月&#xff0c;我身边做短视频的朋友几乎都在聊同一个话题&#xff1a;腾讯 WorkBuddy 和开源 Hypit 搭在一起&#xff0c;能不能真做到"一句话复刻爆款视频"。我自己运营着三个账号&#xff0c;一个偏知识口播&#xff0c;一个偏产品测评。以前看到爆款…

作者头像 李华
网站建设 2026/10/7 6:31:33

SAP MM自动寻源核心:货源清单与配额协议实操详解

做SAP MM的顾问或者供应链运维&#xff0c;特别是干过几年项目的人&#xff0c;基本都会遇到同一个问题&#xff1a;物料成百上千&#xff0c;供应商也不是独家供货&#xff0c;靠Excel记录“谁家的料应该优先买”根本不现实。更麻烦的是&#xff0c;MRP一跑出来几十上百条采购…

作者头像 李华
网站建设 2026/10/7 6:30:54

Vibe Coding全栈开发实战:AI驱动从需求到落地的完整指南

2025年这轮AI编程浪潮&#xff0c;Vibe Coding确实从一个圈内黑话变成了很多人天天在用的开发方式。我自己做了七八年全栈开发&#xff0c;前两年对AI写代码一直保留态度&#xff0c;觉得无非是个高级补全工具。直到最近半年&#xff0c;我把一个带后台的内容管理项目&#xff…

作者头像 李华
网站建设 2026/10/7 6:30:45

C#上位机控制发那科机器人实战:SDK配置与运动控制

1. 项目概述&#xff1a;为什么用C#去“对话”发那科机器人&#xff0c;而不是别的语言&#xff1f;在工厂自动化产线调试现场&#xff0c;我见过太多上位机工程师对着发那科机器人示教器干瞪眼——明明PLC逻辑跑得飞快&#xff0c;视觉系统也标定好了&#xff0c;可就是卡在“…

作者头像 李华
网站建设 2026/10/7 6:30:21

SiC MOSFET仿真精度瓶颈:沟道效应与Silvaco BCA建模

1. 为什么你的SiC MOSFET仿真总在击穿电压或阈值电压上“差那么一点”&#xff1f;你是不是也遇到过这种情况&#xff1a;明明器件结构参数、掺杂浓度、氧化层厚度都按文献和工艺文件一丝不苟地输进Silvaco TCAD&#xff0c;仿真出来的转移特性曲线却比实测数据高了0.3–0.5 V&…

作者头像 李华