资讯动态

Java SaaS短链接系统源码拆解:从短码生成到Redis Stream统计

发布时间:2026/9/28 8:25:23 来源:尧图企业网站定制
简介一套基于Java的SaaS短链接管理系统设计源码面向需要快速构建短链接服务的企业开发者与Java学习者。系统将长网址转换为短链接提供PV、UV等关键访问数据统计并具备安全监控与隐私保障能力适用于营销推广、链接管理和数据分析等场景。压缩包共219个文件以188个Java源文件为主承担业务逻辑与核心算法XML与YAML文件负责系统配置管理SQL文件用于数据库交互另有Lua脚本和HTML页面分别支撑功能扩展与前端展示整体大小563KB。源码模块划分清晰涵盖短链接生成、访问统计监听与消费、回收站管理、用户与分组管理等完整业务链路便于深入研读SaaS平台的分层设计与扩展方式。目前已有389人学习参考适合希望掌握短链接分析机制、模块化架构及企业级开发实践的读者。1. Java SaaS 短链接管理系统一套源码里藏着完整业务闭环做营销投放的人应该都有这种体会长链接带一堆跟踪参数贴进朋友圈、短信、抖音评论区又长又丑还容易被截断。短链接系统看着就是把长 URL 变短但真正落到代码层面它背后是一套完整的 Java 技术栈——多租户隔离、网关过滤、Redis Stream 异步消息、PV/UV/UUI 统计、回收站机制。这套源码一共 219 个文件188 个 Java 源文件把核心业务全部覆盖我把模块一个个拆开看过之后确认它不是一个 Demo而是一个能跑通完整业务闭环的 SaaS 短链接平台。如果你是 Java 后端开发者或者正在准备 SaaS 方向的项目经验这套源码的价值在于短链接表面上是算法题里的 62 进制转换实际工程里要解决的问题是短码冲突、统计消息不丢、租户数据串号、回收站恢复冲突这些脏活累活。本篇我不讲虚的直接按源码里的核心类一条条拆把生成链路、统计链路、SaaS 隔离和部署验证全部落到可操作的代码和参数上。2. 模块与数据模型gateway、aggregation、project 怎么分工2.1 从文件结构反推系统的模块边界拿到源码第一件事不是看代码而是先看目录。这套项目的包结构按照功能拆成了 admin、gateway、aggregation、project 四个大方向。初次打开可能有点懵这四个词不像 MVC 那样直观但它们的职责边界其实很清楚gateway网关卡口处理两件事——外部请求的登录态校验以及短链接跳转的长转短入口。它负责把非法请求挡在业务逻辑之外。project核心业务主体承载短链接实体、分组实体、用户实体和对应的 Service 实现。ShortLinkServiceImpl、GroupServiceImpl、UserServiceImpl 都在这个模块里。aggregation聚合层解决跨模块数据查询的问题。比如后台首页要展示 PV、UV、UUI 趋势如果直接去查 project 的业务表不仅耦合重而且统计 SQL 会把核心表拖慢。aggregation 专门干这件事。admin后台管理能力通常与网关配合给运营人员提供管理端接口。这个拆分方式在 SaaS 项目里很常见叫纵向按业务域切分、横向按访问层级切分。我一般会建议团队这样理解project 是底盘gateway 是门卫aggregation 是分析室admin 是控制台。只有理解了这层设计后面看代码才不会迷路。2.2 数据模型短链接、统计、回收站三张核心表接着看数据库脚本两个 SQL 文件里定义了完整的数据模型。我梳理了核心表关系重点关注短链接主表、点击统计表、回收站表它们构成了整个系统的数据骨架。表名核心字段作用短链接主表短码、长链接、分组ID、用户ID、过期时间、删除标记存储短码与原始链接映射点击统计表短码、PV数、UV数、UUI数、统计日期按天或按小时维度的访问聚合回收站表短码、删除时间、过期清理时间实现软删除与恢复这里有个容易被忽略的设计点短链接表里同时有 create_time 和 del_flag但回收站不是简单地 UPDATE 删除标记而是把记录单独挪到回收站表。为什么这么做因为 del_flag 只能解决查询时不可见解决不了30 天后自动清理的定时任务。单独建表后清理任务只需要扫回收站表不会误伤正常数据。再说统计表它存的是聚合结果而不是原始日志。PV、UV、UUI 这三个指标里PV 好算一次点击加一UV 需要用 Redis 去重UUIUnique User Identifier需要基于用户标识做基数统计。这套源码的做法是统计消息进来先写 Redis Stream消费者异步消费后按小时聚合再落地到统计表。后续章节我细讲这条链路。3. 核心链路一短链接生成与 Redis Stream 消息队列3.1 ShortLinkServiceImpl短码生成、冲突检测、落库短链接系统的核心操作就一个把长链接变成短码。先看 ShortLinkServiceImpl 里最关键的生成方法我摘出主流程去掉与业务无关的边角代码public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) { // 1. 基于自增ID做62进制压缩得到一串短码 String shortLinkSuffix Base62Utils.encodeToBase62Sequence( distributor.generateId() ); String fullShortUrl requestParam.getDomain() / shortLinkSuffix; // 2. 防止短码撞车先查一次如果已存在则重新生成 ShortLinkDO shortLinkDO baseMapper.selectOne( Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getShortUrl, fullShortUrl) ); if (shortLinkDO ! null) { // 常见做法是换一个ID再生成或者加随机盐 shortLinkSuffix Base62Utils.encodeToBase62Sequence( distributor.generateId() ThreadLocalRandom.current().nextInt(100) ); fullShortUrl requestParam.getDomain() / shortLinkSuffix; } // 3. 组装实体落库 ShortLinkDO saveDO ShortLinkDO.builder() .domain(requestParam.getDomain()) .originUrl(requestParam.getOriginUrl()) .fullShortUrl(fullShortUrl) .gid(requestParam.getGid()) .createTime(new Date()) .delFlag(0) .build(); baseMapper.insert(saveDO); return ShortLinkCreateRespDTO.builder() .fullShortUrl(fullShortUrl) .build(); }逻辑说明第一步用分布式 ID 生成器拿一个全局唯一 ID再转成 62 进制字符串这就是短码。第二步为什么要再查一次因为理论上 ID 不会重复但转换后的字符串在极端情况下可能撞库所以源码里做了二次校验这也是我建议你保留的兜底逻辑。第三步落库时注意 delFlag 默认 0后续回收站逻辑会依赖这个字段。参数说明里有个细节短码长度取决于 ID 位数。62 进制下1 位能表示 62 个值2 位能表示 3844 个值一般业务量级下 6 位就够。如果你接手后要改短码长度改的不是这里而是 ID 的位数或进制表长度别在生成方法里写死。3.2 RedisStreamConfiguration为什么用 Redis Stream 而不是 RabbitMQ这套源码的统计链路没有引入额外 MQ 组件而是用 Redis Stream。先看配置类Configuration public class RedisStreamConfiguration { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object redisTemplate new RedisTemplate(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } Bean public StreamMessageListenerContainerString, Object streamMessageListenerContainer( RedisConnectionFactory factory, StreamMessageListenerContainer.StreamMessageListenerContainerOptionsString, Object options) { StreamMessageListenerContainerString, Object container StreamMessageListenerContainer.create(factory, options); container.start(); return container; } }逻辑说明这里注册了一个 Stream 消息监听容器消费者小组基于它接收短链接点击统计消息。Stream 相比 Redis List 的优势是支持消费者组模式多个消费者实例可以分摊消息同时保证一条消息只被一个消费者处理一次。在短链接这种高点击场景下这比 List 的 BRPOPLPUSH 可靠得多。参数说明options 里可以配置拉取批次大小我一般设batchSize(10)pollTimeout 设 2000ms。批次太大会导致单次处理时间过长消费者组 Rebalance 时容易重复消费批次太小则消费吞吐上不去。这个参数要按你的单条消息处理耗时来调——处理 1ms 以内的消息批次 1030 都行处理耗时超过 10ms建议压到 5 左右。选择 Redis Stream 而不是 RabbitMQ核心原因是这套系统里 Redis 本来就在链路中——短链接跳转要查缓存UV 去重要用 Redis Set统计预算也用 Redis。再加一个 MQ 组件等于多维护一套集群不划算。如果你的团队已经有成熟的 MQ 基础设施把 Stream 换成 MQ 也可以但消息体的数据结构不用动这是 Stream 方案留给你的迁移余地。4. 统计链路与租户隔离PV/UV/UUI 和 Group/RecycleBin4.1 ShortLinkStatsMsgConsumer统计消息的消费与聚合点击短链接后请求会触发一个统计消息写入 Redis StreamShortLinkStatsMsgConsumer 负责消费。看核心流程Component RequiredArgsConstructor public class ShortLinkStatsMsgConsumer implements StreamListenerString, Object { private final ShortLinkStatsService shortLinkStatsService; Override public void onMessage(RecordString, Object message) { RecordId recordId message.getId(); Object value message.getValue(); if (value instanceof Map) { MapString, Object statsRecord (MapString, Object) value; // 1. 从消息体里拆出短码、IP、UA、用户标识 String fullShortUrl statsRecord.get(fullShortUrl).toString(); String ip statsRecord.get(ip).toString(); String userAgent statsRecord.get(ua).toString(); String uvId statsRecord.get(uvId).toString(); // 2. 按天维度做PV/UV/UUI统计 Integer pv 1; String uvKey short_link:uv: fullShortUrl : LocalDate.now(); Boolean isNewUv redisTemplate.opsForSet().add(uvKey, uvId) 0; Integer uv isNewUv ? 1 : 0; // 3. 更新或插入统计表 shortLinkStatsService.recordStats(fullShortUrl, pv, uv); } // 4. 手动ACK防止消息丢失 streamOperations.acknowledge(short-link-stats, stats-consumer-group, recordId); } }逻辑说明这段代码就是 PV/UV 统计的最小实现。PV 是事件计数每条消息加 1UV 依赖 Redis Set 的去重能力第一次出现的 uvId 返回 1后续重复访问返回 0累加到当天统计里。UUI 的统计逻辑与 UV 类似只是标识不同——UV 偏 IP UAUUI 更偏业务侧用户标识。参数说明uvId 这个字段的取值决定 UV 准确率。如果 ua 和 ip 拼接作为 uvId同一个手机用 WiFi 和 4G 切换 IP 时会被误判为两个访客。我一般建议优先用设备指纹拿不到就用ip 前几段 UA 屏幕分辨率做拼接能显著降低误判率。isNewUv 判断用的 opsForSet().add() 返回值添加成功返回大于 0说明这个 uvId 第一次来。4.2 GroupServiceImpl 与 RecycleBinServiceImplSaaS 租户隔离与回收站机制SaaS 系统的核心不是功能多而是租户之间不串数据。看 GroupServiceImpl 里查询分组的方法public ListGroupRespDTO listGroup() { // 从登录上下文获取当前用户名作为租户隔离维度 String username UserContext.getUsername(); ListGroupDO groupDOList groupMapper.selectList( Wrappers.lambdaQuery(GroupDO.class) .eq(GroupDO::getUsername, username) .eq(GroupDO::getDelFlag, 0) .orderByDesc(GroupDO::getSortOrder) ); return BeanUtil.convertToList(groupDOList, GroupRespDTO.class); }逻辑说明SaaS 租户隔离最常见的实现方式是逻辑隔离即每条业务数据带租户标识查询时强制带条件。这里用的是 username 作为租户维度所有分组查询都带上用户名条件防止 A 用户看到 B 用户的分组。如果你在改造这套源码可以考虑把 username 替换成 tenantId结构不变但语义更通用。再看 RecycleBinServiceImpl 的回收与恢复逻辑public void moveToRecycleBin(String gid, String fullShortUrl) { // 1. 查出短链接记录 ShortLinkDO shortLinkDO shortLinkMapper.selectOne( Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, gid) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getDelFlag, 0) ); if (shortLinkDO null) { throw new ServiceException(短链接记录不存在); } // 2. 写入回收站表 RecycleBinDO recycleBinDO RecycleBinDO.builder() .gid(shortLinkDO.getGid()) .fullShortUrl(shortLinkDO.getFullShortUrl()) .delTime(new Date()) .build(); recycleBinMapper.insert(recycleBinDO); // 3. 原表标记删除 shortLinkMapper.update(null, Wrappers.lambdaUpdate(ShortLinkDO.class) .eq(ShortLinkDO::getGid, gid) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .set(ShortLinkDO::getDelFlag, 1) ); }逻辑说明回收站不是 DELETE而是挪窝——先往回收站表插入一条带删除时间的记录再把原记录的 delFlag 置为 1。这样原表查询自动过滤掉删除数据同时回收站保留了恢复所需的完整信息。参数说明delTime 字段用来做 30 天自动清理定时任务只需要扫回收站表delTime now - 30d的记录物理删除即可不影响正常数据。恢复时有个关键检查要确认短码没有被别人重新使用。短码资源在被删除后可能被新链接复用如果恢复时不做冲突检测会出现两个长链接共用同一个短码的脏数据。这是我在实际项目里踩过的坑后面避坑章节细说。5. 常见问题与避坑五个最容易翻车的点5.1 短码冲突导致旧链接失效现象生成的短链接刚上线时正常但某个短码访问量突然归零排查发现是另一个长链接占了同一个短码。原因62 进制转换的 ID 空间相对有限在高并发生成场景下如果 ID 生成器回退或时钟回拨可能出现短码重复。源码里虽然有二次查重但查重后与新记录之间没有唯一索引兜底并发窗口下依然可能插入重复短码。解决在短链接表的 full_short_url 字段上加唯一索引这是最靠得住的兜底。同时把短码生成逻辑里的查重改为主键冲突捕获重试也就是先 insert捕获 DuplicateKeyException 后重新生成短码不要先查后插——先查后插在高并发下形同虚设。5.2 Redis Stream 消费者组消息重复消费现象统计数据显示 PV 明显高于实际访问量某些短链接一天内的 PV 几乎是 UV 的 20 倍。原因消费者处理完消息、写库成功之后还没来得及 ACK进程就宕机了。消息会重新进入 Pending 列表恢复后同一批消息会被再次投递导致 PV 重复计数。解决把统计操作设计成幂等。我的做法是在统计消息体里带上一个全局唯一递增的 eventId消费时先查这个 eventId 是否处理过处理过则直接 ACK。如果你不想加这个查询开销至少要在 ACK 前完成全部业务操作不要先 ACK 再异步写库。5.3 UV 统计被 Set 过期时间坑掉现象昨天 UV 还是 1.8 万今天回看变成 1.2 万数据缩水了。原因Redis Set 里存 UV 去重的 key 设置了过期时间但过期时间是从键第一次写入开始算的。如果某个短链接的 Set 键在凌晨过期而当天又有大量新访问夜里 23 点过后的访问全都算成新 UV数据就飘了。解决给 UV Set 的 key 设置过期时间时用当天剩余时间 1 天作为 ttl比如现在是下午 3 点过期时间设为 9 小时确保 key 在当天 24 点之后才过期。另外统计任务要做好对账每天凌晨比对前一天的 UV 趋势出现跳水要能告警。5.4 回收站恢复时短码已被占用现象用户把短链接移入回收站第二天恢复时提示失败但原链接确实没有被物理删除。原因短码从回收站清掉后又被新链接使用。恢复逻辑只检查了回收站表里有没有这条记录没检查短链接主表里有没有同名短码。解决恢复前先查主表如果 full_short_url 已存在且 del_flag 0说明短码被占此时要么生成新短码要么提示用户换一个恢复时间。我改这个逻辑时会复用 createShortLink 里的冲突检测方法而不是重新写一遍。5.5 多租户查询漏带租户条件导致串数据现象线上反馈用户 A 看到了用户 B 的短链接访问记录但用户体系是独立的账号密码都查不到问题。原因部分统计查询接口没有经过 service 层统一封装直接在 mapper 层用selectByShortUrl查询漏掉了租户隔离条件。Java 这种动态拼接 SQL 的项目里多写一个条件容易漏少写一个条件就是安全事故。解决把所有租户相关查询收敛到统一的 Service 方法严禁 Controller 直接注入 Mapper。同时在 SQL 层面加拦截器自动为所有带 tenant_id 或 username 字段的表追加隔离条件。上线前用两个测试租户做交叉验证A 租户登录后确认访问不了 B 租户的任何数据这是 SaaS 系统的底线测试。6. 压测与验证回放脚本确认统计链路没白写改动完上述逻辑最怕的是功能看着正常但压力一上来就翻车。我的习惯是写一个回放脚本模拟并发点击短链接然后验证 PV/UV 是否与预期一致。下面是我常用的一段压测模拟脚本本质上是往 Redis Stream 里灌消息import redis import json import uuid import time import random r redis.Redis(host127.0.0.1, port6379, db0) SHORT_URL https://demo.com/abc123 STREAM_KEY short-link-stats GROUP_NAME stats-consumer-group # 模拟200个独立的UV标识 uv_ids [uuid.uuid4().hex for _ in range(200)] # 每个UV标识访问1到3次模拟PVUV的效果 for uv_id in uv_ids: times random.randint(1, 3) for _ in range(times): msg { fullShortUrl: SHORT_URL, ip: f10.0.{random.randint(0, 255)}.{random.randint(1, 254)}, ua: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7), uvId: uv_id, eventId: uuid.uuid4().hex } r.xadd(STREAM_KEY, msg, maxlen100000) time.sleep(0.001)逻辑说明脚本的核心是制造一个已知的正确答案——200 个 UVPV 总量在 200 到 600 之间。跑完脚本后去统计表里查这个短链接当天的记录如果 UV 等于 200说明 Redis Set 去重逻辑正确如果 PV 等于脚本发送的总消息数说明消费者一条没丢。用已知数据校验消费链路比凭感觉看数字靠谱得多。参数说明maxlen100000 设置了 Stream 的最大长度防止压测数据长期占用内存。实际生产环境这个值建议调到 50 万到 100 万之间短链接量大但消息体很小占不了多少内存。send 端的 sleep(0.001) 是限速用否则单机瞬间塞几十万条消息消费者的消费速度跟不上pending 列表会暴涨。压测时重点盯三个指标Stream 的 pending 数量、消费者组的 lag、统计表写入速率。pending 持续上涨说明消费者消费不过来优先看消费逻辑里有没有耗时操作lag 归零说明消费追平了链路健康。我那套系统上线前就是这么压的压完后透传到测试环境跑了一周确认没有消息积压后才敢上生产。从那以后每次改统计链路我都强制走一遍回放脚本确认当前版本和上一版本的 PV/UV 数字对得上再合代码——否则统计数字是错的全靠运气发现这比功能 bug 可怕多了。这套源码值得下载的场景就是想搞懂短链接统计链路怎么用 Redis Stream 落地或者想参考 SaaS 多租户隔离的分寸感。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑