likes
comments
collection
share

如何为上传文件取一个唯一的文件名我们对 CDN 文件的缓存策略是持久化强缓存,这就要求所有上传文件的文件名都是唯一的,否

作者站长头像
站长
· 阅读数 71

作者:陈杰

背景

古茗内部有一个 CDN 文件上传平台,用户在平台上传文件时,会将文件上传至阿里云 OSS 对象存储,并将 OSS 链接转换成 CDN 链接返回给用户,即可通过 CDN 链接访问到文件资源。我们对 CDN 文件的缓存策略是持久化强缓存(Cache-Control: public, max-age=31536000),这就要求所有上传文件的文件名都是唯一的,否则就有文件被覆盖的风险。有哪些方式可以保证文件名全局唯一?

唯一命名方式

方式一:使用时间戳+随机数

这是我们最容易想到的一种方式:

const name = Date.now() + Math.random().toString().slice(2, 6);
// '17267354922380490'

使用时间戳,加上 4 位随机数,已经可以 99.99999% 保证不会存在文件名重复。可以稍微优化一下:

const name = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
// 'm191x7bii63s'

将时间戳和随机数分别转换成 36 进制,以减少字符串长度。通过上面一步优化可以将字符长度从 17 位减少至 12 位。

使用时间戳+随机数作为文件名的优势是简单粗暴,基本上可以满足述求;但是有极小概率存在文件名冲突的可能。

方式二:使用文件 MD5 值

生成文件的 MD5 Hash 摘要值,在 node 中代码示例如下:

const crypto = require('crypto');
const name = crypto.createHash('md5').update([file]).digest('hex');
// 'f668bd04d1a6cfc29378e24829cddba9'

文件的 MD5 Hash 值可以当成文件指纹,每个文件都会生成唯一的 hash 值(有极小的概率会 hash 碰撞,可以忽略)。使用 MD5 Hash 值作为文件名还可以避免相同文件重复上传;但是缺点是文件名较长。

方式三:使用 UUID

UUID (通用唯一识别码) 是用于计算机体系中以识别信息的一个标识符,重复的概率接近零,可以忽略不计。生成的 UUID 大概长这样:279e573f-c787-4a84-bafb-dfdc98f445cc。

使用 UUID 作为文件名的缺点也是文件名较长。

最终方案

从上述的几种命名方式可以看出,每种方式都有各种的优缺点,直接作为 OSS 的文件命名都不是很满意(期望 CDN 链接尽可能简短)。所以我们通过优化时间戳+随机数方式来作为最终方案版本。

本质上还是基于时间戳随机数 2 部分来组成文件名,但是有以下几点优化:

  • 由于 CDN 链接区分大小写,可以充分利用 数字+大写字母+小写字母(一共 62 个字符),也就是可以转成 62 进制,来进一步缩短字符长度
  • 时间戳数字的定义是,当前时间减去 1970-01-01 的毫秒数。显然在 2024 年的今天,这个数字是非常大的。对此,可以使用 当前时间减去 2024-01-01 的毫秒数 来优化,这会大幅减少时间戳数字大小(2024-01-01 这个时间点是固定的,而且必须是功能上线前的一个时间点,确保不会减出负数)

示例代码如下:

/**
 * 10 进制整数转 62 进制
 */
function integerToBase62(value) {
  const base62Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  const base62 = base62Chars.length;
  value = parseInt(value);
  if (isNaN(value) || !value) {
    return String(value);
  }

  let prefix = '';
  if (value < 0) {
    value = -value;
    prefix = '-';
  }

  let result = '';
  while (value > 0) {
    const remainder = value % base62;
    result = base62Chars[remainder] + result;
    value = Math.floor(value / base62);
  }

  return prefix + result || '0';
}

const part1 = integerToBase62(Date.now() - new Date('2024-01-01').getTime()); // 'OkLdmK'
const part2 = integerToBase62(Math.random() * 1000000).slice(-4); // '3hLT'
const name = part1 + part2; // 'OkLdmK3hLT'

最终文件名字符长度减少到 10 位。但是始终感觉给 4 位随机数太浪费了,于是想着能否在保证唯一性的同时,还能减少随机数的位数。那就只能看看时间戳部分还能不能压榨一下。

只要能保证同一毫秒内只生成一个文件的文件名,就可以保证这个文件名是唯一的,这样的话,随机数部分都可以不要了,所以可以做如下优化:

// 伪代码
async function getFileName() {
  // 等待锁释放,并发调用时保证至少等待 1ms
  await waitLockRelease();

  return integerToBase62(Date.now() - new Date('2024-01-01').getTime());
}

const name = await getFileName();
// 'OkLdmK'

由于 node 服务线上是多实例部署,所以 waitLockRelease 方法是基于 Redis 来实现多进程间加锁,保证多进程间创建的文件名也是唯一的。与此同时,还额外加上了一位随机数,来做冗余设计。最终将文件名字符长度减少至 7 位,且可以 100% 保证唯一性!

总结

看似非常简单的一个问题,想要处理的比较严谨和完美,其实也不太容易,甚至引入了 62 进制编码及加锁逻辑的处理。希望本文的分享能给大家带来收获!

转载自:https://juejin.cn/post/7424901430378545164
评论
请登录