mysql全局唯一ID生成方案(一)

时间:2022-09-23 11:27:53

一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的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

mysql全局唯一ID生成方案(一)


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)需要编码和配置的工作量比较大。