写了一个支持千万级别的短链生成器,欢迎大家交流沟通

153 天前
 bigbigeggs

写了一个支持千万级别的短链生成器,个人感觉比网上的都优秀不少,甚至是最优秀的,优化做到了极致,欢迎大家沟通交流。 如果哪位大佬有比较好的机器,欢迎压测一波,看看性能能到哪里去

代码 github 链接 short-url

优化点(难点、亮点)

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

核心代码如下

  1. 生成 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);  // 解决冲突多的问题,随机字符串
}
  1. 获取 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();
}

2438 次点击
所在节点    分享创造
24 条回复
bigbigeggs
149 天前
@nicoljiang 感谢分享,刚才简单 google 了下 leveldb ,有点像 mongdb ?如果效率高,可以考虑使用,至于为什么我没有选择计数器,使用计数器还需要单独维护一个全局变量,使用中间件的话,反而降低了短链的稳定性
bigbigeggs
149 天前
@oiYuer 如果单说性能的话,要求非常非常高。openresty+lua 是一个很好的解决方案,但它的稳定性感觉有待商榷
emartcn
148 天前
最流行的高性能数据库用上:sqlite
nicoljiang
148 天前
@bigbigeggs 不像 mongodb ;你已经维护一个巨大的 map ,但却不想多维护一个纯数字的定时器么。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1050974

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX