Gem 慎用 mongoid_auto_increment_id 这个 Gem

cuterxy · 2015年03月10日 · 最后由 cuterxy 回复于 2015年03月10日 · 3027 次阅读

刚开始用 MongoDB 做项目的时候,发觉思维还是维持在以前用 MySQL 的惯性,觉得每条记录需要一个自增长 id,于是找到了 mongoid_auto_increment_id 这个 Gem。而这个 Gem 也同时被本站使用,感觉应该还是靠谱的,所以用之前就没有仔细的考虑。后来随着对 MongoDB 了解的加深和对现有项目的反思,发觉 MongoDB 原生的 ObjectId 方式才应该是常态的 id。有关 ObjectId 的研究可以看 这里

首先,自增 id 在分布式环境下是需要依赖一个中心节点来去生成的,而这个过程对于增长速度很快的对象来说,代价是很高的,而且还存在着单点故障的危险。如果项目的对象采用自增长 id,那么相当于埋了一个定时炸弹,日后如果数据量指数级上升,切换到分布式数据库之后,依赖一个中心节点的自增 id 将会不能再使用,否则整体性能就会上不去。

其次,绝大部分的项目,其实真正用到对象需要自增长的 id 的情况是非常少的,如果需要用到,一般也并非一个非常关键的特性。比如说,记录某个用户是网站的第几个注册用户。这类特性,其实用一个单独的字段保存就可以了,哪怕失效了,也可以重新生成,不像是主键那样牵一发动全身。

再次,一旦使用了 mongoid_auto_increment_id 这个 Gem,所有的 model 就缺省使用了自增长 id。而 MongoDB 在 3.0 版本之前所有写操作会有一个全局锁,也就是说,每生成一个自增长 id 都需要占用全局锁资源一次,会大大影响系统的吞吐量。MongoDB 在 3.0 版本之后将写操作的全局锁优化成了表级锁(针对不同的 Collection 加锁), 但是问题是 mongoid_auto_increment_id 在实现的时候将所有 Collection 的自增长 id 都记录在同一个 Collection 下了,这就导致每增加一条记录,还是存在着一个人为的全局锁。

见 mongoid_auto_increment_id 的代码:

class Identity
  # Generate auto increment id
  def self.generate_id(document)
    database_name = Mongoid::Sessions.default.send(:current_database).name
    o = nil
    Mongoid::Sessions.default.cluster.with_primary do |node|
        o = node.command(database_name,
            {"findAndModify" => "mongoid.auto_increment_ids",  # 此处将整个db的所有collection的自增id都依赖同一个collection生成,为了充分发挥 MongoDB 3.0 的性能(库级所改进为表级锁),我觉得应该把 collection_name 加上去,即应该把 "mongoid.auto_increment_ids" 改为 "mongoid.auto_increment_ids.#{ document.collection_name }"
              :query => { :_id => document.collection_name },
              :update => { "$inc" => { :c => 1 }},
              :upsert => true,
              :new => true }, {})
    end
    o["value"]["c"].to_i
  end
end

因此,除非你确认你的项目数据量不会很大,否则还是尽量不要使用自增长 id。另外,为了充分发挥 MongoDB 3.0 的性能,mongoid_auto_increment_id 需要升级一下实现的代码了,应该将不同的 Collection 的自增长 id 保存到不同的 Collection 里去(@huacnlee)。

最后,求把自增长 id 换成 MongoDB 原生 ObjectId 的方案。

mongoid_auto_increment_id 是根据 MongoDB 官方的这篇文章来实现的《Create an Auto-Incrementing Sequence Field》,起初的设计目的是为了要一个简洁的主键,便于应用到 URL 里面,使 URL 更加简洁、优雅

目的:

  1. 简化 ID,以便在 URL 里面使用
  2. 便于用来排序
  3. 便于维护(比如在后台查某些数据,纯数字要比 ObjectId 简单许多)
  4. 利于后期迁移到其他类型的库

其实类似的做法,在我们公司的 MySQL 也有,由于数据量大,我们需要将大量数据划分在多个库,多个表里面,由此带来的问题是 MySQL 的自增编号就无法使用了。

类似的自行维护的方案就不得不用起来


你需要多大的吞度量?


其实有很简单方法来减少自增表的 Write 动作,提高吞吐量的,就是让 generate_id 改成支持批量生成,一次生成 100 - 1000 个,根据实际需求调整,放在内存里面,后面的调用都是内存里面 Array 的 shift 动作,用完了再调用数据库,重新占用一批

也可以配合 Rails.cache 用来存储生产出来的 ID 以便多进程、多机器之间共享

已经根据你的需求实现了, 请用 0.7.0 版本

gem 'mongoid_auto_increment_id', '0.7.0'

然后配置

# cache_store 建议放在 MemoryStore 里面,避免其他缓存区域的 Write Lock
# 但 MemoryStore 带来的问题是进程之间无法共享,最终会导致 ID 不再是有序的了,无法用于排序
# 另外, 重启进程 Cache 丢了没关系,实现是先在 MongoDB 里面把一定的长度占用了,再缓存的,也就是丢弃了一批编号而已
Mongoid::AutoIncrementId.cache_store = ActiveSupport::Cache::MemoryStore.new
Mongoid::AutoIncrementId.seq_cache_size = 500

#1 楼 @huacnlee

你列出来的目的,其实完全可以用另外一个字段(比如说 auto_id)来实现。由于并非每个 Collection 都需要自增 id,所以将自增 id 作为一个可选字段来添加到 Model 的定义,而不是缺省所有 Model 都采用自增 id 作为主键,是一个更加有灵活性和科学的做法。

其实这些需求并非是关键需求。非关键需求的特性,就是缺少的时候不会影响整个系统的运行,而过后又可以通过后台程序再次生成补上。而主键是一个关键字段,是表之间关系的连接点,如果生成错误或者不能生成,将很可能会造成整个系统瘫痪。

因此我依然认为通常情况下缺省应该采用 ObjectId 作为主键。

#2 楼 @huacnlee 这个做法的确能够提高吞吐量,但是系统依然是依赖一个中心节点来生成递增的 id,所以单点依赖的风险依然存在。而且,如果有多个异构的系统在使用同一个数据库,那么所有其他类型的系统都必须遵循同样的逻辑来生成主键,这样也会增加系统的耦合度和复杂度。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号