从0到1搭建短链接系统:哈希冲突+跳转高性能+访问统计——完整实现

引言

上个月市场部搞大促推广,十万条营销短信里带着原始链接——一条带渠道参数的 URL 足足 137 个字符,短信按 70 字拆条计费,一条短信被拆成三条,推广成本直接翻了三倍。更别提线下物料上印 137 个字符的 URL,二维码扫码后密密麻麻一屏。

短链接系统就是解决这个问题的:https://shop.example.com/product/detail?id=8847362&channel=sm&activity=d11&utm_source=xxxhttps://s.example.cn/aB3xK9。看起来只是"把长的变短",但把它当生产系统做,会撞上三个经典工程问题:

  1. 短码怎么生成——哈希会冲突,冲突了怎么办?
  2. 跳转怎么扛住高并发——跳转是整个系统里 QPS 最高的接口,读多写少到什么程度?缓存怎么设计?
  3. 访问统计怎么不拖垮跳转——每次跳转都记一笔,跳转链路被统计拖慢就本末倒置了。

这篇文章从 0 到 1 完整实现:MurmurHash + Base62 生成短码 → Bloom Filter 检测冲突 → DB 存储 → Redis 缓存 → 302 重定向 → 异步访问统计,最后讲清楚为什么成熟方案会用发号器替代哈希


一、需求拆解与总体架构

1.1 功能需求与量化指标

能力需求量化指标
短链生成长链转短码,支持过期时间、自定义短码写 QPS ~100(相对低频)
短链跳转短码 302 重定向到长链读 QPS ~5000(读写比 50:1 起步)
访问统计PV/UV/来源渠道统计,报表 T+1跳转链路统计开销 < 1ms
可靠性短码永不可重复映射到不同长链冲突率 0(数据正确性红线)

先记住读写比这个数字:一个短链被创建一次,但会被点击成千上万次。这决定了整个架构的重心在"读路径"——跳转接口的设计质量就是系统的天花板。

1.2 总体架构

┌──────────┐  ①生链   ┌───────────────┐   短码生成
│ 运营后台/ │ ───────▶ │  短链服务       │ ──▶ MurmurHash + Base62
│ 开放API   │          │  (创建/管理)    │ ──▶ Bloom Filter 查重
└──────────┘          └───────┬───────┘
                              ▼
                    ┌─────────────────┐
                    │  DB: 短码+长链+   │  唯一索引兜底
                    │  过期时间+状态     │
                    └─────────────────┘

┌──────────┐  ②跳转   ┌───────────────┐  命中?    ┌────────────┐
│ 用户点击  │ ───────▶ │  跳转服务       │─────────▶ │ Redis 热链  │
│ s.xx.cn/ │          │  (最高QPS)     │  未命中    └─────┬──────┘
│ aB3xK9   │          └───────┬───────┘───回填───────────┘
└──────────┘                  ▼ 未命中
                    ┌─────────────────┐      ┌────────────┐
                    │  DB(兜底)       │      │ BloomFilter │ 兜底拦截
                    └─────────────────┘      │ "肯定不存在" │ 不存在的短码
                                             └────────────┘
                              │
                              ▼ ③统计(异步,不阻塞跳转)
                    ┌─────────────────┐
                    │ MQ → 统计服务 →   │  PV/UV/渠道报表
                    │ 统计表/数仓       │
                    └─────────────────┘

三个设计决策先立住:

决策选择理由
重定向码302(不是 301)301 浏览器永久缓存,过期短链无法"复活"、统计丢失——跳转必须经过服务端
统计写法异步(MQ/批量)跳转链路同步写库 = 每次跳转多一次 DB 写,QPS 天花板被统计压死
缓存策略Cache Aside + 热点常驻跳转数据不可变(短码→长链映射不变),缓存命中率天然极高

二、短码生成:MurmurHash + Base62

2.1 为什么是 Base62

短码可用的字符集:0-9 a-z A-Z 共 62 个字符(不含易混淆的 -_)。Base62 编码就是"把一个数字用 62 进制表示":

短码长度62^n 组合数够用吗
5 位9.16 亿日常场景够用
6 位568 亿主流选择:千万级短链下冲突概率极低
7 位3.5 万亿超大规模/焦虑型团队

6 位短码是成本与容量的甜点位——比 5 位多 62 倍容量,比 7 位短一截。

2.2 MurmurHash:快、散、稳

哈希函数选型:MD5/SHA 系列安全但重(我们不需要防碰撞的安全性),需要的是快 + 分布均匀 + 稳定(同一个长链每次哈希结果一致)。MurmurHash3 正是这个定位——非加密哈希里的性能王者,Redis Cluster 槽位、Kafka 分区都用它:

/**
 * 长链 → 62进制短码(基础版)
 * 依赖:com.google.guava:guava(Hashing.murmur3_128)
 */
public class ShortCodeGenerator {

    private static final char[] BASE62 =
            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

    public static String encode62(long num) {
        StringBuilder sb = new StringBuilder();
        while (num > 0) {
            sb.append(BASE62[(int) (num % 62)]);
            num /= 62;
        }
        // 补齐 6 位,左补 '0'
        while (sb.length() < 6) {
            sb.append('0');
        }
        return sb.reverse().toString();
    }

    public static String genShortCode(String longUrl) {
        // murmur3_128 取前 64 位作为无符号长整型
        long hash = Hashing.murmur3_128()
                .hashString(longUrl, StandardCharsets.UTF_8)
                .asLong();
        return encode62(Math.abs(hash));
    }
}

2.3 哈希方案的天生缺陷:冲突

问题立刻来了:568 亿的组合空间是"很大",但不是无限大。根据生日悖论,当已有短链数量 N 接近组合空间的平方根量级时,冲突概率就开始不可忽视:

6 位短码空间 S = 568 亿
生日悖论近似:N 条短链时任意两条冲突的概率 ≈ 1 - e^(-N²/2S)
N = 100 万  → 冲突概率 ≈ 0.09%
N = 1000 万 → 冲突概率 ≈ 8.8%   ← 已经不可忽视

而且哈希冲突有个反直觉的坑:同一个长链重复提交会得到同一个短码(这其实是 feature:天然幂等);但两个不同长链哈希相同时,你必须给出确定的裁决——查库发现"这个短码已经指向别的长链",怎么办?

  • 方案 A:换盐重试——加个随机盐再哈希一次,重新查重,直到不冲突。实现简单,代价是"同一长链可能得到不同短码"(幂等性被破坏,除非先查长链是否已存在)。
  • 方案 B:布隆过滤器前置查重——在重试之前先用 O(1) 判断"短码大概率已存在",减少无效的 DB 查询(下一章展开)。
  • 方案 C:干脆不哈希——用全局唯一 ID 发号器,从根上消灭冲突(第七章展开,成熟方案的选择)。

生产级哈希方案 = A + B 的组合:先查长链幂等 → MurmurHash → 布隆过滤器初筛 → DB 唯一索引终审 → 冲突则加盐重试。


三、Bloom Filter:冲突检测与穿透防御的双面手

3.1 原理一页纸

布隆过滤器是一个"大概率存在 / 肯定不存在"的概率结构:

  • 底层是 m 位的 bit 数组 + k 个哈希函数;
  • 写入:元素经 k 个哈希函数映射到 k 个位置,全部置 1;
  • 查询:k 个位置任意一位是 0 → 肯定不存在;全 1 → 可能存在(可能是别人置的 1,即误判)。

两个关键特性对本系统至关重要:

  1. "肯定不存在"的判断 100% 可靠——用于拦截不存在的短码查询(缓存穿透防御);
  2. 空间极省——1 亿个元素、误判率 1%,只需要约 1.14GB... 不对,是约 116MB(每个元素约 9.6 bit),比 HashSet 存短码字符串省一个数量级。

3.2 实现:Redisson 的 RBloomFilter

/**
 * 短码查重 + 穿透防御 双用途布隆过滤器
 * 预期 1 亿短码,误判率 1%
 */
@Configuration
public class BloomFilterConfig {

    @Bean
    public RBloomFilter<String> shortCodeBloomFilter(RedissonClient redisson) {
        RBloomFilter<String> filter = redisson.getBloomFilter("short:code:bloom");
        // tryInit:expectedInsertions 预期插入量,falseProbability 误判率
        // 返回 false 表示过滤器已存在(复用),不会重复初始化
        filter.tryInit(100_000_000L, 0.01);
        return filter;
    }
}

@Service
@RequiredArgsConstructor
public class ShortLinkService {

    private final RBloomFilter<String> bloomFilter;

    /** 创建短链:布隆初筛 → DB 终审 → 冲突加盐重试 */
    public String createShortLink(String longUrl, LocalDateTime expireAt) {
        // 幂等:同一长链已存在直接返回(可选策略)
        ShortLink exist = shortLinkMapper.selectByLongUrl(longUrl);
        if (exist != null && !exist.isExpired()) {
            return exist.getShortCode();
        }

        String code = ShortCodeGenerator.genShortCode(longUrl);
        for (int i = 0; i < 3; i++) {                       // 最多重试 3 次
            if (!bloomFilter.contains(code)) {              // 布隆说"肯定不存在"
                boolean inserted = tryInsert(longUrl, code, expireAt);
                if (inserted) {
                    bloomFilter.add(code);                  // ✅ 入库成功,登记布隆
                    cacheLoader.warmUp(code);               // 顺手预热
                    return code;
                }
                // 插入失败 = 唯一索引冲突(布隆误判或并发竞争)→ 加盐重试
            }
            code = ShortCodeGenerator.genShortCode(longUrl + "#salt#" + i);
        }
        throw new BizException("SHORT_CODE_GEN_FAIL");
    }

    private boolean tryInsert(String longUrl, String code, LocalDateTime expireAt) {
        try {
            shortLinkMapper.insert(new ShortLink(code, longUrl, expireAt));
            return true;
        } catch (DuplicateKeyException e) {
            return false;                                    // 唯一索引兜底:最终防线
        }
    }
}

3.3 布隆过滤器的三个工程注意点

注意点说明
不能删除标准布隆过滤器无法删除元素(位是共享的)。过期短码留在过滤器里只是"多几次误判进 DB 查询",无害;非要删除用 Counting Bloom Filter 或定期重建
重建方案服务重启/扩容误判率恶化时:从 DB 全量 reload 短码重建(亿级约几分钟,凌晨低峰执行)
误判率权衡0.01 误判率 = 1% 的"不存在短码"会穿透到 DB。配合跳转接口的空值缓存(见第五章)可以把它压到接近 0

四、存储设计:一张表撑起跳转

4.1 表结构

CREATE TABLE short_link (
    id           BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    short_code   VARCHAR(16)  NOT NULL COMMENT '短码',
    long_url     VARCHAR(2048) NOT NULL COMMENT '原始长链',
    status       TINYINT      NOT NULL DEFAULT 1 COMMENT '1有效 0禁用',
    expire_at    DATETIME              DEFAULT NULL COMMENT '过期时间 NULL=永久',
    creator      VARCHAR(64)           DEFAULT NULL,
    created_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
                                 ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uk_short_code (short_code),          -- 核心唯一索引:冲突终审 + 跳转查询
    KEY idx_long_url (long_url(64)),                -- 幂等查询(前缀索引省空间)
    KEY idx_expire_at (expire_at)                   -- 过期清理任务扫描
) ENGINE=InnoDB COMMENT='短链映射表';

设计要点:

要点说明
uk_short_code整个系统的正确性基石:布隆可能误判,最终防线是唯一索引
long_url 用前缀索引2048 长度的字段建全量索引太重;前缀 64 位足够区分
不做物理删除过期短链被查询时判断 expire_at 返回 404 页;物理清理走定时任务 + 归档
单表容量评估千万级短链单表无压力;到亿级再按 short_code 哈希分表(分表键=查询键,路由零成本)

4.2 跳转 SQL 就一条

SELECT long_url, status, expire_at
FROM short_link
WHERE short_code = 'aB3xK9';
-- 唯一索引命中,单行返回——但别让 5000 QPS 直接打这里,这是第五章缓存的活

五、高性能跳转:Redis 缓存 + 302

5.1 301 vs 302:一个必须踩对的选项

状态码语义对短链系统的影响
301 永久重定向浏览器缓存映射,后续不再请求短链服务✅ 减少服务端压力;❌ 统计全丢、过期短链无法下线、换目标地址失效
302 临时重定向每次都请求短链服务✅ 统计完整、随时可改映射/下线;❌ 服务端承压(由缓存解决)

结论:一律 302。业务短链 99% 需要点击数据和可管理性,"永久"是伪需求。个别纯 CDN 加速场景才考虑 301。

5.2 跳转接口完整实现

@RestController
@RequiredArgsConstructor
public class RedirectController {

    private final StringRedisTemplate redis;
    private final ShortLinkMapper shortLinkMapper;
    private final RBloomFilter<String> bloomFilter;
    private final AccessEventPublisher accessPublisher;   // 统计事件发布

    private static final String KEY_PREFIX = "short:url:";

    @GetMapping("/{shortCode}")
    public ResponseEntity<Void> redirect(@PathVariable String shortCode,
                                         HttpServletRequest request) {
        // ① 短码格式校验(62进制6位),非法直接 404,挡住大量恶意扫描
        if (!shortCode.matches("[0-9a-zA-Z]{6}")) {
            return notFound();
        }

        // ② Redis 查缓存
        String longUrl = redis.opsForValue().get(KEY_PREFIX + shortCode);

        if (longUrl == null) {
            // ③ 布隆过滤器兜底:肯定不存在 → 404,DB 一个查询都不浪费
            if (!bloomFilter.contains(shortCode)) {
                return notFound();
            }
            // ④ 查 DB
            ShortLink link = shortLinkMapper.selectByCode(shortCode);
            if (link == null) {
                // ⑤ 空值缓存 60s:布隆误判的 1% 在这里被二次拦截
                redis.opsForValue().set(KEY_PREFIX + shortCode, "", 60, TimeUnit.SECONDS);
                return notFound();
            }
            if (!link.isAccessible()) {          // 禁用或过期
                return gone();                    // 410 Gone
            }
            longUrl = link.getLongUrl();
            // ⑥ 回填缓存:映射不可变,可以给长 TTL(如 7 天)
            redis.opsForValue().set(KEY_PREFIX + shortCode, longUrl, 7, TimeUnit.DAYS);
        } else if (longUrl.isEmpty()) {
            return notFound();                    // 空值缓存命中
        }

        // ⑦ 异步发统计事件(不阻塞跳转主链路)
        accessPublisher.publish(shortCode, request);

        // ⑧ 302 重定向
        return ResponseEntity.status(HttpStatus.FOUND)
                .location(URI.create(longUrl))
                .build();
    }

    private ResponseEntity<Void> notFound() {
        // 404 页面可以做成业务入口(活动页/官网),别浪费流量
        return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    }
}

5.3 链路各层的作用总结

请求 → 格式校验(正则,拦恶意扫描)
     → Redis(99.9% 的请求到这就结束了,单次 <1ms)
     → 布隆过滤器(拦"肯定不存在",DB 零消耗)
     → 空值缓存(拦布隆误判的 1%)
     → DB 唯一索引(最后兜底)

压测参考数据(4C8G 容器 × 2 实例 + 同机房 Redis,wrk 压测):纯缓存命中路径 QPS ≈ 18000,P99 ≈ 3ms;DB 回源路径 QPS ≈ 3500。缓存命中率稳定在 99.9% 以上——热点短链的访问高度集中(头部 1% 短链占 60% 流量),长 TTL 策略完全成立。


六、访问统计:异步化是唯一正解

6.1 为什么不能同步写

如果跳转时同步执行"insert 一条访问日志 + update 计数":

  • 每次 5000 QPS 的跳转变成了 5000 QPS 的 DB 写,统计成为整个系统的性能瓶颈
  • 统计服务的任何抖动直接拖垮跳转——本末倒置。

6.2 方案:跳转只发事件,统计端批量落库

/**
 * 跳转链路侧:只发事件,内存队列 + 批量发送,开销 < 1ms
 * 流量大时直接换 Kafka/RocketMQ 生产者
 */
@Component
@RequiredArgsConstructor
public class AccessEventPublisher {

    private final BlockingQueue<AccessEvent> queue = new LinkedBlockingQueue<>(100_000);
    private final KafkaTemplate<String, String> kafka;

    public void publish(String shortCode, HttpServletRequest request) {
        AccessEvent event = new AccessEvent(
                shortCode,
                clientIp(request),                 // UV 依赖(脱敏后的 IP/设备指纹)
                request.getHeader("User-Agent"),   // 端型分析
                request.getHeader("Referer"),      // 来源
                System.currentTimeMillis());
        if (!queue.offer(event)) {
            return;                                 // 队列满直接丢弃:统计可容忍丢点,跳转不可容忍阻塞
        }
    }

    /** 批量刷出到 MQ,5 秒或 500 条触发 */
    @Scheduled(fixedDelay = 5000)
    public void flush() {
        List<AccessEvent> batch = new ArrayList<>(500);
        queue.drainTo(batch, 500);
        if (!batch.isEmpty()) {
            kafka.send("short-link-access", JSON.toJSONString(batch));
        }
    }
}
/**
 * 统计服务侧:消费事件 → 聚合落库(PV 计数 + UV 去重)
 */
@Component
public class AccessEventConsumer {

    @KafkaListener(topics = "short-link-access", groupId = "access-stats")
    public void onMessage(String message) {
        List<AccessEvent> events = JSON.parseArray(message, AccessEvent.class);
        for (AccessEvent e : events) {
            // PV:Redis INCR,按 短码+天 聚合
            redis.opsForValue().increment("pv:" + e.getShortCode() + ":" + dayKey(e.getTs()));
            // UV:HyperLogLog 去重,内存占用固定 ~12KB/短码/天
            redis.opsForHyperLogLog()
                 .add("uv:" + e.getShortCode() + ":" + dayKey(e.getTs()), e.getClientIp());
            // 明细异步写数仓/ClickHouse,供渠道归因等深度分析
        }
    }
}

统计设计的取舍表:

维度方案理由
PVRedis INCR(短码+天 为 key)原子计数,定时刷回 MySQL
UVHyperLogLog12KB 固定内存做到亿级去重,误差 0.81% 对报表完全可接受
明细MQ → 数仓/ClickHouse渠道归因、漏斗分析是离线场景,别在线上库做
丢失容忍队列满丢弃 / Redis 未落盘丢失统计是"软需求",跳转是"硬需求",永远让路

七、进阶:用发号器(Snowflake)替代哈希

7.1 哈希方案 vs 发号器方案

写流量起来之后(日增百万短链),哈希方案的两个别扭会放大:

  1. 冲突处理成为常态——1000 万存量时 8.8% 冲突率意味着"加盐重试"高频发生,且布隆重建时间越来越长;
  2. 短码长度不可控——哈希值分布均匀,短码"长短随机",总有一些尾部 0 的浪费。

发号器方案:不用长链内容生成短码,而是用一个全局递增 ID,编码成 62 进制——ID 唯一 → 短码必然唯一,冲突问题从物理上消失

/**
 * 发号器方案:Snowflake ID → Base62
 * 关键:Snowflake 趋势递增 → 低 38 位变化空间足够,直接用(时间戳部分参与编码)
 */
public class SnowflakeShortCodeGenerator {

    private final Snowflake snowflake;   // MyBatis-Plus 内置 / 自研均可

    public String genShortCode() {
        long id = snowflake.nextId();
        // 取 id 的低 38 位(2748 亿空间)+ 随机 6 位随机数混淆防遍历
        long mixed = (id & 0x3FFFFFFFFL) ^ ThreadLocalRandom.current().nextInt(62);
        return ShortCodeGenerator.encode62(mixed);
    }
}

两个方案对比(选型就看这张表):

维度哈希方案(MurmurHash)发号器方案(Snowflake/号段)
冲突存在,需布隆+重试+唯一索引物理上无冲突
同长链幂等天然幂等(同链同码)需额外查重实现幂等
短码可预测性均匀随机,天然防遍历递增可被遍历爬取 → 需混淆位
依赖需要发号器(Snowflake 依赖机器时钟,需处理回拨)
自定义短码需额外逻辑统一走"号段+自定义保留区间"
适用规模千万级以内亿级以上 / 开放平台(对外发号)

我们的最终架构:内部运营场景哈希方案足够(幂等省心);对外 API/超大规模用发号器。两者共用同一张表和同一条跳转链路——短码只是字符串,跳转侧根本不关心它怎么来的,这正是分层的好处。


八、常见问题

8.1 短链服务宕机,用户点击会怎样?

302 短链不缓存,跳转服务不可用 = 点击全部失败。高可用三件套:服务多实例 + Redis 主从/哨兵 + DB 直连兜底路径(缓存层全挂时,布隆+DB 链路仍能跳转,QPS 降低但不中断)。更狠的做法是 CDN 边缘缓存头部 1 万条热点短链的 302 响应(牺牲部分统计精度)。

8.2 短码被恶意遍历爬取怎么办?

哈希方案短码空间随机,遍历成本高;发号器方案递增可遍历。防御:① 短码加随机混淆位(第七章代码);② 同一 IP 高频访问不同短码 → 风控限流;③ 短链服务加鉴权开关(内部系统短链要求登录态)。

8.3 短链被滥用(spam 链接)怎么治理?

三道关:创建时黑名单校验(域名信誉库)+ 长链内容抽检;跳转时目标站点被举报/拉黑 → 拦截页;统计侧发现异常流量模式(单短码异常高频、异常 UA)自动禁用短码。开放平台的短链服务必须有治理,否则会沦为钓鱼链接的中转站

8.4 Redis 全部失效(如主从切换数据丢失)怎么办?

冷启动雪崩防御:缓存 TTL 加随机抖动(7 天 ± 8h)避免同时过期;全量失效时靠布隆过滤器挡住不存在短码,存在短码回源 DB(QPS 3500 的回源能力 + 限流保护),系统降级运行不雪崩。恢复期用"提前后台预热 + 渐进放量"。

8.5 统计的 UV 用 IP 准确吗?

不准确但有参考价值:移动网络出口 IP 聚合、IPv6 变化都会失真。生产做法:IP + User-Agent 哈希做伪 ID,或前端种 Cookie/设备指纹拿真实 ID。统计精度取决于业务需要:运营报表 IP+UA 够用,广告计费必须上设备指纹 + 防作弊。

8.6 为什么不直接用现成的开源方案?

有(如自建基于 Nginx+Redis 的轻量方案、部分云厂商的短链服务)。自研的价值在于:统计深度定制(渠道归因)、与企业内权限/审计体系打通、数据自主。本文的架构恰好也是一次很好的综合练习——哈希、布隆过滤器、缓存、MQ、发号器,每一块都是后端面试与实战的高频组件。


九、总结

链路速查卡

创建链路(写,~100 QPS)
  长链 → 幂等查重 → MurmurHash → Base62
       → Bloom Filter 初筛 → DB 唯一索引终审
       → 冲突加盐重试 → 成功:登记布隆 + 预热缓存

跳转链路(读,~5000 QPS)
  短码 → 格式校验 → Redis(99.9%命中) → 布隆兜底
       → 空值缓存 → DB 回源 → 回填缓存(TTL 7天)
       → 异步发统计事件 → 302

统计链路(异步,可丢)
  内存队列 → 批量 → MQ → PV(INCR) + UV(HyperLogLog) + 明细入数仓

关键数据

  • 短码:6 位 Base62,568 亿空间
  • 冲突概率:100 万短链 0.09%,1000 万 8.8%(发号器方案归零)
  • 布隆过滤器:1 亿元素、1% 误判 ≈ 116MB
  • 跳转性能:缓存命中 P99 3ms、QPS 18000(2 实例压测)
  • 统计开销:跳转链路 <1ms(内存队列 + 批量发送)

一句话

短链系统的三层功力:短码生成层解决"唯一性"(哈希+布隆+唯一索引,或发号器一劳永逸);跳转层解决"高并发"(不可变映射让缓存命中率天然 99.9%,302 保住统计和可管理性);统计层解决"不拖累主链路"(异步是唯一正解,软需求永远给硬需求让路)。把读写分离的直觉刻进架构里,5000 QPS 的跳转不过是缓存面前的一碟小菜。

给团队的建议

阶段建议
起步期(<100 万短链)哈希方案 + Redis 缓存,一天上线
成长期引入布隆过滤器 + 空值缓存 + 统计异步化
规模期(亿级/开放 API)切发号器方案,短码按需分表,统计入数仓
永远要做唯一索引兜底、302 而非 301、跳转链路限流与降级预案

互动话题:你们的短链服务是自研还是买的?短码生成用的哈希还是发号器?踩过什么统计的坑?评论区聊聊。


参考资料


标题:从0到1搭建短链接系统:哈希冲突+跳转高性能+访问统计——完整实现
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/08/1788593702072.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消