在我们初次构建PlanetScale的API时,我们需要决定使用哪种标识符类型。我们明确知道,不希望使用整数ID,以免泄露我们所有数据表中的记录数量。

解决此问题的常见方法是使用UUID(通用唯一标识符)。UUID很不错,因为它几乎不可能生成重复,它还能隐藏内部ID。然而,UUID有一个问题,它在URL中占用了大量空间,例如:
api.planetscale.com/v1/deploy-requests/7cb776c5-8c12-4b1a-84aa-9941b815d873

试着双击这个URL中的ID来选择并复制它,你会发现不能直接选中。浏览器会将其解释为由5个不同的单词组成。

虽然这看起来只是个小问题,但如果我们希望构建一个让开发者喜欢的产品,那么细节必须考虑周全。

NanoID

我们决定自己的ID需要满足以下几个条件:

  • 比UUID更短
  • 容易通过双击选中
  • 低概率碰撞
  • 能够在多种编程语言中方便生成(我们的后端使用Ruby和Go)

这促使我们选择了NanoID,它完全满足这些需求。

以下是一些NanoID示例:

izkpm55j334u  
z2n60bhrj7e8  
qoucu12dag1x  

这些ID在URL中更加便于使用:
api.planetscale.com/v1/deploy-requests/izkpm55j334u

ID长度与碰撞概率

ID碰撞指的是生成两个相同ID的情况。如果碰撞概率很低,这不是大问题。应用可以检测到碰撞,自动生成新ID,然后继续工作。但如果碰撞经常发生,就会成为一个重大问题。

ID越长、越复杂,发生碰撞的概率越低。所需ID复杂度取决于应用场景。在我们的案例中,我们使用了NanoID碰撞工具,并决定使用12字符长的ID,字母表为0123456789abcdefghijklmnopqrstuvwxyz

根据计算,这样的组合会在接下来的约35年中产生1%的碰撞概率,假设我们每小时生成1,000个ID。

如果将来需要降低碰撞概率,我们只需增加ID生成器生成的长度,然后更新数据库模式以支持新的ID大小。

在Rails中生成NanoID

我们的API是一个基于Ruby on Rails的应用。对于所有面向公开的模型,我们在数据库中添加了一个public_id列。我们仍然为主键使用标准的自增BigInt,而public_id仅作为外部标识符。

示例数据库模式

我们添加了public_id列,并同时增加了唯一约束以防止重复:

SQL1CREATE TABLE `user` (
2  `id` bigint NOT NULL AUTO_INCREMENT,
3  `public_id` varchar(12) DEFAULT NULL,
4  `name` varchar(255) NOT NULL,
5  `created_at` datetime(6) NOT NULL,
6  `updated_at` datetime(6) NOT NULL,
7  PRIMARY KEY (`id`),
8  UNIQUE KEY `idx_public_id` (`public_id`)
9) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

自动生成ID

我们创建了一个可以被模型共享的模块,用来自动为我们生成ID。在Rails中,模块(Concern)可以提高代码复用,减少重复。每当应用创建新记录时,这段代码都会运行,并生成ID:

模型代码:

Ruby1# app/models/user.rb
2class User < ApplicationRecord
3  # 对每个需要public_id的模型,包含生成器
4  include PublicIdGenerator
5end

生成器代码:

Ruby1# app/models/concerns/public_id_generator.rb
2
3require "nanoid"
4
5module PublicIdGenerator
6  extend ActiveSupport::Concern
7
8  included do
9    before_create :set_public_id
10  end
11
12  PUBLIC_ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
13  PUBLIC_ID_LENGTH = 12
14  MAX_RETRY = 1000
15
16  PUBLIC_ID_REGEX = /[#{PUBLIC_ID_ALPHABET}]{#{PUBLIC_ID_LENGTH}}\z/
17
18  class_methods do
19    def generate_nanoid(alphabet: PUBLIC_ID_ALPHABET, size: PUBLIC_ID_LENGTH)
20      Nanoid.generate(size: size, alphabet: alphabet)
21    end
22  end
23
24  # 为我们生成随机字符串作为public ID
25  def set_public_id
26    return if public_id.present?
27    MAX_RETRY.times do
28      self.public_id = generate_public_id
29      return unless self.class.where(public_id: public_id).exists?
30    end
31    raise "Failed to generate a unique public id after #{MAX_RETRY} attempts"
32  end
33
34  def generate_public_id
35    self.class.generate_nanoid(alphabet: PUBLIC_ID_ALPHABET)
36  end
37end

在Go中生成NanoID

NanoID生成器可用于多种语言。在PlanetScale,我们还有一个基于Go的后端服务,它也需要生成public_id

以下是我们的Go实现代码:

Go1// Package publicid provides public ID values in the same format as
2// PlanetScale’s Rails application.
3package publicid
4
5import (
6	"strings"
7
8	nanoid "github.com/matoous/go-nanoid/v2"
9	"github.com/pkg/errors"
10)
11
12// Fixed nanoid parameters used in the Rails application.
13const (
14	alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
15	length   = 12
16)
17
18// New generates a unique public ID.
19func New() (string, error) { return nanoid.Generate(alphabet, length) }
20
21// Must is the same as New, but panics on error.
22func Must() string { return nanoid.MustGenerate(alphabet, length) }
23
24// Validate checks if a given field name’s public ID value is valid according to
25// the constraints defined by package publicid.
26func Validate(fieldName, id string) error {
27	if id == "" {
28		return errors.Errorf("%s cannot be blank", fieldName)
29	}
30
31	if len(id) != length {
32		return errors.Errorf("%s should be %d characters long", fieldName, length)
33	}
34
35	if strings.Trim(id, alphabet) != "" {
36		return errors.Errorf("%s has invalid characters", fieldName)
37	}
38
39	return nil
40}

总结

为开发者提供绝佳的体验是PlanetScale的一项重要目标。这些看似微不足道的细节,例如是否能快速复制ID,都会对体验产生重要影响。NanoID能够满足我们对应用的需求,同时不牺牲开发者体验。



为什么我们为PlanetScale的API选择了NanoID插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.choupangxia.com/2025/05/23/why-we-chose-nanoids-for-planetscales-api/