李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 网站地图
    • 网站统计
    • 另一个网站
    • 我的导航站
    • 赞助
  • 留言
  • 走心评论
  • 系列文章
  • Now
  • 每日心情
  • 论坛
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 原创
  3. 正文

Java设计支持千万级别的短链

2025年2月21日 约 1,941 字7 分钟 27点热度 0人点赞 0条评论
本文最后更新于 2025年2月21日,距今已 537 天,其中的信息可能已经发生变化,请注意甄别。

短链生成的几种方法

业界实现短链的方式大概是有两种。

1. Hash算法

由长url通过 hash 算法,生成短的url,如果hash冲突,需要解决解决hash冲突。那么这个哈希函数该怎么取呢,相信肯定有很多人说用 MD5,SHA 等算法,其实这样做有点杀鸡用牛刀了,而且既然是加密就意味着性能上会有损失,我们其实不关心反向解密的难度,反而更关心的是哈希的运算速度和冲突概率。

能够满足这样的哈希算法有很多,这里推荐 Google 出品的 MurmurHash 算法,MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。与其它流行的哈希函数相比,对于规律性较强的 key,MurmurHash 的随机分布特征表现更良好。非加密意味着着相比 MD5,SHA 这些函数它的性能肯定更高(实际上性能是 MD5 等加密算法的十倍以上),也正是由于它的这些优点,所以虽然它出现于 2008,但目前已经广泛应用到 Redis、MemCache、Cassandra、HBase、Lucene 等众多著名的软件中。

1.1 如何缩短域名

MurmurHash32会生成32位的十进制,MurmurHash64会生成64位的十进制。那我们把它转为 62 进制可缩短它的长度,为什么是62进制,不是64呢?因为62进制表示 【a-z A-Z 0-9】字符之和。

1.2 如何解决hash冲突

在优秀的哈希函数,都不可避免地会产生哈希冲突(尽管概率很低),该怎么解决呢。我们设计如下mysql表

CREATE TABLE `short_url` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `lurl` varchar(150) NOT NULL,
  `surl` varchar(10) NOT NULL,
  `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_surl` (`surl`),
  KEY `idx_lurl` (`lurl`)
) ENGINE=InnoDB AUTO_INCREMENT=15536 DEFAULT CHARSET=utf8;
  1. 获取长url,使用murmur64进行hash,并且使用Base62 encode一下,取前6位
  2. 根据短链去short_url表中查找看是否存在相关记录,如果不存在,将长链与短链对应关系插入数据库中,存储。
  3. 如果存在,则hash冲突了。此时在长串上拼接一个随机字段(注意这块优化),再次hash即可,直到没有冲突为止。

以上步骤显然是要优化的,插入一条记录居然要经过两次 sql(根据短链查记录,将长短链对应关系插入数据库中),如果在高并发下,显然会成为瓶颈。

  1. 我们需要给短链字段 surl 加上唯一索引
  2. 我们hash之后插入数据库,如果插入失败,说明违反了唯一性索引,此时我们重新 hash 再插入即可,看起来在违反唯一性索引的情况下是多执行了步骤,但我们要知道 MurmurHash 发生冲突的概率是非常低的,基本上不太可能发生,所以这种方案是可以接受的。
  3. 如果同一个URL,频繁请求,这种会冲突多次,对此我们引入了LRU Cache,进行判断,如果在cache里面,直接返回即可,不在生成之后,再加入到cache里面

也就是整一个流程我们只和数据库有一次交互,同时我们引入了LRU的缓存,极大了提高了性能。

2. 发号器

维护一个自增id,比如 1,2,3 这样的整数递增 ID,当收到一个长链转短链的请求时,ID 生成器为其分配一个 ID,再将其转化为 62 进制,拼接到短链域名后面就得到了最终的短网址。但此方法需要全局维护一个自增id,同时同一个长的url会生成不同的短的url,并且短的url会有规律,比较容易猜测到。

常见的有以下几种:uuid,redis计数,Snowflake雪花算法,Mysql 自增主键。总和比较感觉雪花算法以及redis计数比较靠谱,可以尝试去使用。

Hash函数

本次选择的hash映射方式,来生成短链。底层数据存储选择是mysql,通过mysql的分库分表,读写分离,也可以有非常高效的效率。如果采用redis,缓存会丢失数据,如果采用hbase,效率不可控,故最后选择mysql作为底层存储数据。

先说下hash函数测试的结论,比较有说服力, 可以直接看HashTest类

100W数据,murmur32算法(产生一个32位的hash值),100W大概会有121个冲突

i = 100000(10W), conflictSize = 1
i = 200000(20W), conflictSize = 6
i = 300000(30W), conflictSize = 12
i = 400000(40W), conflictSize = 19
i = 500000(50W), conflictSize = 32
i = 600000(60W), conflictSize = 46
i = 700000(70W), conflictSize = 54
i = 800000(80W), conflictSize = 76
i = 900000(90W), conflictSize = 94
i = 1000000(100W), conflictSize = 121

修改为 murmur64算法,100W 0冲突,500W 0冲突,建议使用murmur64算法

算法实现

生成url核心算法(着重看下hash冲突解决方法 && LRU的cache也需要关注)
public String generateShortUrl(String longUrl) {
    if (StringUtils.isEmpty(longUrl)) {
        throw new RuntimeException("longUrl 不能为空");
    }

    String shortUrl = CacheUtils.get(MapConstants.longCache, longUrl);
    if (StringUtils.isNotEmpty(shortUrl)) {
        return shortUrl;
    }

    return getShortUrl(longUrl, getLongUrlRandom(longUrl));
}

private String getShortUrl(String rawUrl, String longUrl) {
    long hash = HashUtil.murmur64(longUrl.getBytes());
    String base62 = Base62.encode(hash + "");
    log.info("longUrl = {}, hash = {}, base62 = {}", longUrl, hash, base62);
    if (StringUtils.isEmpty(base62)) {
        throw new RuntimeException("hash 算法有误");
    }

    String shortUrl = StringUtils.substring(base62, 6);
    ShortUrl url = new ShortUrl(rawUrl, shortUrl);
    try {
        int insert = shortUrlDAO.insert(url); // 这里进行分库分表 提高性能
        if (insert == 1) {
            CacheUtils.put(MapConstants.longCache, rawUrl, shortUrl);
        }
    } catch (DuplicateKeyException  e) {
        // Hash冲突
        log.warn("hash冲突 触发唯一索引 rawUrl = {}, longUrl = {}, shortUrl = {}, e = {}", rawUrl, longUrl, shortUrl, e.getMessage(), e);
        CacheUtils.put(MapConstants.hashFailMap, rawUrl, shortUrl);
        return getShortUrl(rawUrl, getLongUrlRandom(shortUrl));
    } catch (Exception e) {
        log.error("未知错误 e = {}", e.getMessage(), e);
        throw new RuntimeException("msg = " + e.getMessage());
    }

    return shortUrl;
}

private String getLongUrlRandom(String longUrl) {
    return longUrl + RandomUtil.randomString(6);  // 解决冲突多的问题,随机字符串
}
获取url核心算法
public String getLongUrl(String shortUrl) {
    if (StringUtils.isEmpty(shortUrl)) {
        throw new RuntimeException("shortUrl 不能为空");
    }

    String longUrl = CacheUtils.get(MapConstants.shortCache, shortUrl);
    if (StringUtils.isNotEmpty(longUrl)) {
        return longUrl;
    }

    LambdaQueryWrapper<ShortUrl> wrapper = new QueryWrapper<ShortUrl>().lambda().eq(ShortUrl::getSUrl, shortUrl);
    ShortUrl url = shortUrlDAO.selectOne(wrapper);
    CacheUtils.put(MapConstants.shortCache, shortUrl, url.getLUrl());
    return url.getLUrl();
}

可以看到生成短链只需要访问一次数据库,获取短链也只需要访问一次数据库,是非常的快的。

优化点(难点、亮点)

  1. 生成短链只需要访问一次数据库。而不是传统的先查询,在判断插入,而是直接插入,用唯一索引来判断是否hash冲突
  2. 利用LRUCache,将最近生成的几千个kv放进map中,一段时间内,同一个长url会生成相同的短url
  3. hash冲突后,给hash冲突值 加一个随机url,降低冲突概率
  4. 选择比较优秀的murmur64 hash算法
  5. get获取常链的时候,利用LRU识别热点数据,直接从map中读取,防止打挂数据库

最后

本文对短链设计方案作了详细地剖析,旨在给大家提供几种不同的短链设计思路,文中涉及到挺多的技术细节。比如murmur64 hash算法,base62,LRU,以及为什么选择mysql,而不是redis等等。文中没有展开讲,建议大家回头可以去再详细了解一下,同时也希望大家有空,可以自己动手实现一套短链服务,一定会有不小的收获。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/article/4221

推荐阅读

  • SpringBoot 实现接口防刷的 5 种实现方案
  • URL地址末尾加不加“/”有什么区别
  • 部署consul配置中心
  • 浅谈一下redis分布式锁和zookeeper分布式锁的区别以及各自的优缺点
  • 共识算法之Paxos 协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: URL 分布式 短链
最后更新:2025年2月21日

岁月同一天 8 月 12 日

回望过去的今天,你在写什么

  • 6 年前 2020年8月12日
    jstat命令使用(JDK1.8)

    概述 jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下: jstat [-命令选项] [vm…

相关文章
  • Kafka 为什么要抛弃 Zookeeper?2025年10月11日
  • RedisTemplate和Redisson的区别2025年5月29日
  • 分布式锁-Zookeeper实现分布式锁2019年11月11日
  • Redis 不只是缓存:8 大实战场景 + 深度避坑指南,从入门到架构师级应用2025年10月13日
  • URL地址末尾加不加“/”有什么区别2025年5月23日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

不将沉重累坠的银元装在怀中,来自讨无谓的苦吃。

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Kratos+ v1.1.16版本更新说明 WorkBuddy介绍 Kratos+ v1.1.14版本更新说明 Spring Boot 指定外部配置文件的方式 Spring Boot 配置加载优先级总结 Claude Fable 5(claude-fable-5)深度详解
给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页这个域名注册整整十年了,十年时间,真快啊Kratos+ v1.1.14版本更新说明WordPress实现用户评论等级排行榜插件
使用shell脚本统一修改maven项目的版本 图数据库选型:Neo4j、Janus、HugeGraph 今晚,回家过年! ElasticSearch入门-基本概念介绍以及安装 SpringBoot 实现 RSA+AES 自动接口解密 Kafka常见面试题(一)
最近评论
李锋镝 发布于 2 天前(08月10日) 等我搞一个数据转换的插件~
李锋镝 发布于 2 天前(08月10日) 精美可担不起~😂
李锋镝 发布于 2 天前(08月10日) PHP是世界上最伟大的语言=。=
Hary 发布于 3 天前(08月09日) 我都想用了,但是是ty,转换有点麻烦
老张博客 发布于 4 天前(08月08日) 做的越来越精美了,好看。
标签聚合
Spring JAVA K8s SQL Claude Redis JVM 设计模式 分布式 MySQL IDEA 架构 ElasticSearch 日常 SpringBoot AI编程 AI 数据库 WordPress 多线程
友情链接
  • Serendipity
  • 老张博客
  • 韩小韩博客
  • Mr.Sun的博客
  • 九仞之行
  • 志文工作室
  • 风渡言
  • 知向前端
  • 拾趣博客导航
  • Honesty
  • 瓦匠个人小站
  • 旧时繁华
  • 彬红茶日记
  • 懋和道人
  • 临窗旋墨
  • 搬砖日记
  • 林羽凡
  • 哥斯拉
  • 韩情脉脉
  • 皮皮社

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

正在博友圈履约中

域名年龄

Theme Kratos+ By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号