首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从千亿次点击到毫秒级响应:短链系统的设计艺术

从千亿次点击到毫秒级响应:短链系统的设计艺术

作者头像
果酱带你啃java
发布2026-04-14 12:36:57
发布2026-04-14 12:36:57
490
举报

在这个信息爆炸的时代,我们每天都在与链接打交道 —— 社交媒体分享、短信营销、二维码扫描... 但你是否想过,那些动辄几十甚至上百个字符的冗长 URL,是如何被压缩成短短几个字符的?短链系统看似简单,实则蕴含着分布式系统设计的精髓。

我参与过日均处理千亿次请求的短链系统重构。今天,我将从 0 到 1 带你剖析短链系统的设计奥秘,不仅告诉你 "怎么做",更要讲透 "为什么这么做"。无论你是刚入行的开发者,还是想优化现有系统的架构师,这篇文章都将为你提供可落地的实战指南。

一、短链系统核心原理与挑战

1.1 什么是短链系统?

短链系统(URL Shortener)是一种能将长 URL 转换为短 URL 的服务。当用户访问短 URL 时,系统会将其重定向到原始的长 URL。例如:

  • 长 URL:https://www.google.com/search?q=url+shortener&rlz=1C1GCEU_zh-CN___CN1002&oq=url+shortener&aqs=chrome..69i57j0i512l9.3848j0j7&sourceid=chrome&ie=UTF-8
  • 短 URL:https://goo.gl/abc123

1.2 核心原理

短链系统的核心流程如下:

1.3 设计挑战

一个工业级短链系统需要应对以下挑战:

  • 高并发秒杀、营销活动可能带来突发流量峰值
  • 低延迟用户对跳转速度的感知极其敏感
  • 高可用短链失效可能导致业务中断
  • 数据一致性确保短码与长 URL 的映射关系不丢失
  • 安全性防止恶意 URL 被转换和传播
  • 可扩展性支持业务增长带来的存储和流量需求

二、核心组件设计

2.1 系统架构

一个完整的短链系统架构如下:

2.2 技术选型

基于 Java 生态,我们选择以下技术栈(均为最新稳定版本):

  • 基础框架Spring Boot 3.2.0
  • API 文档SpringDoc OpenAPI 3.0.0 (Swagger3)
  • 持久层MyBatis-Plus 3.5.5
  • 数据库MySQL 8.0.35
  • 缓存Redis 7.2.4
  • 消息队列Kafka 3.6.1
  • 监控Spring Boot Actuator 3.2.0 + Micrometer 1.12.0
  • 熔断限流Resilience4j 2.1.0
  • 工具类Lombok 1.18.30 + Apache Commons Lang3 3.14.0

三、短码生成算法深度解析

短码(Short Code)是短链系统的核心,它决定了短 URL 的长度、唯一性和安全性。

3.1 短码设计原则

  • 长度适中通常 6-8 个字符(平衡易用性和可用空间)
  • 唯一性确保每个长 URL 对应唯一短码(或相反)
  • 不可预测性防止恶意猜测(尤其对敏感链接)
  • 字符集选择应避免易混淆字符(如 0 和 O、1 和 l)

推荐使用 Base62 字符集(62 个字符):

  • 大写字母:A-Z(26 个)
  • 小写字母:a-z(26 个)
  • 数字:0-9(10 个)

3.2 主流生成算法对比

算法

优点

缺点

适用场景

自增 ID+Base62

实现简单、唯一性高

可预测性强

非敏感场景、需要顺序生成

哈希 + 截取

不可预测、分布均匀

可能冲突、需解决碰撞

敏感场景、对安全性要求高

UUID 压缩

全球唯一、无需中心节点

长度较长(通常需 12 字符)

分布式系统、无中心 ID 生成器

3.3 工业级实现方案

3.3.1 Base62 编码工具类
代码语言:javascript
复制
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;

/**
 * Base62编码工具类
 * 用于将数字ID转换为短码,支持62进制与10进制互转
 *
 * @author 果酱
 */
public class Base62Utils {
    /**
     * Base62字符集(不含易混淆字符)
     */
    private static final String BASE62_CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789";
    /**
     * 基数
     */
    private static final int BASE = BASE62_CHARACTERS.length();

    /**
     * 将长整型数字转换为Base62字符串
     *
     * @param number 待转换的数字,必须大于等于0
     * @return Base62字符串
     * @throws IllegalArgumentException 如果数字为负数
     */
    public static String encode(long number) {
        if (number < 0) {
            throw new IllegalArgumentException("数字必须是非负数: " + number);
        }
        if (number == 0) {
            return String.valueOf(BASE62_CHARACTERS.charAt(0));
        }

        StringBuilder sb = new StringBuilder();
        while (number > 0) {
            int remainder = (int) (number % BASE);
            sb.append(BASE62_CHARACTERS.charAt(remainder));
            number = number / BASE;
        }

        return sb.reverse().toString();
    }

    /**
     * 将Base62字符串转换为长整型数字
     *
     * @param base62String Base62字符串
     * @return 对应的长整型数字
     * @throws IllegalArgumentException 如果输入字符串包含无效字符
     */
    public static long decode(String base62String) {
        Objects.requireNonNull(base62String, "Base62字符串不能为空");
        if (base62String.isEmpty()) {
            throw new IllegalArgumentException("Base62字符串不能为空");
        }

        long number = 0;
        for (char c : base62String.toCharArray()) {
            int index = BASE62_CHARACTERS.indexOf(c);
            if (index == -1) {
                throw new IllegalArgumentException("无效的Base62字符: " + c);
            }
            number = number * BASE + index;
        }

        return number;
    }
}
代码语言:javascript
复制

3.3.2 分布式 ID 生成器

基于雪花算法(Snowflake)实现分布式 ID 生成,确保 ID 全局唯一:

代码语言:javascript
复制
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;

/**
 * 分布式ID生成器(基于雪花算法)
 * 生成64位唯一ID,格式:[1位符号位][41位时间戳][10位机器ID][12位序列号]
 *
 * @author 果酱
 */
@Slf4j
public class SnowflakeIdGenerator {
    /**
     * 起始时间戳(2024-01-01 00:00:00)
     */
    private static final long START_TIMESTAMP = 1704067200000L;

    /**
     * 机器ID所占位数
     */
    private static final int WORKER_ID_BITS = 10;

    /**
     * 序列号所占位数
     */
    private static final int SEQUENCE_BITS = 12;

    /**
     * 机器ID最大值(2^10 - 1)
     */
    private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1;

    /**
     * 序列号最大值(2^12 - 1)
     */
    private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;

    /**
     * 机器ID左移位数
     */
    private static final int WORKER_ID_SHIFT = SEQUENCE_BITS;

    /**
     * 时间戳左移位数
     */
    private static final int TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    /**
     * 机器ID
     */
    private final long workerId;

    /**
     * 序列号,用于同一毫秒内生成多个ID
     */
    private long sequence = 0L;

    /**
     * 上一次生成ID的时间戳
     */
    private long lastTimestamp = -1L;

    /**
     * 构造函数
     *
     * @param workerId 机器ID,范围0~1023
     * @throws IllegalArgumentException 如果机器ID超出范围
     */
    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException("机器ID超出范围,必须在0~" + MAX_WORKER_ID + "之间: " + workerId);
        }
        this.workerId = workerId;
        log.info("雪花ID生成器初始化完成,机器ID:{}", workerId);
    }

    /**
     * 生成下一个唯一ID
     *
     * @return 64位唯一ID
     */
    public synchronized long nextId() {
        long currentTimestamp = getCurrentTimestamp();

        // 如果当前时间戳小于上一次生成ID的时间戳,说明系统时钟回拨
        if (currentTimestamp < lastTimestamp) {
            log.error("系统时钟回拨,拒绝生成ID,上一次时间戳:{}, 当前时间戳:{}", lastTimestamp, currentTimestamp);
            throw new RuntimeException("系统时钟回拨,无法生成ID");
        }

        // 如果是同一毫秒,序列号加1
        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 如果序列号超出最大值,等待下一毫秒
            if (sequence == 0) {
                currentTimestamp = waitUntilNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒,序列号重置为0
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;

        // 组合ID:时间戳部分 + 机器ID部分 + 序列号部分
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    /**
     * 获取当前时间戳(毫秒)
     *
     * @return 当前时间戳
     */
    private long getCurrentTimestamp() {
        return Instant.now().toEpochMilli();
    }

    /**
     * 等待到下一毫秒
     *
     * @param lastTimestamp 上一次的时间戳
     * @return 新的时间戳
     */
    private long waitUntilNextMillis(long lastTimestamp) {
        long timestamp = getCurrentTimestamp();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentTimestamp();
        }
        return timestamp;
    }
}
代码语言:javascript
复制

3.3.3 短码生成服务

结合 ID 生成器和 Base62 编码,实现短码生成服务:

代码语言:javascript
复制
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;

/**
 * 短码生成服务
 * 负责将长URL转换为唯一的短码
 *
 * @author 果酱
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortCodeService {
    /**
     * 短码最小长度
     */
    private static final int MIN_CODE_LENGTH = 6;

    /**
     * 分布式ID生成器
     */
    private final SnowflakeIdGenerator snowflakeIdGenerator;

    /**
     * 短链映射DAO
     */
    private final ShortUrlMapper shortUrlMapper;

    /**
     * 生成短码
     *
     * @param longUrl 长URL
     * @return 生成的短码
     */
    public String generateShortCode(String longUrl) {
        Objects.requireNonNull(longUrl, "长URL不能为空");

        // 检查是否已存在映射(避免重复生成)
        String existingCode = shortUrlMapper.selectShortCodeByLongUrl(longUrl);
        if (StringUtils.hasText(existingCode)) {
            log.info("长URL已存在短码映射,长URL:{}, 短码:{}", maskUrl(longUrl), existingCode);
            return existingCode;
        }

        // 生成唯一ID
        long id = snowflakeIdGenerator.nextId();

        // 转换为Base62短码
        String shortCode = Base62Utils.encode(id);

        // 确保短码长度不小于最小值
        if (shortCode.length() < MIN_CODE_LENGTH) {
            shortCode = StringUtils.leftPad(shortCode, MIN_CODE_LENGTH, BASE62_FILL_CHAR);
        }

        log.info("生成短码成功,长URL:{}, 短码:{}", maskUrl(longUrl), shortCode);
        return shortCode;
    }

    /**
     * 对URL进行脱敏处理(只保留前10和后10个字符)
     *
     * @param url 原始URL
     * @return 脱敏后的URL
     */
    private String maskUrl(String url) {
        if (url.length() <= 20) {
            return url;
        }
        return url.substring(0, 10) + "..." + url.substring(url.length() - 10);
    }

    /**
     * 用于填充短码的字符
     */
    private static final char BASE62_FILL_CHAR = 'A';
}
代码语言:javascript
复制

四、存储设计与优化

4.1 数据模型设计

短链系统核心数据模型包括:

4.1.1 短链映射表(核心表)
代码语言:javascript
复制
CREATE TABLE `short_url` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `short_code` varchar(20) NOT NULL COMMENT '短码',
  `long_url` text NOT NULL COMMENT '原始长URL',
  `domain` varchar(50) NOT NULL DEFAULT 's.example.com' COMMENT '短链域名',
  `expire_time` datetime DEFAULT NULL COMMENT '过期时间,NULL表示永久有效',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `create_user` varchar(50) DEFAULT NULL COMMENT '创建用户',
  `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_short_code` (`short_code`,`domain`) COMMENT '短码+域名唯一索引',
  KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引',
  KEY `idx_expire_time` (`expire_time`) COMMENT '过期时间索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短链映射表';
代码语言:javascript
复制

4.1.2 访问统计表
代码语言:javascript
复制
CREATE TABLE `short_url_access` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `short_code` varchar(20) NOT NULL COMMENT '短码',
  `domain` varchar(50) NOT NULL COMMENT '短链域名',
  `access_time` datetime NOT NULL COMMENT '访问时间',
  `ip` varchar(50) DEFAULT NULL COMMENT '访问IP',
  `user_agent` text COMMENT '用户代理信息',
  `referrer` text COMMENT '来源页面',
  `country` varchar(50) DEFAULT NULL COMMENT '国家',
  `region` varchar(50) DEFAULT NULL COMMENT '地区',
  `city` varchar(50) DEFAULT NULL COMMENT '城市',
  `browser` varchar(50) DEFAULT NULL COMMENT '浏览器',
  `os` varchar(50) DEFAULT NULL COMMENT '操作系统',
  `device` varchar(50) DEFAULT NULL COMMENT '设备类型',
  PRIMARY KEY (`id`),
  KEY `idx_short_code_time` (`short_code`,`domain`,`access_time`) COMMENT '短码+域名+访问时间索引',
  KEY `idx_access_time` (`access_time`) COMMENT '访问时间索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短链访问统计表';
代码语言:javascript
复制

4.2 MyBatis-Plus 实体与 Mapper

4.2.1 短链映射实体
代码语言:javascript
复制
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 短链映射实体
 *
 * @author 果酱
 */
@Data
@TableName("short_url")
@Schema(description = "短链映射实体")
public class ShortUrl {
    @TableId(type = IdType.AUTO)
    @Schema(description = "主键ID")
    private Long id;

    @TableField("short_code")
    @Schema(description = "短码", required = true)
    private String shortCode;

    @TableField("long_url")
    @Schema(description = "原始长URL", required = true)
    private String longUrl;

    @TableField("domain")
    @Schema(description = "短链域名", defaultValue = "s.example.com")
    private String domain;

    @TableField("expire_time")
    @Schema(description = "过期时间,null表示永久有效")
    private LocalDateTime expireTime;

    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @Schema(description = "创建时间", accessMode = Schema.AccessMode.READ_ONLY)
    private LocalDateTime createTime;

    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    @Schema(description = "更新时间", accessMode = Schema.AccessMode.READ_ONLY)
    private LocalDateTime updateTime;

    @TableField("create_user")
    @Schema(description = "创建用户")
    private String createUser;

    @TableField("status")
    @Schema(description = "状态:0-禁用,1-正常", defaultValue = "1")
    private Integer status;
}
代码语言:javascript
复制

4.2.2 Mapper 接口
代码语言:javascript
复制
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import java.util.List;

/**
 * 短链映射Mapper
 *
 * @author 果酱
 */
public interface ShortUrlMapper extends BaseMapper<ShortUrl> {
    /**
     * 根据长URL查询短码
     *
     * @param longUrl 长URL
     * @return 短码,如果不存在则返回null
     */
    String selectShortCodeByLongUrl(@Param("longUrl") String longUrl);

    /**
     * 根据短码和域名查询长URL
     *
     * @param shortCode 短码
     * @param domain 域名
     * @return 长URL,如果不存在或已过期则返回null
     */
    String selectLongUrlByCodeAndDomain(
            @Param("shortCode") String shortCode,
            @Param("domain") String domain);

    /**
     * 分页查询短链列表
     *
     * @param page 分页参数
     * @param queryWrapper 查询条件
     * @return 分页结果
     */
    IPage<ShortUrl> selectPage(
            IPage<ShortUrl> page,
            @Param(Constants.WRAPPER) Wrapper<ShortUrl> queryWrapper);

    /**
     * 批量插入短链记录
     *
     * @param shortUrls 短链列表
     * @return 插入成功的数量
     */
    int batchInsert(@Param("list") List<ShortUrl> shortUrls);
}
代码语言:javascript
复制

4.3 分库分表策略

当短链数量达到亿级规模时,需要进行分库分表:

  1. 分表键选择short_code为分表键
  2. 分表算法一致性哈希(Consistent Hashing)
  3. 分表数量初期 16 张表,可动态扩展至 256 张

分表配置(MyBatis-Plus 分片策略):

代码语言:javascript
复制
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import org.apache.commons.codec.digest.Md5Crypt;
import java.util.Objects;

/**
 * 短链表分表处理器
 *
 * @author 果酱
 */
public class ShortUrlTableShardingHandler implements TableNameHandler {
    /**
     * 基础表名
     */
    private static final String BASE_TABLE_NAME = "short_url_";

    /**
     * 分表数量
     */
    private static final int TABLE_COUNT = 16;

    @Override
    public String dynamicTableName(String sql, String tableName) {
        // 从SQL中提取short_code参数(实际实现需更复杂的SQL解析)
        // 这里简化处理,假设通过ThreadLocal传递shortCode
        String shortCode = ShortCodeHolder.get();
        if (StringUtils.isBlank(shortCode)) {
            throw new IllegalArgumentException("短码不能为空,无法确定分表");
        }

        // 计算分表索引
        int tableIndex = calculateTableIndex(shortCode);

        // 返回实际表名
        return BASE_TABLE_NAME + tableIndex;
    }

    /**
     * 计算分表索引
     *
     * @param shortCode 短码
     * @return 分表索引
     */
    private int calculateTableIndex(String shortCode) {
        Objects.requireNonNull(shortCode, "短码不能为空");

        // 使用MD5哈希获取哈希值
        String md5 = Md5Crypt.md5Crypt(shortCode.getBytes());
        // 取哈希值的最后8位作为整数
        long hashValue = Long.parseLong(md5.substring(md5.length() - 8), 16);
        // 取模计算分表索引
        return (int) (hashValue % TABLE_COUNT);
    }
}
代码语言:javascript
复制

4.4 缓存策略

为提升读取性能,采用多级缓存策略:

  1. 本地缓存Caffeine(内存缓存,TTL 5 分钟)
  2. 分布式缓存Redis(集群模式,TTL 30 分钟)

缓存实现:

代码语言:javascript
复制
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 短链缓存管理器
 * 实现多级缓存(本地缓存+Redis)
 *
 * @author 果酱
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ShortUrlCacheManager {
    /**
     * 缓存键前缀
     */
    private static final String CACHE_KEY_PREFIX = "short_url:";

    /**
     * Redis缓存过期时间(分钟)
     */
    private static final int REDIS_TTL_MINUTES = 30;

    /**
     * 本地缓存过期时间(分钟)
     */
    private static final int LOCAL_TTL_MINUTES = 5;

    /**
     * 本地缓存最大数量
     */
    private static final int LOCAL_CACHE_MAX_SIZE = 10_0000;

    private final ShortUrlMapper shortUrlMapper;
    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 本地缓存
     */
    private final LoadingCache<String, String> localCache = Caffeine.newBuilder()
            .maximumSize(LOCAL_CACHE_MAX_SIZE)
            .expireAfterWrite(LOCAL_TTL_MINUTES, TimeUnit.MINUTES)
            .recordStats()
            .build(this::loadLongUrlFromDb);

    /**
     * 获取长URL(优先从缓存获取)
     *
     * @param shortCode 短码
     * @param domain 域名
     * @return 长URL,如果不存在则返回null
     */
    public String getLongUrl(String shortCode, String domain) {
        Objects.requireNonNull(shortCode, "短码不能为空");
        String actualDomain = StringUtils.defaultIfBlank(domain, "s.example.com");
        String cacheKey = buildCacheKey(shortCode, actualDomain);

        // 1. 从本地缓存获取
        try {
            return localCache.get(cacheKey);
        } catch (Exception e) {
            log.warn("从本地缓存获取长URL失败,key:{}", cacheKey, e);
        }

        // 2. 从Redis获取
        String longUrl = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.hasText(longUrl)) {
            log.debug("从Redis获取长URL成功,key:{}", cacheKey);
            // 同步到本地缓存
            localCache.put(cacheKey, longUrl);
            return longUrl;
        }

        // 3. 从数据库获取并更新缓存
        longUrl = loadLongUrlFromDb(cacheKey);
        if (StringUtils.hasText(longUrl)) {
            // 更新Redis缓存
            stringRedisTemplate.opsForValue().set(
                    cacheKey, 
                    longUrl, 
                    REDIS_TTL_MINUTES, 
                    TimeUnit.MINUTES
            );
            // 更新本地缓存
            localCache.put(cacheKey, longUrl);
        }

        return longUrl;
    }

    /**
     * 从数据库加载长URL
     *
     * @param cacheKey 缓存键
     * @return 长URL,如果不存在则返回null
     */
    private String loadLongUrlFromDb(String cacheKey) {
        // 解析缓存键,获取shortCode和domain
        String[] parts = cacheKey.split(":", 3);
        if (parts.length < 3) {
            log.error("缓存键格式错误,key:{}", cacheKey);
            return null;
        }

        String shortCode = parts[1];
        String domain = parts[2];

        log.debug("从数据库加载长URL,shortCode:{}, domain:{}", shortCode, domain);
        return shortUrlMapper.selectLongUrlByCodeAndDomain(shortCode, domain);
    }

    /**
     * 构建缓存键
     *
     * @param shortCode 短码
     * @param domain 域名
     * @return 缓存键
     */
    private String buildCacheKey(String shortCode, String domain) {
        return CACHE_KEY_PREFIX + shortCode + ":" + domain;
    }

    /**
     * 清除缓存
     *
     * @param shortCode 短码
     * @param domain 域名
     */
    public void clearCache(String shortCode, String domain) {
        Objects.requireNonNull(shortCode, "短码不能为空");
        String actualDomain = StringUtils.defaultIfBlank(domain, "s.example.com");
        String cacheKey = buildCacheKey(shortCode, actualDomain);

        // 清除本地缓存
        localCache.invalidate(cacheKey);

        // 清除Redis缓存
        stringRedisTemplate.delete(cacheKey);

        log.info("清除缓存成功,key:{}", cacheKey);
    }
}
代码语言:javascript
复制

五、核心业务实现

5.1 短链服务接口

代码语言:javascript
复制
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;

/**
 * 短链服务接口
 *
 * @author 果酱
 */
@Tag(name = "短链服务", description = "提供短链的创建、查询、跳转等功能")
public interface ShortUrlService extends IService<ShortUrl> {
    /**
     * 创建短链
     *
     * @param longUrl 长URL
     * @param domain 短链域名,为空则使用默认域名
     * @param expireTime 过期时间,为空则永久有效
     * @param createUser 创建用户
     * @return 创建的短链信息
     */
    @Operation(summary = "创建短链", description = "将长URL转换为短URL")
    @ApiResponse(responseCode = "200", description = "创建成功",
            content = @Content(schema = @Schema(implementation = ShortUrl.class)))
    ShortUrl createShortUrl(
            @Parameter(description = "长URL", required = true) @RequestParam String longUrl,
            @Parameter(description = "短链域名") @RequestParam(required = false) String domain,
            @Parameter(description = "过期时间") @RequestParam(required = false) LocalDateTime expireTime,
            @Parameter(description = "创建用户") @RequestParam(required = false) String createUser);

    /**
     * 根据短码和域名查询短链信息
     *
     * @param shortCode 短码
     * @param domain 域名
     * @return 短链信息,如果不存在则返回null
     */
    @Operation(summary = "查询短链信息", description = "根据短码和域名查询短链详细信息")
    ShortUrl getShortUrlByCodeAndDomain(
            @Parameter(description = "短码", required = true) @RequestParam String shortCode,
            @Parameter(description = "域名") @RequestParam(required = false) String domain);

    /**
     * 短链跳转
     *
     * @param shortCode 短码
     * @param domain 域名
     * @param response HTTP响应对象
     * @throws IOException 如果跳转失败
     */
    @Operation(summary = "短链跳转", description = "将短URL重定向到原始长URL")
    void redirect(
            @Parameter(description = "短码", required = true) @PathVariable String shortCode,
            @Parameter(description = "域名") @RequestParam(required = false) String domain,
            HttpServletResponse response) throws IOException;

    /**
     * 分页查询短链列表
     *
     * @param pageNum 页码
     * @param pageSize 每页大小
     * @param keyword 搜索关键词(长URL或短码)
     * @return 分页查询结果
     */
    @Operation(summary = "分页查询短链列表", description = "根据关键词分页查询短链列表")
    IPage<ShortUrl> queryShortUrls(
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
            @Parameter(description = "每页大小") @RequestParam(defaultValue = "10") int pageSize,
            @Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword);

    /**
     * 更新短链状态
     *
     * @param id 短链ID
     * @param status 状态:0-禁用,1-正常
     * @return 是否更新成功
     */
    @Operation(summary = "更新短链状态", description = "启用或禁用短链")
    boolean updateStatus(
            @Parameter(description = "短链ID", required = true) @RequestParam Long id,
            @Parameter(description = "状态:0-禁用,1-正常", required = true) @RequestParam Integer status);
}
代码语言:javascript
复制

5.2 服务实现类

代码语言:javascript
复制
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Objects;

/**
 * 短链服务实现类
 *
 * @author 果酱
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortUrlServiceImpl extends ServiceImpl<ShortUrlMapper, ShortUrl> implements ShortUrlService {
    /**
     * 默认短链域名
     */
    private static final String DEFAULT_DOMAIN = "s.example.com";

    /**
     * 正常状态
     */
    private static final int STATUS_NORMAL = 1;

    /**
     * 禁用状态
     */
    private static final int STATUS_DISABLED = 0;

    private final ShortCodeService shortCodeService;
    private final ShortUrlCacheManager cacheManager;
    private final ShortUrlAccessService accessService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortUrl createShortUrl(String longUrl, String domain, LocalDateTime expireTime, String createUser) {
        // 参数校验
        StringUtils.hasText(longUrl, "长URL不能为空");
        String actualDomain = StringUtils.defaultIfBlank(domain, DEFAULT_DOMAIN);

        log.info("开始创建短链,长URL:{}, 域名:{}", shortCodeService.maskUrl(longUrl), actualDomain);

        // 生成短码
        String shortCode = shortCodeService.generateShortCode(longUrl);

        // 检查短码是否已存在(防止并发问题)
        ShortUrl existing = getShortUrlByCodeAndDomain(shortCode, actualDomain);
        if (Objects.nonNull(existing)) {
            log.info("短码已存在,直接返回,短码:{}", shortCode);
            return existing;
        }

        // 创建短链记录
        ShortUrl shortUrl = new ShortUrl();
        shortUrl.setShortCode(shortCode);
        shortUrl.setLongUrl(longUrl);
        shortUrl.setDomain(actualDomain);
        shortUrl.setExpireTime(expireTime);
        shortUrl.setCreateUser(createUser);
        shortUrl.setStatus(STATUS_NORMAL);

        // 保存到数据库
        save(shortUrl);

        log.info("短链创建成功,短码:{}, 短URL:https://{}/{}", shortCode, actualDomain, shortCode);
        return shortUrl;
    }

    @Override
    public ShortUrl getShortUrlByCodeAndDomain(String shortCode, String domain) {
        StringUtils.hasText(shortCode, "短码不能为空");
        String actualDomain = StringUtils.defaultIfBlank(domain, DEFAULT_DOMAIN);

        log.debug("查询短链信息,短码:{}, 域名:{}", shortCode, actualDomain);

        // 查询数据库
        return getOne(new LambdaQueryWrapper<ShortUrl>()
                .eq(ShortUrl::getShortCode, shortCode)
                .eq(ShortUrl::getDomain, actualDomain)
                .eq(ShortUrl::getStatus, STATUS_NORMAL)
                .and(wrapper -> wrapper
                        .isNull(ShortUrl::getExpireTime)
                        .or()
                        .gt(ShortUrl::getExpireTime, LocalDateTime.now())));
    }

    @Override
    public void redirect(String shortCode, String domain, HttpServletResponse response) throws IOException {
        StringUtils.hasText(shortCode, "短码不能为空");
        String actualDomain = StringUtils.defaultIfBlank(domain, DEFAULT_DOMAIN);

        log.info("处理短链跳转,短码:{}, 域名:{}", shortCode, actualDomain);

        // 从缓存获取长URL
        String longUrl = cacheManager.getLongUrl(shortCode, actualDomain);

        if (StringUtils.isBlank(longUrl)) {
            log.warn("短链不存在或已过期,短码:{}, 域名:{}", shortCode, actualDomain);
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "短链不存在或已过期");
            return;
        }

        // 记录访问日志(异步)
        accessService.recordAccessAsync(shortCode, actualDomain, response);

        // 302重定向(临时重定向,便于统计点击量)
        response.sendRedirect(longUrl);
        log.debug("短链跳转成功,短码:{}, 目标URL:{}", shortCode, shortCodeService.maskUrl(longUrl));
    }

    @Override
    public IPage<ShortUrl> queryShortUrls(int pageNum, int pageSize, String keyword) {
        log.info("分页查询短链列表,页码:{}, 每页大小:{}, 关键词:{}", pageNum, pageSize, keyword);

        // 构建查询条件
        LambdaQueryWrapper<ShortUrl> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.hasText(keyword)) {
            queryWrapper.and(wrapper -> wrapper
                    .like(ShortUrl::getLongUrl, keyword)
                    .or()
                    .like(ShortUrl::getShortCode, keyword));
        }

        // 按创建时间倒序
        queryWrapper.orderByDesc(ShortUrl::getCreateTime);

        // 分页查询
        IPage<ShortUrl> page = new Page<>(pageNum, pageSize);
        return page(page, queryWrapper);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateStatus(Long id, Integer status) {
        Objects.requireNonNull(id, "短链ID不能为空");
        if (status != STATUS_NORMAL && status != STATUS_DISABLED) {
            throw new IllegalArgumentException("状态必须是0或1");
        }

        log.info("更新短链状态,ID:{}, 新状态:{}", id, status);

        // 查询短链信息
        ShortUrl shortUrl = getById(id);
        if (Objects.isNull(shortUrl)) {
            log.error("短链不存在,ID:{}", id);
            return false;
        }

        // 如果状态没有变化,直接返回成功
        if (shortUrl.getStatus().equals(status)) {
            log.info("短链状态未变化,ID:{}", id);
            return true;
        }

        // 更新状态
        shortUrl.setStatus(status);
        boolean result = updateById(shortUrl);

        // 如果禁用短链,清除缓存
        if (status == STATUS_DISABLED) {
            cacheManager.clearCache(shortUrl.getShortCode(), shortUrl.getDomain());
        }

        log.info("更新短链状态完成,ID:{}, 结果:{}", id, result);
        return result;
    }
}
代码语言:javascript
复制

5.3 访问统计服务

代码语言:javascript
复制
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.Objects;

/**
 * 短链访问统计服务
 *
 * @author 果酱
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortUrlAccessService {
    /**
     * Kafka主题
     */
    private static final String KAFKA_TOPIC = "short_url_access";

    private final KafkaTemplate<String, ShortUrlAccess> kafkaTemplate;
    private final UserAgentParser userAgentParser;
    private final IpLocationResolver ipLocationResolver;

    /**
     * 异步记录访问日志
     *
     * @param shortCode 短码
     * @param domain 域名
     * @param response HTTP响应对象
     */
    @Async("accessLogExecutor")
    public void recordAccessAsync(String shortCode, String domain, HttpServletResponse response) {
        try {
            recordAccess(shortCode, domain, response);
        } catch (Exception e) {
            log.error("记录访问日志失败,短码:{}, 域名:{}", shortCode, domain, e);
        }
    }

    /**
     * 记录访问日志
     *
     * @param shortCode 短码
     * @param domain 域名
     * @param response HTTP响应对象
     */
    private void recordAccess(String shortCode, String domain, HttpServletResponse response) {
        Objects.requireNonNull(shortCode, "短码不能为空");

        // 创建访问记录
        ShortUrlAccess access = new ShortUrlAccess();
        access.setShortCode(shortCode);
        access.setDomain(domain);
        access.setAccessTime(LocalDateTime.now());

        // 获取客户端IP
        String ip = getClientIp(response);
        access.setIp(ip);

        // 解析IP地理位置
        if (StringUtils.hasText(ip)) {
            IpLocation location = ipLocationResolver.resolve(ip);
            if (Objects.nonNull(location)) {
                access.setCountry(location.getCountry());
                access.setRegion(location.getRegion());
                access.setCity(location.getCity());
            }
        }

        // 获取并解析User-Agent
        String userAgent = response.getHeader("User-Agent");
        access.setUserAgent(userAgent);
        if (StringUtils.hasText(userAgent)) {
            UserAgentInfo info = userAgentParser.parse(userAgent);
            if (Objects.nonNull(info)) {
                access.setBrowser(info.getBrowser());
                access.setOs(info.getOs());
                access.setDevice(info.getDevice());
            }
        }

        // 获取来源页面
        String referrer = response.getHeader("Referer");
        access.setReferrer(referrer);

        // 发送到Kafka
        kafkaTemplate.send(KAFKA_TOPIC, shortCode, access);
        log.debug("访问日志记录成功,短码:{}, IP:{}", shortCode, ip);
    }

    /**
     * 获取客户端真实IP
     *
     * @param response HTTP响应对象
     * @return 客户端IP
     */
    private String getClientIp(HttpServletResponse response) {
        // 实际实现需要从请求头中获取,如X-Forwarded-For、X-Real-IP等
        // 这里简化处理
        return "127.0.0.1";
    }
}
代码语言:javascript
复制

六、高可用与性能优化

6.1 限流与熔断

使用 Resilience4j 实现限流和熔断,保护系统不被流量峰值击垮:

代码语言:javascript
复制
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 短链控制器(带限流和熔断)
 *
 * @author 果酱
 */
@Slf4j
@RestController
@RequiredArgsConstructor
public class ShortUrlController {
    private final ShortUrlService shortUrlService;

    /**
     * 短链跳转接口(带限流和熔断)
     * 限流:每秒最多处理1000个请求
     * 熔断:失败率超过50%时触发熔断,持续10秒
     */
    @RateLimiter(name = "shortUrlRedirect", fallbackMethod = "redirectFallback")
    @CircuitBreaker(name = "shortUrlRedirect", fallbackMethod = "redirectFallback")
    public void redirect(
            @PathVariable String shortCode,
            @RequestParam(required = false) String domain,
            HttpServletResponse response) throws IOException {
        shortUrlService.redirect(shortCode, domain, response);
    }

    /**
     * 跳转接口降级方法
     */
    public void redirectFallback(
            String shortCode,
            String domain,
            HttpServletResponse response,
            Exception e) throws IOException {
        log.error("短链跳转降级,短码:{}, 域名:{}", shortCode, domain, e);
        response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "服务繁忙,请稍后再试");
    }
}
代码语言:javascript
复制

Resilience4j 配置:

代码语言:javascript
复制
resilience4j:
  ratelimiter:
    instances:
      shortUrlRedirect:
        limitRefreshPeriod: 1s
        limitForPeriod: 1000
        timeoutDuration: 0
  circuitbreaker:
    instances:
      shortUrlRedirect:
        slidingWindowSize: 100
        failureRateThreshold: 50
        waitDurationInOpenState: 10000
        permittedNumberOfCallsInHalfOpenState: 10
        registerHealthIndicator: true
代码语言:javascript
复制

6.2 异步处理

使用 Spring 的 @Async 注解实现异步处理,提升系统吞吐量:

代码语言:javascript
复制
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 异步线程池配置
 *
 * @author 果酱
 */
@Configuration
@EnableAsync
public class AsyncConfig {
    /**
     * 访问日志处理线程池
     */
    @Bean(name = "accessLogExecutor")
    public Executor accessLogExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(50);
        // 队列容量
        executor.setQueueCapacity(1000);
        // 线程名称前缀
        executor.setThreadNamePrefix("access-log-");
        // 线程空闲时间
        executor.setKeepAliveSeconds(60);
        // 拒绝策略:由提交任务的线程处理
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}
代码语言:javascript
复制

6.3 数据库优化

  1. 索引优化为频繁查询的字段建立合适的索引
  2. 读写分离主库写入,从库读取
  3. 批量操作批量插入和更新减少数据库交互
  4. SQL 优化避免全表扫描,优化 JOIN 操作

MyBatis-Plus 分页插件配置:

代码语言:javascript
复制
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-Plus配置
 *
 * @author 果酱
 */
@Configuration
public class MyBatisPlusConfig {
    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
代码语言:javascript
复制

七、监控与告警

7.1 监控指标设计

代码语言:javascript
复制
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * 短链系统监控指标收集器
 *
 * @author 果酱
 */
@Component
public class ShortUrlMetrics {
    private final MeterRegistry meterRegistry;

    // 创建短链指标
    private Counter createSuccessCounter;
    private Counter createFailCounter;
    private Timer createTimer;

    // 跳转指标
    private Counter redirectSuccessCounter;
    private Counter redirectFailCounter;
    private Timer redirectTimer;

    // 缓存指标
    private Counter cacheHitCounter;
    private Counter cacheMissCounter;

    public ShortUrlMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @PostConstruct
    public void init() {
        // 初始化创建短链指标
        createSuccessCounter = meterRegistry.counter("short_url.create.success");
        createFailCounter = meterRegistry.counter("short_url.create.fail");
        createTimer = Timer.builder("short_url.create.duration")
                .description("创建短链耗时")
                .register(meterRegistry);

        // 初始化跳转指标
        redirectSuccessCounter = meterRegistry.counter("short_url.redirect.success");
        redirectFailCounter = meterRegistry.counter("short_url.redirect.fail");
        redirectTimer = Timer.builder("short_url.redirect.duration")
                .description("短链跳转耗时")
                .register(meterRegistry);

        // 初始化缓存指标
        cacheHitCounter = meterRegistry.counter("short_url.cache.hit");
        cacheMissCounter = meterRegistry.counter("short_url.cache.miss");
    }

    // 创建短链指标记录方法
    public void recordCreateSuccess(long durationMs) {
        createSuccessCounter.increment();
        createTimer.record(durationMs, TimeUnit.MILLISECONDS);
    }

    public void recordCreateFail() {
        createFailCounter.increment();
    }

    // 跳转指标记录方法
    public void recordRedirectSuccess(long durationMs) {
        redirectSuccessCounter.increment();
        redirectTimer.record(durationMs, TimeUnit.MILLISECONDS);
    }

    public void recordRedirectFail() {
        redirectFailCounter.increment();
    }

    // 缓存指标记录方法
    public void recordCacheHit() {
        cacheHitCounter.increment();
    }

    public void recordCacheMiss() {
        cacheMissCounter.increment();
    }
}
代码语言:javascript
复制

7.2 健康检查

代码语言:javascript
复制
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Objects;

/**
 * 短链系统健康检查指示器
 *
 * @author 果酱
 */
@Component("shortUrlSystem")
public class ShortUrlHealthIndicator implements HealthIndicator {
    private final StringRedisTemplate redisTemplate;
    private final ShortUrlMapper shortUrlMapper;

    public ShortUrlHealthIndicator(StringRedisTemplate redisTemplate, ShortUrlMapper shortUrlMapper) {
        this.redisTemplate = redisTemplate;
        this.shortUrlMapper = shortUrlMapper;
    }

    @Override
    public Health health() {
        // 检查Redis健康状态
        boolean redisHealthy = checkRedisHealth();

        // 检查数据库健康状态
        boolean dbHealthy = checkDbHealth();

        if (redisHealthy && dbHealthy) {
            return Health.up()
                    .withDetail("redis", "健康")
                    .withDetail("database", "健康")
                    .build();
        } else {
            return Health.down()
                    .withDetail("redis", redisHealthy ? "健康" : "不健康")
                    .withDetail("database", dbHealthy ? "健康" : "不健康")
                    .build();
        }
    }

    /**
     * 检查Redis健康状态
     */
    private boolean checkRedisHealth() {
        try {
            return redisTemplate.getConnectionFactory().getConnection().ping().equals("PONG");
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 检查数据库健康状态
     */
    private boolean checkDbHealth() {
        try {
            shortUrlMapper.selectCount(null);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
代码语言:javascript
复制

八、安全措施

8.1 URL 安全检测

代码语言:javascript
复制
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.Set;

/**
 * URL安全检测器
 * 防止恶意URL被转换为短链
 *
 * @author 果酱
 */
@Slf4j
@Component
public class UrlSafetyChecker {
    /**
     * 禁止的URL前缀
     */
    private static final Set<String> FORBIDDEN_PREFIXES = Set.of(
            "javascript:", "vbscript:", "data:", "mailto:", "tel:"
    );

    /**
     * 禁止的域名
     */
    private static final Set<String> FORBIDDEN_DOMAINS = Set.of(
            "malicious.com", "phishing.com", "spam.com"
    );

    /**
     * 检查URL是否安全
     *
     * @param url 待检查的URL
     * @return true-安全,false-不安全
     */
    public boolean isSafeUrl(String url) {
        Objects.requireNonNull(url, "URL不能为空");

        // 检查URL前缀
        String lowerUrl = url.toLowerCase();
        for (String prefix : FORBIDDEN_PREFIXES) {
            if (lowerUrl.startsWith(prefix)) {
                log.warn("URL包含禁止的前缀,URL:{},前缀:{}", url, prefix);
                return false;
            }
        }

        // 解析并检查域名(实际实现需要更复杂的URL解析)
        String domain = extractDomain(url);
        if (FORBIDDEN_DOMAINS.contains(domain)) {
            log.warn("URL包含禁止的域名,URL:{},域名:{}", url, domain);
            return false;
        }

        // 其他安全检查(如长度限制、特殊字符检查等)
        if (url.length() > 1000) {
            log.warn("URL过长,可能存在安全风险,长度:{}", url.length());
            return false;
        }

        return true;
    }

    /**
     * 从URL中提取域名(简化实现)
     */
    private String extractDomain(String url) {
        // 实际实现应使用java.net.URL或专门的URL解析库
        try {
            java.net.URL urlObj = new java.net.URL(url);
            return urlObj.getHost();
        } catch (Exception e) {
            log.error("解析URL失败,URL:{}", url, e);
            return "";
        }
    }
}
代码语言:javascript
复制

8.2 防滥用措施

  1. IP 限流限制同一 IP 的创建频率
  2. 验证码对可疑请求要求输入验证码
  3. 黑名单封禁恶意用户和 IP
  4. 内容审核人工审核敏感领域的短链

九、总结与最佳实践

9.1 核心收获

通过本文,我们学习了如何设计一个工业级短链系统,包括:

  1. 短码生成基于雪花算法和 Base62 编码的高可用方案
  2. 存储设计MySQL 分库分表 + Redis 缓存的多级存储策略
  3. 性能优化多级缓存、异步处理、限流熔断等技术手段
  4. 高可用保障监控告警、健康检查、容灾备份
  5. 安全措施URL 检测、防滥用机制

9.2 最佳实践

  1. 短码长度推荐 6-8 个字符(平衡可用性和存储空间)
  2. 跳转方式优先使用 302 临时重定向(便于统计和灵活调整)
  3. 缓存策略热门短链本地缓存 + Redis 分布式缓存
  4. 扩展策略提前规划分库分表,避免后期数据迁移
  5. 监控重点跳转成功率、响应时间、缓存命中率

9.3 未来展望

短链系统可以进一步扩展以下功能:

  1. 个性化短码允许用户自定义短码
  2. 密码保护为敏感短链设置访问密码
  3. 访问控制限制特定用户或 IP 访问
  4. 数据分析提供详细的访问统计和趋势分析
  5. API 集成开放 API 供第三方系统集成
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在这个信息爆炸的时代,我们每天都在与链接打交道 —— 社交媒体分享、短信营销、二维码扫描... 但你是否想过,那些动辄几十甚至上百个字符的冗长 URL,是如何被压缩成短短几个字符的?短链系统看似简单,实则蕴含着分布式系统设计的精髓。
  • 一、短链系统核心原理与挑战
    • 1.1 什么是短链系统?
    • 1.2 核心原理
    • 1.3 设计挑战
  • 二、核心组件设计
    • 2.1 系统架构
    • 2.2 技术选型
  • 三、短码生成算法深度解析
    • 3.1 短码设计原则
    • 3.2 主流生成算法对比
    • 3.3 工业级实现方案
      • 3.3.1 Base62 编码工具类
      • 3.3.2 分布式 ID 生成器
      • 3.3.3 短码生成服务
  • 四、存储设计与优化
    • 4.1 数据模型设计
      • 4.1.1 短链映射表(核心表)
      • 4.1.2 访问统计表
    • 4.2 MyBatis-Plus 实体与 Mapper
      • 4.2.1 短链映射实体
      • 4.2.2 Mapper 接口
    • 4.3 分库分表策略
    • 4.4 缓存策略
  • 五、核心业务实现
    • 5.1 短链服务接口
    • 5.2 服务实现类
    • 5.3 访问统计服务
  • 六、高可用与性能优化
    • 6.1 限流与熔断
    • 6.2 异步处理
    • 6.3 数据库优化
  • 七、监控与告警
    • 7.1 监控指标设计
    • 7.2 健康检查
  • 八、安全措施
    • 8.1 URL 安全检测
    • 8.2 防滥用措施
  • 九、总结与最佳实践
    • 9.1 核心收获
    • 9.2 最佳实践
    • 9.3 未来展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档