发布时间:2026-02-19
浏览次数:0
在广告投放场景里,在短信通知场景中,在社交媒体等场景之中,冗长的原始URL不但影响用户体验,而且还可能超出字符限制,短链系统成为后端开发的基础刚需。然而普通短链系统容易出现高并发瓶颈问题,普通短链系统存在数据一致性差问题,普通短链系统有存储膨胀等问题,无法支撑十亿级数据量级。本文会从专业角度拆解十亿级短链系统的设计逻辑,本文会结合实战代码落地,本文会总结生产环境高频踩坑点,助力后端开发者快速搭建高性能、高可用的短链服务。
十亿级短链系统的核心诉求与技术难点
短链系统核心在于“长URL→短码→长 URL”这种映射转换,然而在十亿级量级的情况下,系统设计要突破普通短链的局限,还要结合近期后端技术热点来满足以下核心诉求,并且要应对对应的技术难点。
读写具备高并发特性:短链系统里典型的读写比例大于一百比一,跳转的请求远远多于生成请求,数量高达十亿级别的数据要能够支撑每秒十万以上的QPS,而且延迟必须控制在十毫秒以内,以此来防止用户出现点击之后卡顿的情况。
数据存在唯一性以及一致性:相同的长URL要返回一样的短码,以此来防止资源被浪费,与此同时,在分布式部署的状况下要确保多节点的数据能够同步,进而避免短码出现冲突。
3. 高可靠以及可塑性:必须达到在一周七日全日任意时刻都能正常运转,具备极高的容错能力,并且能够依据数据总量的扩充变化而变得更具容纳多种情况的能力,去处理猛然突然增加涌现的流量(就像开展活动投放时迎来的高峰值那种情况)。
防止短码暴力枚举,防止恶意URL跳转,确保安全性,短码要简洁,需5至8位,要适配URL场景,避免特殊字符转义麻烦,实现轻量化。
要做到十亿级 URL 映射数据方面存储高效,就得控制存储成本,得避免冗余情况发生,并且与此同时,要保证查询效率,还得杜绝全表扫描。
综合2026年之后的后端技术热点情况,当下主流的解决方案运用“分布式ID生成,缓存分层,主从数据库”这样的架构,以此避开传统单体短链系统存在的性能瓶颈,与此同时还融入编码以及Redis缓存优化等具有实战性质的技术,从而兼顾性能以及可维护性。
十亿级短链系统的底层逻辑拆解
包含十亿级短链的系统,其核心底层逻辑,能够概括成“3 大核心模块 +1 套优化体系”,每个模块的底层道理,直接对系统的性能上限起到决定性作用,下面将逐个进行剖析:
(一)核心模块1:短码生成原理(系统基石)
短码属于短链系统的核心标识内容,其底层核心为“唯一ID朝着短码”的编码转换操作,核心要求有以下几点:它得够短(长度范围是5至8位),具备唯一性,呈现出不可预测状态,并且不存在特殊字符。当下主流方案的对比情况以及底层逻辑情形如下:
1. 方案对比(底层逻辑差异):
2. 优势处于编码底层:为什么不进行选用呢?仅仅只包含字母以及数字,没有诸如/、+等这类特殊字符,自然而然地符合URL的要求,用不着进行转义操作;然而其他的特殊字符于URL里是需要转义的,这般会使得短码的长度增加,进而对体验造成影响。62进制的位数与其能够进行表示的范围存在对应关系:6位的时候能够表示568亿,7位的时候能够表示3.5万亿,完全可以满足十亿级别的数据需求。
(二)核心模块2:存储原理(高可用关键)
数据存储达到十亿级别的规模时,需要同时兼顾查询速度快,容量足够大,具备很高的可用性,其底层所运用的是Redis缓存加上MySQL主从这样的分层存储架构,它的原理是以下这样:
1. Redis缓存层,也就是热点数据所在层,其底层借助Redis内存操作特性,存储热点短码到长URL的映射,查询延迟小于1ms,来支撑高并发读写,核心设计是Key为short:{},Value是长URL,TTL依据访问热度动态设置,热点永久缓存,冷数据设短期TTL,同时还有缓存穿透防护,对于不存在的短码缓存空值如“”,TTL设为60秒,防止恶意刷量压垮数据库。
2. MySQL存储层,这里指的是全量数据部分:它的底层运用主从复制架构,主库承担写入工作,具体涵盖短码与长URL映射、自定义短码校验等方面,从库负责读取操作,以此来分摊查询压力。其核心优化措施为:针对长URL构建MD5哈希索引,这样做既能节省存储空间,又能够快速达成“相同长URL返回相同短码”的去重逻辑,进而避免冗余数据的出现。
(三)核心模块3:跳转原理(用户体验核心)
用户对短链进行点击之后的跳转方面的逻辑,在底层所依靠的是HTTP重定向这一机制,其核心流程是下面这样:
用户发出请求,请求链接为,https协议的,short.com这个网址,其为短链。
2. 负载均衡器将请求分发至应用服务器;
3. 应用服务器会先去查询Redis缓存,以此来获取对应的长URL,要是缓存没有被命中的话,那就去查询MySQL从库,在获取相关内容之后再同步至Redis。
应用服务器给出HTTP 302这般的重定向响应,其字段被设置成长度较长的URL。
5. 用户浏览器自动跳转至原始长URL。
处于底层的优势在于,302重定向的时候,不需要应用服务器来转发内容,仅仅是返回跳转相关的指令,如此一来能够降低服务器所承受的压力,与此同时还能够支持短链统计,诸如点击次数、地理位置这些方面,进而适配业务的需求。
(四)优化体系:十亿级量级的性能兜底原理
将此进行以分布式方式展开的部署优化,具体做法是运用微服务架构,就是要拆开URL生成服务,分解重定向服务,分裂统计服务。然后让各个服务能够独立地实现扩容,以此方式来避开单体所产生的瓶颈。
2. 限流,采用底层利用或Nginx的方式,来实现流量限制,以此防止突然突发流量将整个系统击垮啦;另外,针对于异常节点,像Redis出现宕机这种情况的,要进行熔断操作,并且要做到能够自动切换到备用节点中噢。
3. 存储方面的扩容优化举措:MySQL运用分库分表的方式且是按照短码首字符进行哈希分表的,Redis采用集群模式来实现此种效果,以此来支撑数据量能够以一种线性状的方式进行扩容。
十亿级短链系统落地步骤(Java+Redis+MySQL)
此次实战运用Java 25 LTS ,加上Boot 4.0 ,再加Redis 7 .2 ,以及MySQL 8 .0 ,其附和2026年之后的后端技术热点 ,步骤是清晰的所以能够落地 ,覆盖了短码生成 、存储 、跳转的整个流程 ,并且一并适配十亿级的数据量级。
(一)环境准备(二)步骤1:数据库设计(MySQL)
设计一个表,用于存储短码跟长 URL 的映射关系,要适配达到十亿级别的数据,还要添加唯一索引以及哈希索引intellij idea logo,以此来优化查询以及去重性能。
CREATE TABLE `url_mapping` (
`id` BIGINT NOT NULL COMMENT 'Snowflake分布式ID',
`short_code` VARCHAR(10) NOT NULL COMMENT 'Base62编码短码,5-8位',
`long_url` TEXT NOT NULL COMMENT '原始长URL',
`custom` BOOLEAN DEFAULT FALSE COMMENT '是否为自定义短码',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`expires_at` TIMESTAMP NULL COMMENT '过期时间(可为NULL,永久有效)',
`click_count` BIGINT DEFAULT 0 COMMENT '点击次数',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_short_code` (`short_code`) COMMENT '短码唯一索引,防止冲突',
INDEX `idx_long_url_hash` ((MD5(`long_url`))) COMMENT '长URL哈希索引,加速去重查询'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT 'URL短链映射表';
描述:主从复制部署省略(参照MySQL官方文档),从库只开放读取权限,主库承担写入职责,分担查询压力。
(三)步骤2:核心工具类实现
进行ID生成工具类的实现,以及编码工具类的实现,这属于短码生成的关键核心部分,目标是适配高并发的场景。
// 1. Snowflake ID生成工具类(分布式ID,支持多节点无冲突)
public class SnowflakeIdGenerator {
// 符号位(1位,固定0)、时间戳(41位)、机器ID(10位)、序列号(12位)
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
// 时间戳偏移量(2026-01-01 00:00:00的时间戳)
private static final long TIMESTAMP_OFFSET = 1778083200000L;
// 机器ID偏移量、序列号偏移量
private static final long MACHINE_ID_SHIFT = 12;
private static final long SEQUENCE_SHIFT = 0;
// 机器ID最大值(10位,0-1023)
private static final long MAX_MACHINE_ID = (1L << 10) - 1;
// 序列号最大值(12位,0-4095)
private static final long MAX_SEQUENCE = (1L << 12) - 1;
// 构造方法,传入机器ID(0-1023)
public SnowflakeIdGenerator(long machineId) {
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException("机器ID超出范围(0-1023)");
}
this.machineId = machineId;
}
// 生成分布式ID
public synchronized long generateId() {
long currentTimestamp = System.currentTimeMillis();
// 防止时钟回拨
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,无法生成ID");
}
// 同一毫秒,序列号自增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 序列号耗尽,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 新的毫秒,序列号重置为0
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 组合ID:时间戳偏移 + 机器ID + 序列号
return ((currentTimestamp - TIMESTAMP_OFFSET) << (MACHINE_ID_SHIFT + SEQUENCE_SHIFT))
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
// 等待下一毫秒
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
// 2. Base62编码工具类(URL友好,无特殊字符)
public class Base62Utils {
// Base62字符集(0-9、a-z、A-Z,共62个字符)
private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = 62;
// 长整数编码为Base62短码
public static String encode(long num) {
if (num == 0) {
return String.valueOf(CHARS.charAt(0));
}
StringBuilder sb = new StringBuilder();
while (num > 0) {
long rem = num % BASE;
sb.append(CHARS.charAt((int) rem));
num /= BASE;
}
// 反转字符串(因为编码时是从低位到高位)
return sb.reverse().toString();
}
// Base62短码解码为长整数
public static long decode(String shortCode) {
if (shortCode == null || shortCode.isEmpty()) {
throw new IllegalArgumentException("短码不能为空");
}
long num = 0;
for (int i = 0; i < shortCode.length(); i++) {
char c = shortCode.charAt(i);
int index = CHARS.indexOf(c);
if (index == -1) {
throw new IllegalArgumentException("短码包含非法字符:" + c);
}
num = num * BASE + index;
}
return num;
}
}
(四)步骤3:核心业务逻辑实现( Boot)
达成URL短链生成这一核心接口,实现URL短链跳转这一核心接口,开展自定义短码校验这一核心接口,进行Redis缓存的整合,使之适配高并发场景。
// 1. 配置类(Redis配置、Snowflake配置)
@Configuration
public class AppConfig {
// Redis配置(集群模式)
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 序列化配置(避免乱码)
StringRedisSerializer serializer = new StringRedisSerializer();
template.setKeySerializer(serializer);
template.setValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
// Snowflake ID生成器(机器ID可从配置文件读取,多节点部署时设置不同值)
@Value("${snowflake.machine-id}")
private long machineId;
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator() {
return new SnowflakeIdGenerator(machineId);
}
}
// 2. 业务接口
public interface UrlShortenerService {
// 生成短链(默认短码)
String generateShortUrl(String longUrl);
// 生成短链(自定义短码)
String generateCustomShortUrl(String longUrl, String customShortCode);
// 根据短码获取长URL(用于跳转)
String getLongUrlByShortCode(String shortCode);
}
// 3. 业务实现类(核心逻辑,整合缓存和数据库)
@Service
public class UrlShortenerServiceImpl implements UrlShortenerService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
@Autowired
private UrlMappingMapper urlMappingMapper; // MyBatis Mapper接口
// Redis Key前缀
private static final String REDIS_KEY_PREFIX = "short:";
// 缓存穿透:不存在的短码缓存值
private static final String NOT_FOUND = "NOT_FOUND";
// 不存在短码的缓存TTL(60秒)
private static final long NOT_FOUND_TTL = 60;
// 生成默认短链
@Override
@Transactional(rollbackFor = Exception.class)
public String generateShortUrl(String longUrl) {
// 1. 先查询缓存,判断长URL是否已生成过短码(去重)
String cacheKey = REDIS_KEY_PREFIX + "long:" + DigestUtils.md5DigestAsHex(longUrl.getBytes());
String existingShortCode = redisTemplate.opsForValue().get(cacheKey);
if (existingShortCode != null) {
return existingShortCode; // 已存在,直接返回
}
// 2. 查询数据库,判断长URL是否已存在(去重,基于MD5索引)
UrlMapping existingMapping = urlMappingMapper.selectByLongUrlMd5(DigestUtils.md5DigestAsHex(longUrl.getBytes()));
if (existingMapping != null) {
// 同步至缓存,设置永久有效(热点数据)
redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + existingMapping.getShortCode(), longUrl);
redisTemplate.opsForValue().set(cacheKey, existingMapping.getShortCode());
return existingMapping.getShortCode();
}
// 3. 生成Snowflake ID + Base62编码,得到短码
long snowflakeId = snowflakeIdGenerator.generateId();
String shortCode = Base62Utils.encode(snowflakeId);
// 4. 插入数据库(主库写入)
UrlMapping urlMapping = new UrlMapping();
urlMapping.setId(snowflakeId);
urlMapping.setShortCode(shortCode);
urlMapping.setLongUrl(longUrl);
urlMapping.setCustom(false);
urlMapping.setClickCount(0L);
urlMappingMapper.insert(urlMapping);
// 5. 同步至缓存
redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + shortCode, longUrl);
redisTemplate.opsForValue().set(cacheKey, shortCode);
return shortCode;
}
// 生成自定义短链
@Override
@Transactional(rollbackFor = Exception.class)
public String generateCustomShortUrl(String longUrl, String customShortCode) {
// 1. 校验自定义短码是否已存在(查询数据库和缓存)
if (redisTemplate.hasKey(REDIS_KEY_PREFIX + customShortCode)) {
throw new RuntimeException("自定义短码已被占用");
}
UrlMapping existingMapping = urlMappingMapper.selectByShortCode(customShortCode);
if (existingMapping != null) {
throw new RuntimeException("自定义短码已被占用");
}
// 2. 生成Snowflake ID(用于数据库主键)
long snowflakeId = snowflakeIdGenerator.generateId();
// 3. 插入数据库
UrlMapping urlMapping = new UrlMapping();
urlMapping.setId(snowflakeId);
urlMapping.setShortCode(customShortCode);
urlMapping.setLongUrl(longUrl);
urlMapping.setCustom(true);
urlMapping.setClickCount(0L);
urlMappingMapper.insert(urlMapping);
// 4. 同步至缓存
redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + customShortCode, longUrl);
String cacheKey = REDIS_KEY_PREFIX + "long:" + DigestUtils.md5DigestAsHex(longUrl.getBytes());
redisTemplate.opsForValue().set(cacheKey, customShortCode);
return customShortCode;
}
// 根据短码获取长URL(跳转核心)
@Override
public String getLongUrlByShortCode(String shortCode) {
String redisKey = REDIS_KEY_PREFIX + shortCode;
// 1. 先查询缓存
String longUrl = redisTemplate.opsForValue().get(redisKey);
// 2. 缓存命中:判断是否为不存在的短码
if (NOT_FOUND.equals(longUrl)) {
return null;
}
// 3. 缓存命中且有效,返回长URL
if (longUrl != null) {
// 点击次数自增(异步,不影响主流程)
CompletableFuture.runAsync(() -> urlMappingMapper.incrementClickCount(shortCode));
return longUrl;
}
// 4. 缓存未命中,查询数据库(从库读取)
UrlMapping urlMapping = urlMappingMapper.selectByShortCode(shortCode);
if (urlMapping != null) {
longUrl = urlMapping.getLongUrl();
// 同步至缓存(永久有效)
redisTemplate.opsForValue().set(redisKey, longUrl);
// 点击次数自增
CompletableFuture.runAsync(() -> urlMappingMapper.incrementClickCount(shortCode));
return longUrl;
}
// 5. 数据库未找到,缓存空值(防止穿透)
redisTemplate.opsForValue().set(redisKey, NOT_FOUND, NOT_FOUND_TTL, TimeUnit.SECONDS);
return null;
}
}
// 4. 控制层(跳转接口+生成接口)
@RestController
@RequestMapping("/")
public class UrlShortenerController {
@Autowired
private UrlShortenerService urlShortenerService;
// 短链跳转接口(核心,用户点击短链触发)
@GetMapping("/{shortCode}")
public ResponseEntity redirect(@PathVariable String shortCode) {
String longUrl = urlShortenerService.getLongUrlByShortCode(shortCode);
if (longUrl == null) {
return ResponseEntity.notFound().build(); // 404
}
// 返回302重定向
return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(longUrl)).build();
}
// 生成短链接口(供前端调用)
@PostMapping("/generate")
public ResponseEntity generateShortUrl(@RequestParam String longUrl) {
String shortCode = urlShortenerService.generateShortUrl(longUrl);
// 拼接短链域名(实际部署时替换为自己的域名)
String shortUrl = "https://short.com/" + shortCode;
return ResponseEntity.ok(shortUrl);
}
// 生成自定义短链接口
@PostMapping("/generate/custom")
public ResponseEntity generateCustomShortUrl(@RequestParam String longUrl, @RequestParam String customShortCode) {
try {
String shortCode = urlShortenerService.generateCustomShortUrl(longUrl, customShortCode);
String shortUrl = "https://short.com/" + shortCode;
return ResponseEntity.ok(shortUrl);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
(五)步骤4:系统测试与部署
单元测试,要针对短码生成实施相关测试,针对自定义短码开展相关测试,针对跳转做相应测试,针对缓存穿透防护去实施有关测试,目的在于切实保证不存在bug。
就压力测试而言,要运用那种能够模拟出超过10万的QPS请求的方式,来对系统响应时间展开测试,这个响应时间还得控制在仅仅10毫秒之内才行,同时还要去测试并发处理的能力,以此来检定Redis缓存以及MySQL主从的优化所呈现出来的效果。
3. 进行部署时,采用容器化的方式来部署,运用微服务集群的形式开展部署,此微服务集群部署是在多应用服务器的基础上进行的,同时,Redis集群以及MySQL主从也被用于部署,而负载均衡器则选择了Nginx,以此达成高可用的目的。
十亿级短链系统生产环境避坑指南
综合实战以及近期生产环境当中的案例,归纳出6个高频踩坑之处,直接避开系统性能方面的瓶颈以及数据安全有关的问题,对系统稳定运行起到助力作用:
避坑点1是短码碰撞这个致命问题,严禁把MD5截取用作短码,因为在十亿级数据的情况下碰撞概率非常高。必须选用ID加方案,并且要在数据库字段添加唯一索引,以此来兜底防止碰撞。
第二,避坑要点之二为,缓存穿透,要是不缓存不存在的短码,那么恶意攻击者就会频频请求无效短码,进而直接致使MySQL从库被压垮,所以需要添加空值缓存,而且其TTL为60秒,与此同时要限制单IP的请求频率。
首先,避坑点3是时钟回拨,其次,ID依赖系统时间,然后,要是服务器时钟回拨,就会生成重复ID,接着需要在ID生成工具类里添加时钟回拨校验,并且要定期同步服务器时间。
避开陷阱要点4提示存在如下情况,存储规模扩大,也就是十亿级别的长URL进行存储时会占用数量可观的空间,可以针对长URL实施压缩存储,像是借助Gzip的方式来压缩,与此同时,按照固定的周期去清理已经过期的短链,这里所指的短链并非是NULL状态,进而达成释放存储空间的目的。
存在避坑点5,即高并发写入瓶颈,要是直接写入MySQL主库,当每秒写入数量超过1万的时候就会出现瓶颈,这种情况下需要引入消息队列,比如说采用异步方式写入数据库,并且要预先通过Redis缓存同步短码映射,以此保证读取不受影响。
6. 避坑点6:关于短码可枚举所涵盖的安全问题——一旦短码出现可被逐一列举的情况(就好像是通过自增ID加上其他方式来实现那样),攻击者就能利用这种情况逐个获取短码,进而使得敏感的长URL被泄露出去,所以必须针对由ID生成的短码进行一些小小的混淆操作(像是对字符的排列顺序稍微做些调整之类的),以此来提高安全性。
总结
设计十亿级URL短链系统的时候,其核心要点在于,要实现高并发读写的相应支撑,还要保障数据的一致性,同时要达成存储上的高效扩容,其底层依靠的是一种短码生成方案,该方案基于ID加上编码来实现,并且有一个分层存储架构,此架构包含Redis缓存以及MySQL主从,另外还有微服务分布式部署所带来的高可用设计。
由专业剖析起始,将系统底层原理予以拆解,把Java + Redis + MySQL的完整实战步骤予以提供,把生产环境高频踩坑点予以总结,契合2026年后端技术热点,兼顾专业性以及可落地性。对于后端开发者来讲,短链系统是分布式架构、缓存优化、高并发处理的典型实践事例,掌握其设计逻辑以及落地技巧,能够快速提升分布式系统设计能力。
在进行实际落地操作的时候,能够依据自身所拥有的业务场景,像是是否存在对于短链统计的需求,以及过期时间的设定情况,从而做到灵活地去进行调整,其最为关键的要点便在于紧紧把住“缓存优先、分布式无状态、分层存储” 这三个至关重要的关键点intellij idea logo,如此一来就能够支撑十亿级数据量级实现稳定的运行状态。
互动提出问题:你于搭建短链系统的环节中,碰到过哪些属于高并发或者数据一致性方面的问题情形?欢迎在评论的区域之内发表谈论留存意见!
如有侵权请联系删除!
Copyright © 2023 江苏优软数字科技有限公司 All Rights Reserved.正版sublime text、Codejock、IntelliJ IDEA、sketch、Mestrenova、DNAstar服务提供商
13262879759
微信二维码