一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。
目前几种可行的主键生成策略有:
1. UUID:使用UUID作主键是最简单的方案,但是缺点也是非常明显的。由于UUID非常的长,除占用大量存储空间外,最主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。(UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。)
2. 结合数据库维护一个Sequence表:此方案的思路也很简单,在数据库中建立一个Sequence表,表的结构类似于:
CREATE
TABLE
`
SEQUENCE
` (
`tablename`
varchar
(30)
NOT
NULL
,
`nextid`
bigint
(20)
NOT
NULL
,
PRIMARY
KEY
(`tablename`)
) ENGINE=InnoDB
|
每当需要为某个表的新纪录生成ID时就从Sequence表中取出对应表的nextid,并将nextid的值加1后更新到数据库中以备下次使用。此方案也较简单,但缺点同样明显:由于所有插入任何都需要访问该表,该表很容易成为系统性能瓶颈,同时它也存在单点问题,一旦该表数据库失效,整个应用程序将无法工作。有人提出使用Master-Slave进行主从同步,但这也只能解决单点问题,并不能解决读写比为1:1的访问压力问题。
3. flickr提供了一个扩展的更好的方案: 他们建了一个专门用作生成 uid 的表,例如取名叫 uid_sequence,并拆成若干的子表(假设两个),自增步长设置为2(机器数目),这两张表可以放在不同的物理机器上。其中一个表负责生成奇数uid,另一个负责生成偶数uid
uid_sequence 表的设计
#server1:
CREATE
TABLE
`uid_sequence` (
`id`
bigint
(20) unsigned
NOT
NULL
auto_increment,
`stub`
char
(1)
NOT
NULL
default
''
,
PRIMARY
KEY
(`id`),
UNIQUE
KEY
`stub` (`stub`)
) ENGINE=MyISAM AUTO_INCREMENT=1;
|
#server2:
CREATE
TABLE
`uid_sequence` (
`id`
bigint
(20) unsigned
NOT
NULL
auto_increment,
`stub`
char
(1)
NOT
NULL
default
''
,
PRIMARY
KEY
(`id`),
UNIQUE
KEY
`stub` (`stub`)
) ENGINE=MyISAM AUTO_INCREMENT=2;
|
在每个数据库配置文件中添加下边的配置,设置自动增长的步长为2
#Server1: my.conf
auto-increment-increment = 2
|
#Server2: my.conf
auto-increment-increment = 2
|
SELECT * from uid_sequence 输出:
+-------------------+------+
| id | stub |
+-------------------+------+
| 72157623227190423 | a |
如果我需要一个全局的唯一的64位uid,则执行:
REPLACE
INTO
uid_sequence (stub)
VALUES
(
'a'
);
SELECT
LAST_INSERT_ID();
|
用 REPLACE INTO 代替 INSERT INTO 的好处是避免表行数太大,还要另外定期清理。
stub 字段要设为唯一索引,这个 sequence 表只有一条纪录,但也可以同时为多张表生成全局主键,例如 user_ship_id。除非你需要表的主键是连续的,那么就另建一个 user_ship_id_sequence 表。
经过实际对比测试,使用 MyISAM 比 Innodb 有更高的性能。
这里flickr使用两台数据库作为自增序列生成,通过这两台机器做主备和负载均衡。因为flickr的数据库ID生成服务器是专用服务器,服务器上只有一个数据库,数据库中表都是用于生成Sequence的,所以配置了全局范围的auto-increment-offset和auto-increment-increment,这会影响到数据库中的每一个表。
MySQL 中 last_insert_id() 的并发问题
因为是两条SQL语句,所以这两条语句之间会不会有并发问题?
答案是不会,因为 last_insert_id() 是 Connection 级别的,是单个连接客户端里执行的insert语句最近一条,客户端之间是不会影响,没必要锁定和事务处理。
4. Redis生成ID
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用于生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis A,B,C,D,E。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
|
使用Redis集群也可以防止单点故障的问题。使用Redis来生成每天从0开始的流水号比较适合。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。
优点:
1)不依赖于数据库,灵活方便,且性能优于数据库。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
2)需要编码和配置的工作量比较大。