Saas短链接设计与实现
项目介绍
为什么做这个项目?学习还是交付商业?
做这个项目主要有两个出发点
从业务价值出发
短链接在实际场景中是一个非常典型的高并发、高访问的应用,比如:
- 营销推广(短信、广告投放)
- 用户增长(裂变分享)
- 数据统计(点击来源分析)
这些场景有几个特点:读多写少、访问集中、流量突发性强
所以我希望通过这个项目,把:
- 缓存设计(Redis)
- 高并发控制(限流、削峰)
- 分库分表(ShardingSphere)
这些技术串成一个完整的解决方案,而不是零散知识点。
从技术提升角度出发
这个项目我刻意覆盖了一些面试和实际工作中的核心问题,比如:
- 缓存一致性(旁路缓存 + 删除策略)
- 缓存击穿(双重判定锁)
- 缓存穿透(布隆过滤器)
- 高并发削峰(RocketMQ)
- 系统保护(Sentinel 限流降级)
- 分库分表设计(ShardingSphere)
相当于是把**“高并发系统设计的一整套打法”做了一次完整落地**。
这个项目的定位是:“基于真实业务场景设计的高并发系统实践项目”,虽然不是直接商用系统,但在架构设计和问题解决上是对标生产环境的。
缓存与数据库一致性怎么保证?
首先,在项目中,我采用的是旁路缓存(Cache Aside) + 更新时删除缓存策略
整体流程是:
- 读数据:
- 先查 Redis
- 不存在 → 查 MySQL
- 再写回 Redis
- 写数据(更新短链接):
- 先更新数据库
- 再删除缓存
其次,在为了保证高并发下的一致性问题,我做了以下两层优化:
双重判定锁(解决缓存击穿)
场景:
缓存失效瞬间,大量请求打到数据库
我的做法:
1 | 1. 线程A发现缓存没有 |
这样可以保证:
- 同一时间只有一个线程访问数据库
- 避免 DB 被打爆
布隆过滤器(解决缓存穿透)
场景:查询一个根本不存在的短链接
如果不做处理:
- 每次都会打到数据库
我的做法:用布隆过滤器提前判断 key 是否存在
- 不存在 → 直接返回
- 存在 → 再走 Redis / DB
大幅减少无效查询压力
你这个项目是单机部署的吗?
在开发或测试环境下,确实是单机部署(比如本地 Redis、单 MySQL),但:代码层面没有依赖单机特性,随时可以切到分布式部署
首先在应用层可以部署多个Spring Boot实例,通过Nginx做负载均衡,其次Redis可以部署集群,同时,ShardingSphere分库分表架构也可以将数据分散到多个数据库 / 表。
印象深刻的错误及改进方法
在项目早期,其实我的设计是比较简单的:
1 | Redis 查不到 → 直接查数据库 → 写回缓存 |
当时我觉得已经够用了,但在一次压测中发现了一个很严重的问题,缓存失效瞬间,数据库 QPS 瞬间飙升,延迟明显增加。
其实我在做这个项目之前,一直存在一个认知错误,就是我认为缓存击穿只是一个很宽泛的概念,Redis已经足以挡住大部分流量,但是后来我发现:真正危险的是“缓存失效的那一瞬间”,不是平均流量,而是瞬时流量的冲击。
我后面调研了一些方案,最终引入了双重判定锁
1 | 1. Redis miss |
在此基础上,我还做了进一步优化,对于批量创建短链接的情况,如果没有设置过期时间,我在30天默认的情况下,加入一个随机值,避免大量key同时过期,造成缓存雪崩问题。并在每次创建短链接的时候,做好缓存预热。
优化后再做压测,发现没有出现数据库被打爆的情况。
这次经历让我意识到:高并发系统设计的关键,从来不是不是“功能能跑”,而是“极端场景是否稳定”
Redis
有没有关注过缓存的命中率
主要通过两种方法
- 一是在应用侧封装缓存访问的 Proxy/Wrapper,对查询为空(miss)进行计数并结合总请求量计算命中率,便于按业务维度拆分分析。
- 二是redis自带的
INFO stats可以查询命中次数,来计算命中率。
我的项目中短链接一般读多写少,且固定时间段访问频繁,缓存命中率 >= 90%
Redis没找到,回源数据库替换的策略是什么?
我的策略并不是简单查DB,而是做了分层处理:
1 | 1. 查询 Redis |
我不仅关注 Redis 命中率,通过监控 + 埋点持续优化缓存效果;
在缓存未命中时,我采用的是: 布隆过滤器 + 双重判定锁 + 回源 DB + 缓存回填 + 空值缓存兜底
Redis常用命令
创建 key 时直接设置过期时间(最常用)
1
SET key value EX 60 //秒级
给已有 key 设置过期时间
1
EXPIRE key seconds
查看 key 的过期时间
1
2TTL key
50 //还有50秒过期TTL返回值含义
0 | 剩余过期时间 | -1 | key存在但没有过期时间 | -2 | key不存在 |
取消过期时间
1
PERSIST key
具体再讲一下你这个分布式锁是怎么设计的?
我使用 Redisson 实现分布式锁,主要用于缓存击穿场景下的并发控制,
通过:
lock控制并发- 双重判定减少 DB 访问
- Watch Dog 防止锁提前释放(每隔30s自动刷新锁过期时间)
短链接 Redis 过期时间如何设置?
有有效期的短链接
- 例如:7天、30天
- Redis TTL = 短链接过期时间
- 到期自动删除,避免脏数据
永久短链接
不能真的“永久缓存”,否则 Redis 会被打爆
- 设置一个较长 TTL(如 7天 / 30天)
- 每次访问时:延长过期时间(类似 LRU)
短链接缓存的过期时间通常与业务有效期绑定,对于永久链接会设置一个较长 TTL,并结合访问时续期机制,保证热点数据常驻、冷数据自动淘汰。
过期删除策略
惰性删除
当访问 key 时:如果发现过期,直接删除
定期删除
Redis 每隔一段时间随机扫描:过期 key,然后删除。
内存淘汰策略
在设置了过期时间的数据中进行淘汰:
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰:
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
在我的项目中,当 Redis 内存不足时,会通过配置 allkeys-lru 或 lfu 策略淘汰低频访问的短链接,优先保留热点数据。同时所有缓存数据都设置 TTL,避免长期占用内存。即使缓存被淘汰,也可以通过数据库和布隆过滤器兜底,保证系统可用性。
redis如何快速查找到大Key?
直接使用 redis-cli --bigkeys 工具快速定位。对于精确内存占用,可以使用 MEMORY USAGE 命令。找到大 key 后,通常通过拆分 key、异步删除(UNLINK)或优化数据结构来解决,避免影响 Redis 性能。大 key 本质上是设计问题,应该在建模阶段避免,比如短链接系统中不会把所有访问记录存在一个 key,而是通过分片或消息队列异步处理。
Redis扩容后布隆过滤器还能用吗?
Redis Cluster 扩容不会影响布隆过滤器,因为布隆过滤器通常是以一个 key 的 bitmap 存储,Redis 扩容只会迁移 key,不会拆分 key,因此数据不会被破坏。但布隆过滤器本身有容量限制,如果实际数据量超过设计容量,误判率会明显上升,这时候就需要对布隆过滤器进行扩容。
SETNX命令了解吗?
SETNX是Redis提供的“只有key不存在才设置成功”的原子命令,常用于实现分布式锁和幂等控制。
但SETNX只能保证单个操作的原子性,如果业务涉及多个步骤(如设置过期时间),就可能出现异常情况,例如程序在SETNX之后宕机,导致key没有过期,从而产生死锁或幂等失效问题。
为什么不能光使用Redis?
如果只使用Redis的多条命令组合(如exists + set),在高并发场景下会出现竞态条件,导致重复执行。
为什么redis加lua不会产生并发冲突
Lua 脚本在 Redis 内部是原子执行的,多条操作在执行过程中不会被其他客户端打断,因此即使多个请求同时操作 Redis,也不会产生并发冲突。
Lua脚本执行一半失败了重试能恢复吗
Redis Lua 脚本在 Redis 内部是原子执行的,要么全部执行成功,要么执行失败回滚。
因此即使脚本执行一半失败,也不会留下脏数据,可以安全地重试。
布隆过滤器怎么扩容?
一般有三种方式:
第一是重新构建新的布隆过滤器并重新导入数据;
第二是使用分片布隆过滤器,通过 hash 将数据分布到多个 BloomFilter 中;
第三是使用 RedisBloom 提供的可扩展布隆过滤器,让系统自动创建新的 BloomFilter。
Redis 主从架构下分布式锁可能失效吗?
在 Redis 主从架构中,如果使用 Redis 实现分布式锁,确实可能出现锁失效的问题。因为 Redis 的主从复制是异步的,如果客户端在 Master 上加锁成功,但锁还没同步到 Slave 时 Master 就宕机了,此时 Slave 被提升为新的 Master,由于它没有这把锁的数据,其他客户端就可能再次加锁成功,从而导致多个客户端同时持有锁。
常见的解决方案有三种:
第一是使用 Redis 作者提出的 RedLock 算法,通过多个独立 Redis 节点来保证锁的可靠性;
第二是使用 Redis 的 WAIT 命令,在加锁后等待数据同步到从节点;
第三是在业务层增加幂等控制,比如数据库唯一索引等,作为最终保障。
在redis集群模式下,主节点和从节点都挂了,请求到这个主节点负责的key的请求会怎么样
在 Redis Cluster 中,每个主节点负责一部分 hash slot。如果某个主节点及其所有从节点都宕机,那么该主节点负责的 slot 就无法被服务。
如果配置 cluster-require-full-coverage=yes(默认),只要有 slot 无法提供服务,整个集群都会进入 CLUSTERDOWN 状态,所有请求都会失败。
如果配置为 no,那么只有属于该 slot 的 key 访问失败,其他节点负责的 slot 仍然可以正常访问。
布隆过滤器
布隆过滤器占多少空间
主要取决于元素个数,误判率,哈希散列函数的个数,1亿容量,0.01%的误判率的布隆过滤器的大小大概在400MB左右。
Hash
短链接的哈希函数在对称加密和非对称加密怎么选择?
短链接生成主要关注 唯一性和不可预测性。
- 短链接项目,采用的就是 MurmurHash 非加密哈希;
- 如果短链接中有敏感信息,需要不可预测性,可以用 AES 对称加密生成短码并截断;
- 非对称加密太慢,通常只用于签名或验证,不直接生成短码。
ShadingSphere
介绍一下 ShardingSphere
ShardingSphere 是一个开源的分布式数据库中间件,用于解决单库单表在数据量和并发下的性能瓶颈问题。主要分为ShardingSphere-JDBC和ShardingSphere-Proxy。
它的核心原理是对 SQL 进行解析,根据分片键路由到具体数据库或表执行,再将结果进行归并。在我的项目中,我使用 ShardingSphere-JDBC 按 gid 进行分表,保证数据局部性,提高查询效率,同时也意识到分片键设计和扩容是使用过程中需要重点考虑的问题。
短链接表的分表怎么做的?
项目的核心逻辑是 按短链接分组 ID(gid)进行水平分表:
为什么按分组 ID 分表
- 保证同一个分组下的所有短链接都在同一个物理表。
- 优点:
- 查询单个分组的短链接时,无需跨表查询。
- 可以保证数据局部性,减少 join/跨表扫描。
- 方便单分组的统计和管理。
如何实现分表
使用 ShardingSphere + 分库分表策略
分表规则示例:
1
2
3
4
5actualDataNodes: ds_0.t_link_${0..15}
tableStrategy:
standard:
shardingColumn: gid
shardingAlgorithmName: link_table_hash_mod这样同一个 gid 的所有短链接都会落到相同表。
查询和写入透明:
- 应用层只知道逻辑表
short_link。 - ShardingSphere 自动路由到物理表。
- 应用层只知道逻辑表
分库结合分表
- 如果系统大到单库压力大,可以先按 gid 分库,再在每个库内按 gid 分表。
- 这样保证了扩容灵活性和水平可扩展性。
单表性能瓶颈,单表不能超过多少数据量
单表的数据量没有绝对上限,但在实际工程中,InnoDB 单表在 500万到1000万时就需要关注性能问题,超过千万级通常需要分表。瓶颈主要来自 B+树层级增加、Buffer Pool 命中率下降以及写入成本增加。不过具体性能还取决于索引设计、查询方式和并发情况,因此分库分表通常是提前规划,而不是等完全性能崩溃再做。
单表数据量并没有一个严格的上限,但在实际工程中,一般会有一个经验阈值。
通常来说:
- 100万以内:性能基本没问题
- 100万 ~ 1000万:需要关注索引和查询优化
- 1000万以上:开始明显变慢,需要考虑分表
- 5000万 ~ 1亿以上:基本必须拆分(分库分表)
这个瓶颈并不是 MySQL 存不下,而是查询性能和维护成本撑不住,主要体现在几个方面:
- 第一是索引性能下降
- 第二是深分页问题
- 第三是锁竞争和写入压力
- 第四是维护成本高
当单表数据量达到千万级以上时,一般就要考虑优化手段,比如:
- 通过合理索引、覆盖索引优化查询
- 避免深分页(用游标 / 延迟关联)
- 冷热数据分离(历史数据归档)
- 最关键的是:分库分表(比如用 ShardingSphere)
跨gid查询情况
如果要做“计费统计”,本质是什么问题?本质是:“跨分片的聚合查询问题”
比如:
1 | 统计:某个租户创建了多少短链接 |
方案一:项目初期,需要新增这个需求,可以考虑调整分片策略(成本较高),改为按 tenant_id 分库分表
方案二:不直接跨库查,而是:维护一张聚合表(计费表)
1 | tenant_id | short_link_count | update_time |
结合项目里的 RocketMQ 👇
1 | 1. 创建短链接 |
MQ
为什么选择RocketMQ,跟Kafka和RabbitMQ有什么区别
“我选择 RocketMQ 是因为它在吞吐量和可靠性之间取得了较好的平衡,相比 Kafka 更适合业务消息场景,支持延迟消息、顺序消息和事务消息;相比 RabbitMQ 又具有更高的吞吐能力,能够应对短链接系统的高并发访问日志场景,因此更适合作为削峰填谷的消息队列。”
为什么短链接统计要用 MQ?
理论版:
短链接访问量非常大,如果每次访问都同步写数据库,会导致数据库压力过大,并影响请求响应时间。因此系统采用 RocketMQ 进行异步解耦。
用户访问短链接时,只负责发送一条统计消息到 MQ,由消费者异步完成 PV、UV 统计以及数据库更新,从而实现削峰填谷,提高系统的吞吐量和稳定性。
真实场景版:
在我们的短链接项目中,其实最开始是没有使用消息队列的。
当用户访问短链接时,我们除了做跳转,还需要记录大量统计信息,比如 PV、UV、IP、地区、设备、浏览器等信息,并且还要更新 Redis 和数据库。
这些统计操作如果全部在接口同步执行,会导致接口逻辑变得比较重。测试的时候发现,在并发稍高的时候,接口响应时间明显增加,大概会从原本几十毫秒上升到两三百毫秒。
而短链接跳转其实是一个对响应速度要求非常高的场景,如果用户点击链接需要等待很久才跳转,体验会比较差。
所以我们后来把统计相关的逻辑通过消息队列进行异步处理。用户访问短链接时,接口只负责完成跳转,然后把统计数据发送到消息队列中,由消费者异步去处理统计逻辑,比如写数据库、更新统计信息等。
这样改造之后,接口的核心路径就只剩下查询短链接并完成重定向,接口响应时间明显降低,同时统计逻辑也不会影响用户访问体验。
你的消息队列是如何设计的?
在短链接系统中,我使用RocketMQ实现访问统计的异步处理。用户访问短链接后,服务端会收集访问环境信息并发送统计消息到MQ。消费者收到消息后进行PV、UV、UIP以及浏览器、设备、地区等维度的统计,并将数据写入数据库。为了防止重复消费,我通过Redis实现消息幂等控制,并结合RocketMQ的消费重试机制保证消息可靠处理。在高并发场景下,通过Redisson分布式锁保证统计数据的一致性,从而实现了高性能、高可靠的统计系统。
RocketMQ发送消息有几种方式?
RocketMQ 发送消息主要有三种方式:
- 第一是同步发送 syncSend,发送方会等待 Broker 返回发送结果,可靠性最高。
- 第二是异步发送 asyncSend,发送后立即返回,通过回调获取结果,适合高并发场景。
- 第三是单向发送 sendOneWay,不关心发送结果,适合日志等对可靠性要求不高的场景。
你这个 MQ 发送是同步还是异步?
我这里使用的是 RocketMQ 的同步发送,通过 rocketMQTemplate.syncSend 方法发送统计消息。同步发送可以立即获得 Broker 的返回结果,如果发送失败可以及时记录日志或进行补偿。不过同步发送会增加一定的接口延迟,如果在高并发场景下,也可以优化为异步发送来进一步降低接口响应时间。
短链接要求快速重定向,为什么还要同步发送 MQ?
我们最开始选择同步发送,是为了保证统计消息可靠,避免数据丢失。因为初期系统访问量不大,同步发送的延迟对用户体验影响不明显。
随着访问量增加,为了降低核心链路延迟,我们可以优化为异步发送,核心链路只负责跳转,统计逻辑异步处理,同时结合幂等和重试机制保证数据最终一致性。
MQ发送失败怎么办?
一般有几种解决方案:
- 第一是发送失败重试,比如 RocketMQTemplate 支持重试机制。
- 第二是记录日志进行人工排查。
- 第三是使用本地消息表或者事务消息保证最终一致性。
在高可靠场景中,通常会结合本地消息表 + MQ 重试机制来保证消息不丢失。
如果 Redis 或者 MQ 挂了怎么办?
在我们的短链接系统里,其实短链接跳转是核心链路,而统计属于非核心逻辑,所以即使 Redis 或消息队列出现问题,我们也不会让它影响用户的跳转体验。
如果 Redis 或 MQ 出现故障,我们会做一个降级处理。接口在发送统计消息时,如果发现消息发送失败,会先把统计数据记录到本地日志或者本地缓冲队列中,而不会阻塞跳转逻辑。这样用户访问短链接依然可以正常完成重定向。
等消息队列或者 Redis 恢复之后,可以通过一个补偿任务把本地积压的统计数据重新发送到消息队列,再由消费者去完成统计处理。
另外在实际系统中,一般 Redis 会做主从 + Sentinel 或者 Cluster 来保证高可用,消息队列本身也有副本机制,所以完全不可用的情况其实比较少。
那如果统计数据丢了怎么办?( Redis 或者 MQ 挂了)
程序世界是没有银弹的,任何设计都不能既要也要,统计数据本身不是强一致要求的数据,所以即使少量丢失也是可以接受的。但如果业务对数据要求比较高,可以通过消息持久化、重试机制以及补偿任务来减少数据丢失的概率。
MQ为什么需要幂等?
MQ 在某些情况下可能会发生重复投递,例如消费者处理失败或者网络异常导致 ACK 未成功返回,因此同一条消息可能会被消费多次。如果不做幂等控制,会导致统计数据重复累加,例如 PV、UV 统计错误。因此需要通过 Redis 或数据库唯一索引实现幂等控制。
你是如何实现 MQ 幂等的?
RocketMQ 的消息投递语义是 At Least Once,在网络异常或消费失败的情况下可能会出现重复投递,因此消费者必须实现幂等控制。
在我的项目中,我通过 Redis 实现消息幂等。具体做法是为每条消息生成唯一的 MessageId,然后在消费时通过 Redis 的 SETNX 操作设置一个幂等 Key。如果 Key 设置成功,说明当前消费者获得了处理权,可以继续执行业务逻辑;如果设置失败,说明该消息已经被其他消费者处理过。
在业务处理成功后,会将 Redis 中的状态从 “处理中” 更新为 “处理完成”。如果消费过程中发生异常,则会删除该幂等 Key,让消息可以在 RocketMQ 的重试机制下再次被消费。
通过这种方式可以保证同一条消息在高并发和重复投递的情况下也只会被成功处理一次。
分三步来讲:
唯一标识(唯一消息 ID)
每条监控消息携带 短链接 ID + 时间戳 + 请求唯一序号,作为全局唯一 Key。
Redis 或数据库中存储这个 Key,消费者处理时先检查是否已存在:
1
2
3
4if exists(key) then
skip
else
process and set(key, expire_time)这样保证同一条消息只被处理一次。
Redis 实现幂等
- 利用 Redis 的
SETNX或SET key value NX EX ttl- 如果返回 true → 消息第一次处理 → 写入数据库。
- 如果返回 false → 已处理 → 忽略。
- TTL 可以控制幂等记录的生命周期,节省内存。
数据库唯一约束
- 对监控表的
(short_link_id, timestamp)设置联合唯一索引,重复写入会失败,由业务捕获并忽略。 - 结合消息重试策略,保证最终统计正确。
异常怎么处理 + 怎么保证不丢数据 + 怎么避免重复
在我这个短链接监控场景里,如果消费消息过程中业务逻辑出现异常,比如写数据库失败或者下游服务挂了,我不会直接吞掉这个异常,而是会让消息进入重试流程。
具体来说,结合我前面说的状态机幂等,如果执行过程中抛异常,我会把这条消息的状态从 PROCESSING 标记为 FAILED,同时把异常抛出去,让 MQ 感知到这次消费失败。像 RocketMQ 默认是支持重试机制的,它会按照一定的策略(比如延迟队列)把这条消息重新投递。
这样下一次再消费的时候,我会先查 Redis 里的状态,如果是 FAILED,是允许重新处理的,相当于进入“重试分支”。如果之前已经成功(SUCCESS),那就直接跳过,保证幂等。
另外我还会考虑一个边界问题,就是如果一直失败怎么办。比如下游服务一直不可用,这时候不能无限重试,所以我会结合 RocketMQ 的最大重试次数,超过阈值之后,把消息投递到死信队列(DLQ)。对于死信消息,我一般会:
- 做监控告警(比如日志或告警系统)
- 人工排查问题
- 必要的话做补偿处理
还有一个细节是,为了避免消息“卡死”在 PROCESSING 状态(比如服务宕机),我会给这个状态设置一个 TTL,比如几分钟。如果超时了还没变成 SUCCESS,说明可能异常中断了,这时候下一次消费是可以重新抢占处理权的。
如何保证消息队列的顺序性
RocketMQ 顺序性保证方法:
消息发送顺序
RocketMQ 支持 同一个消息队列(Queue)内的顺序消费。
发送消息时,按 短链接 ID 取模选择队列:
1
queue_index = hash(short_link_id) % total_queues
同一个短链接的消息总是进入同一个队列,消费者顺序处理 → 顺序性得到保证。
我们按短链接 ID 对消息进行哈希分队,每个队列的消息使用顺序消费模式(Orderly Consumer),确保同一短链接的访问日志按时间顺序处理,同时结合幂等机制保证最终统计正确。
高QPS下如何高效消费?
在我的短链接系统中,高 QPS 场景下主要通过以下方式提升消费能力:首先通过增加 RocketMQ 的队列数量和消费者实例来提升并发消费能力;其次采用批量消费和批量写库减少 IO 开销;同时通过消息队列实现削峰填谷,避免高峰流量直接冲击数据库;另外结合 Redis 和分库分表优化存储性能,从而保证系统在高并发场景下依然能够稳定处理消息。
MQ消费失败怎么办?
如果消费者抛出异常,RocketMQ 会自动触发重试机制,消息会重新投递给消费者,直到消费成功或者达到最大重试次数。
MQ消息丢失怎么办?
我们采用同步发送,并且RocketMQ本身提供刷盘机制保证消息持久化。此外还可以通过消息重试和死信队列机制保证消息最终被消费。
MQ消息积压怎么办?
可以通过增加消费者实例数量来提升消费能力,同时RocketMQ支持分区队列,可以并行消费。另外也可以通过监控消费堆积情况及时扩容消费者。
为什么使用 Redisson 锁?
因为短链接访问量可能非常高,在高并发情况下多个线程同时更新统计数据可能会导致数据不一致,因此使用 Redisson 分布式锁保证同一时间只有一个线程更新统计数据。
PV UV UIP 怎么统计?
PV 是页面访问量,每访问一次加一;UV 是独立访客,通过用户标识或者 Cookie 判断是否第一次访问;UIP 是独立 IP,通过 IP 去重判断是否首次访问。
Sentinel相关
项目中的限流是怎么做的,用的什么限流算法?
我在项目中使用 Sentinel 做接口限流,底层采用滑动窗口算法来统计 QPS,相比固定窗口更平滑,能够有效避免流量突刺。当请求超过阈值时会触发限流降级,同时也可以结合 WarmUp 或匀速排队模式应对突发流量,保证系统稳定性。
为什么使用 Sentinel 做限流?
在短链接系统中,创建短链接接口属于写操作,如果在高并发或者恶意请求的情况下,大量请求可能会同时访问数据库和 Redis,从而导致系统资源被耗尽。因此我们引入 Alibaba Sentinel 对关键接口进行限流。
Sentinel 可以通过 QPS 限流规则限制接口的访问频率,当请求超过阈值时直接进行降级处理,从而避免系统被突发流量压垮,保证系统稳定运行。
Sentinel 是怎么做限流的?
Sentinel 底层采用 滑动窗口算法 统计单位时间内的请求数量。
具体流程是:
1 | 请求进入 |
当超过限流阈值时,Sentinel 会直接抛出 BlockException,然后执行我们定义的 blockHandler 降级方法返回友好提示。
为什么只对创建短链接接口限流?
短链接系统中有两类接口:
读接口
- 短链接跳转
- 短链接查询
这些接口通常:QPS非常高、逻辑简单、基本走缓存
不适合限流。
而 创建短链接接口属于写操作,涉及:
- 数据库写入
- Redis写入
- 唯一性校验
成本较高,因此更适合进行限流保护。
限流之后是怎么降级的?
当 Sentinel 触发限流规则后,会抛出 BlockException,然后执行我们定义的 blockHandler 方法。
例如在创建短链接接口中,我们自定义了降级方法:createShortLinkBlockHandlerMethod
当请求被限流时,不再执行创建短链接的业务逻辑,而是直接返回:当前访问人数过多,请稍后再试
这种方式可以快速失败,从而保护系统资源。
改进设计
短链接生成高并发时怎么加速?
我会从业务分级、存储优化、缓存优化、算法优化四个方向考虑。
- VIP 用户优先:可以给 VIP 用户走独立通道,保证核心客户体验。
- 分库分表:将短链接表拆分,避免热点表写入压力,提高 DB 并发能力。
- Redis + 分布式 ID:使用 Redis 自增或者雪花 ID 快速生成唯一短码,同时将短链映射缓存在 Redis,减少 DB 写入压力。Redis 可通过 Cluster 或 Sentinel 扩容。
- 更快的哈希函数:选择 MurmurHash,减少 CPU 计算开销,并且避免冲突。
- 异步或短码池:预生成短码,用户请求时直接取,用内存/缓存响应,进一步提高生成速度。
短链接存满了怎么扩容?占用多少空间?
当短链接存满或访问量增大时,我会从数据库和缓存两方面扩容。数据库通过 ShardingSphere 做水平分库分表,保证写入和存储均摊,同时可以归档长期不活跃的短链接释放空间;Redis 使用集群分片扩容,并结合 LRU 淘汰策略控制内存。空间占用上,每条短链接约 150200 字节,1 亿条数据大约 2025GB,在数据库和缓存中都可以通过水平扩展和归档策略处理大规模增长。
其他
短链接输入到页面展示
用户访问短链接从网络角度看,就是一个标准的浏览器 HTTP 请求流程:
- DNS 解析 → 得到 IP
- TCP/TLS 建立连接
- HTTP 请求短链接 → 服务器返回 302 重定向
- 再次连接 → 再次1,2步骤
- 浏览器请求目标 URL → 返回页面资源
- 浏览器渲染 → 页面展示
统计数据可以通过服务端异步发送 MQ 或写入数据库,不影响跳转速度。
JVM调优参数
首先针对堆内存设置-Xms4g -Xmx4g避免动态扩容,然后根据项目高并发特点选择G1GC并配置-XX:MaxGCPauseMillis=200控制停顿时间,最后开启-XX:+PrintGCDetails等监控参数便于问题排查。
注意
- 生成的短链接是需要保障在当前域名下唯一的




