You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails 5中高效生成Basecamp风格hash_id替代自增ID的方案

嘿,关于在Rails 5里生成高效的Basecamp式Hash ID来替代自增ID的问题,我刚好有几个靠谱的方案,能帮你摆脱频繁exists?查询带来的性能困扰:

方案1:数据库唯一约束 + 异常重试(推荐)

这个方案的核心思路是:放弃提前查询exists?,转而依赖数据库的唯一索引保证Hash ID的唯一性,仅在极罕见的碰撞发生时重试。这种方式能大幅减少数据库查询次数,性能提升非常明显。

具体步骤:

  1. 给Hash ID字段添加唯一索引
    先写一个迁移文件,确保hash_id字段在数据库层面是唯一的:

    class AddUniqueIndexToYourModelsHashId < ActiveRecord::Migration[5.2]
      def change
        add_column :your_models, :hash_id, :string
        add_index :your_models, :hash_id, unique: true
      end
    end
    
  2. 在模型中生成Hash ID并处理碰撞
    在模型的before_create回调里生成Hash ID,当出现唯一约束冲突时(也就是极罕见的碰撞),捕获异常并重试:

    class YourModel < ApplicationRecord
      before_create :generate_hash_id
    
      private
    
      def generate_hash_id
        self.hash_id = generate_random_hash_id
      rescue ActiveRecord::RecordNotUnique
        # 碰撞概率极低,几乎不会走到这里
        retry
      end
    
      def generate_random_hash_id
        # 生成9位Base62格式的ID(数字+大小写字母),和Basecamp风格一致
        chars = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a
        (1..9).map { chars.sample }.join
        # 如果你偏好SecureRandom的实现,也可以用下面的写法
        # SecureRandom.urlsafe_base64(9).tr('-_', ('a'..'z').to_a.sample(2).join)
      end
    end
    

为什么高效?

9位Base62的Hash ID有62^9 ≈ 1.3×10¹⁶种可能,碰撞概率低到可以忽略不计。绝大多数情况下,生成Hash ID后直接插入即可,不需要任何额外查询。只有在极端巧合下才会重试一次,性能损耗微乎其微。

方案2:基于自增ID的加密Hash ID(简单易用)

如果可以接受Hash ID和自增ID存在映射关系,使用hashids gem是更简单的选择——它会把自增ID加密成短字符串,完全不需要查询或重试,因为每个自增ID对应唯一的Hash ID。

具体步骤:

  1. 安装gem
    在Gemfile里添加:

    gem 'hashids'
    

    然后执行bundle install。

  2. 在模型中配置Hash ID

    class YourModel < ApplicationRecord
      # 用一个安全的盐值(可以存在Rails credentials里)
      HASHID_SALT = Rails.application.credentials.dig(:hashid, :salt)
      # 指定Hash ID的长度,和Basecamp保持一致设为9
      HASHID_LENGTH = 9
    
      # 生成Hash ID
      def hash_id
        @hash_id ||= Hashids.new(HASHID_SALT, HASHID_LENGTH).encode(id)
      end
    
      # 通过Hash ID查找记录
      def self.find_by_hash_id(hash_id)
        decoded_id = Hashids.new(HASHID_SALT, HASHID_LENGTH).decode(hash_id).first
        find(decoded_id) if decoded_id
      end
    end
    

优缺点:

  • 优点:实现零成本,不需要数据库额外查询,也不用担心碰撞。
  • 缺点:如果有人知道你的盐值,可能可以反向解码出自增ID,从而推测出你的记录数量和增长速度。如果完全不想暴露自增ID的信息,方案1更合适。
方案3:预生成Hash ID池(高并发场景)

如果你的应用处于超高并发场景,连极罕见的重试都想避免,可以提前预生成一批Hash ID存在一个单独的“池”里,创建记录时直接从池里取未使用的ID。

具体步骤:

  1. 创建Hash ID池表
    写迁移文件:

    class CreateHashIdPools < ActiveRecord::Migration[5.2]
      def change
        create_table :hash_id_pools do |t|
          t.string :hash_id, null: false, unique: true
          t.boolean :used, default: false, null: false
    
          t.timestamps
        end
        add_index :hash_id_pools, [:used, :created_at]
      end
    end
    
  2. 预生成Hash ID
    写一个Rake任务或者后台Job,定期生成一批Hash ID插入到池里:

    namespace :hash_ids do
      desc "预生成一批Hash ID到池里"
      task generate: :environment do
        batch_size = 1000
        chars = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a
        hash_ids = batch_size.times.map { (1..9).map { chars.sample }.join }
        # 批量插入,避免重复
        HashIdPool.insert_all(hash_ids.map { |h| { hash_id: h } }, unique_by: :hash_id)
      end
    end
    
  3. 从池里取ID创建记录
    在模型中通过事务加行锁的方式,取一个未使用的ID:

    class YourModel < ApplicationRecord
      before_create :assign_hash_id_from_pool
    
      private
    
      def assign_hash_id_from_pool
        # 用行锁保证不会被其他请求抢走同一个ID
        pool_entry = HashIdPool.where(used: false).lock(true).take
        if pool_entry
          self.hash_id = pool_entry.hash_id
          pool_entry.update!(used: true)
        else
          # 如果池空了,临时生成一个(或者触发预生成任务)
          self.hash_id = generate_random_hash_id
        end
      end
    
      def generate_random_hash_id
        chars = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a
        (1..9).map { chars.sample }.join
      end
    end
    

优缺点:

  • 优点:彻底避免了创建时的查询和重试,性能最优,适合超高并发场景。
  • 缺点:需要额外维护ID池,增加了系统复杂度,还要监控池的剩余数量,避免耗尽。

总结一下,如果你想要完全独立于自增ID的随机Hash ID,方案1是最优选择——实现简单,性能损耗几乎可以忽略,碰撞概率极低。如果可以接受Hash ID和自增ID存在映射关系,方案2的hashids gem能让你零查询实现需求。至于方案3,更适合超高并发的场景,需要额外维护ID池,但能彻底避免创建时的性能开销。

内容的提问来源于stack exchange,提问作者chrickso

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 07:05:31