MySQL分库分表早已经不是什么新鲜话题了。甚至已经成了说到MySQL就会说到的话题。在一张表中,MySQL提供了原生的自增主键实现。但是在这样的分布式系统中,怎么保证数据在多张表上的ID是唯一的呢?
Flickr提出了一个方案,将文章简单翻译一下给大家,方便大家阅读。嫌弃我翻译水平太烂的,请移步原文:http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/
——————————————— 我就是分割线 分割线就是我 —————————————–
这篇文章是“在Flickr里使用,广泛使用,扩展MySQL”系列文章的第一弹。
Ticket服务本身其实没什么好说的,但他是Flickr系统里一个重要的组成部分。是我们接下来要谈到的话题的核心,就像sharding和主主复制一样。Ticket服务器给我们提供了一个生成全局(Flickr范围内)唯一的整数的服务,这个整数用来作为我们分布式系统中的主键使用。
为什么(需要这样一种机制)?
Sharding(或者叫data partition)(译者注:数据分片)是使我们的数据存储能力能够水平扩展的方案。我们不是将我们所有的数据都存在一个物理的数据库上,相反,我们有很多数据库,每个数据库存储一部分数据,然后我们将压力负载均衡到这些数据库上去。但在某些情况下,我们需要在不同的数据库之间进行数据合并,所以我们需要主键是全局唯一的。另外,我们的数据库sharding是基于主主复制的。这意味着我们需要保证,在一个分片内,主键也是唯一的,这样才能避免主键冲突(重复)。我们当然希望能够和其他人一样,使用MySQL的自增字段来生成主键。但是,这种方案在跨多个物理或逻辑数据库时,没有办法保证唯一性。
(使用)GUIDs(行不行)?
看来,我们就是需要一个全局唯一的id,那为什么不用GUID(译者注:GUID=Globally Unique Identifier 全局唯一标示)呢?最大的原因就是GUID太大了,而且他在MySQL中索引效果太差了。为了保证我们的MySQL足够快,我们将所有要查询的条件都索引起来,然后只在索引键上做查询。所以,索引的大小就成了一个关键性的指标。如果你不能将你的索引全部都放到内存里,那你的数据库就不可能足够快。而且,Ticket服务给我们的ID是有序的。这个特点给我们做业务报表和做调试的时候带来了很大的方便性,而且我们还可以再上面做一些缓存的优化方案。
(使用)一致性哈希(行不行)?
像 Amazon’s Dynamo和其他一些系统,在数据存储上面提供了一致性哈希环来解决GUID和分片的问题。这种方案更适合于写廉价的应用场景(比如: LSMTs),而MySQL是一种针对随机读取专门做过优化的系统。
集中式自增
我们不能让MySQL的自增跨数据库工作,那如果我们只用一个数据库呢?假设在有人上传图片的时候,我们都往这个数据库库中插入一条数据,然后用这个表的自增主键值作为我们所有数据库的唯一主键。
在目前每秒60+张图片上传的情况下,这张表很快就会变得奇大无比。就算在中央数据库中我们只存储图片的id,而不存储其他任何信息,这张表也很快就会大到无法管理的地步。更何况图片的评论,收藏,分组,标签等等等等也都需要唯一ID。
REPLACE INTO
大约小十年前,MySQL在ANSI SQL标准上做了一个非标准的“REPLACE INTO”扩展。虽然后来有“INSERT ON DUPLICATE KEY UPDATE”更好的解决了这类问题,但REPLACE INTO现在仍然能用。
REPLACE和INSERT很像。除了在新插入的行中主键或唯一所以中的值在表中已经存在的情况下,会先删除老的那行数据,再插入新的这行数据。
这样的话,我们就可以通过更新(replace)数据库中的某一行来获取一个新的自增主键ID了。
来个总结
一个Flickr Ticket服务器是一个只包含一个数据库独立的数据库服务器。然后这个数据库里包含了一些类似Ticket32和Ticket64的表(分别用于提供32位整型主键和64位长整型主键)。
Tickets64 的Sckeme大概像这样:
1
2
3
4
5
6
|
CREATE
TABLE
`
Tickets64
`
(
`
id
`
bigint
(
20
)
unsigned
NOT
NULL
auto_increment
,
`
stub
`
char
(
1
)
NOT
NULL
default
''
,
PRIMARY
KEY
(
`
id
`
)
,
UNIQUE
KEY
`
stub
`
(
`
stub
`
)
)
ENGINE
=
MyISAM
|
SELECT * from Tickets64 返回的一行数据大概像这样:
1
2
3
4
5
|
+
--
--
--
--
--
--
--
--
--
-
+
--
--
--
+
|
id
|
stub
|
+
--
--
--
--
--
--
--
--
--
-
+
--
--
--
+
|
72157623227190423
|
a
|
+
--
--
--
--
--
--
--
--
--
-
+
--
--
--
+
|
当我们需要一个新的64位的主键的时候,我们可以通过执行下面的SQL得到:
1
2
|
REPLACE
INTO
Tickets64
(
stub
)
VALUES
(
'a'
)
;
SELECT
LAST_INSERT_ID
(
)
;
|
单点问题
你绝对不愿意看到你的ID服务器因为单点问题挂掉。我们实现“高可用”的方法是跑两个Ticket服务。但在两个机器之间进行写入/更新的复制是会有问题的,由于必要的锁的存在,会严重降低整个网站的性能。我们的做法是将ID生成的职责均分到两个服务上,一个生成奇数,一个生成偶数。设置如下:
1
2
3
4
5
6
7
|
TicketServer1
:
auto
-
increment
-
increment
=
2
auto
-
increment
-
offset
=
1
TicketServer2
:
auto
-
increment
-
increment
=
2
auto
-
increment
-
offset
=
2
|
然后我们使用轮询的方式去轮流访问这两个服务器,来达到负载均衡的目的和应对停机的情况。如果有一边不能保持同步了(停机了),那最多就是可能会有连续的几十万个奇数(或者偶数)的ID。但这不会有任何副作用。
更多的队列
在Ticket服务器中,除了Ticket32和Ticket64这样的两张表,我们其实还有其他更多的表。我们的图片,账号,离线任务,组等等都有自己独立的ID队列。离线任务有它自己独立的ID队列是因为它太多了,然后我们又不希望有不必要的因素导致队列增长(译者注:前面提到,ID的大小一定程度上反应了业务的数据量)。分组和账号有他们自己的ID队列是因为我们很少拿他们来做比较。图片有自己的ID队列是因为我们要保证切换之后要和之前单表的自增ID保持同步,因为之前的自增id我们可以很方便的知道我们总共上传了多少张图片。
就是这样
虽然,这个方案看起来不是特别优雅。但令人震惊的是从2006年1月13号我们上线到现在,在生产环境跑的都非常好。这个设计已经成为我们Flickr工程师的一个设计理念——“在最坏的情况下也能工作”的一个典型代表了。