刚开始用 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 的方案。