视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Mysql全局ID生成方法_MySQL
2020-11-09 20:16:15 责编:小采
文档


生产系统随着业务增长总会经历一个业务量由小变大的过程,可扩展性是考量数据库系统高可用性的一个重要指标;在单表/数据库数据量过大,更新量不断飙涨时,MySQL DBA往往会对业务系统提出sharding的方案。既然要sharding,那么不可避免的要讨论到sharding key问题,在有些业务系统中,必须保证sharding key全局唯一,比如存放商品的数据库等,那么如何生成全局唯一的ID呢,下文将从DBA的角度介绍几种常见的方案。

1、使用CAS思想

什么是CAS协议

Memcached于1.2.4版本新增CAS(Check and Set)协议类同于Java并发的CAS(Compare and Swap)原子操作,处理同一item被多个线程更改过程的并发问题

CAS的基本原理

基本原理非常简单,一言以蔽之,就是“版本号”,每个存储的数据对象,都有一个版本号。

我们可以从下面的例子来理解:

不采用CAS,则有如下的情景:

•第一步,A取出数据对象X;
•第二步,B取出数据对象X;
•第三步,B修改数据对象X,并将其放入缓存;
•第四步,A修改数据对象X,并将其放入缓存。

结论:第四步中会产生数据写入冲突。

采用CAS协议,则是如下的情景。

•第一步,A取出数据对象X,并获取到CAS-ID1;

•第二步,B取出数据对象X,并获取到CAS-ID2;

•第三步,B修改数据对象X,在写入缓存前,检查CAS-ID与缓存空间中该数据的CAS-ID是否一致。结果是“一致”,就将修改后的带有CAS-ID2的X写入到缓存。

•第四步,A修改数据对象Y,在写入缓存前,检查CAS-ID与缓存空间中该数据的CAS-ID是否一致。结果是“不一致”,则拒绝写入,返回存储失败。

这样CAS协议就用了“版本号”的思想,解决了冲突问题。(乐观锁概念)

其实这里并不是严格的CAS,而是使用了比较交换原子操作的思想。

生成思路如下:每次生成全局id时,先从sequence表中获取当前的全局最大id。然后在获取的全局id上做加1操作,加1后的值更新到数据库,如加1后的值为203,表名是users,数据表结构如下:

CREATE TABLE `SEQUENCE` (
 `name` varchar(30) NOT NULL COMMENT '分表的表名',
 `gid` bigint(20) NOT NULL COMMENT '最大全局id',
 PRIMARY KEY (`name`)
) ENGINE=innodb 

sql语句

update sequence set gid = 203 where name = 'users' and gid < 203; 

sql语句的 and gid < 203 是为了保证并发环境下gid的值只增不减。

如果update语句的影响记录条数为0说明,已经有其他进程提前生成了203这个值,并写入了数据库。需要重复以上步骤从新生成。

代码实现如下:

//$name 表名
function next_id_db($name){
 //获取数据库全局sequence对象
 $seq_dao = Wk_Sequence_Dao_Sequence::getInstance();
 $threshold = 100; //最大尝试次数
 for($i = 0; $i < $threshold; $i++){
 $last_id = $seq_dao->get_seq_id($name);//从数据库获取全局id
 $id = $last_id +1;
 $ret = $seq_dao->set_seq_id($name, $id);
 if($ret){
 return $id;
 break;
 }
 }
 return false;
}

2、使用全局锁

在进行并发编程时,一般都会使用锁机制。其实,全局id的生成也是解决并发问题。

生成思路如下:

在使用redis的setnx方法和memcace的add方法时,如果指定的key已经存在,则返回false。利用这个特性,实现全局锁

每次生成全局id前,先检测指定的key是否存在,如果不存在则使用redis的incr方法或者memcache的increment进行加1操作。这两个方法的返回值是加1后的值,如果存在,则程序进入循环等待状态。循环过程中不断检测key是否还存在,如果key不存在就执行上面的操作。

代码如下:

//使用redis实现
//$name 为 逻辑表名
function next_id_redis($name){
 $redis = Wk_Redis_Util::getRedis();//获取redis对象
 $seq_dao = Wk_Sequence_Dao_Sequence::getInstance();//获取存储全局id数据表对象
 if(!is_object($redis)){
 throw new Exception("fail to create redis object");
 }
 $max_times = 10; //最大执行次数 避免redis不可用的时候 进入死循环
 while(1){
 $i++;
 //检测key是否存在,相当于检测锁是否存在
 $ret = $redis->setnx("sequence_{$name}_flag",time());
 if($ret){
 break;
 }
 if($i > $max_times){
 break;
 }
 $time = $redis->get("sequence_{$name}_flag");
 if(is_numeric($time) && time() - $time > 1){//如果循环等待时间大于1秒,则不再等待。
 break;
 }
 }
 $id = $redis->incr("sequence_{$name}");
 //如果操作失败,则从sequence表中获取全局id并加载到redis
 if (intval($id) === 1 or $id === false) {
 $last_id = $seq_dao->get_seq_id($name);//从数据库获取全局id
 if(!is_numeric($last_id)){
 throw new Exception("fail to get id from db");
 }
 $ret = $redis->set("sequence_{$name}",$last_id);
 if($ret == false){
 throw new Exception("fail to set redis key [ sequence_{$name} ]");
 }
 $id = $redis->incr("sequence_{$name}");
 if(!is_numeric($id)){
 throw new Exception("fail to incr redis key [ sequence_{$name} ]");
 }
 }
 $seq_dao->set_seq_id($name, $id);//把生成的全局id写入数据表sequence
 $redis->delete("sequence_{$name}_flag");//删除key,相当于释放锁
 $db = null;
 return $id;
} 

3、redis和db结合

使用redis直接操作内存,可能性能会好些。但是如果redis死掉后,如何处理呢?把以上两种方案结合,提供更好的稳定性。
代码如下:

function next_id($name){
 try{
 return $this->next_id_redis($name);
 }
 catch(Exception $e){
 return $this->next_id_db($name);
 }
} 

4、Flicker的解决方案

因为mysql本身支持auto_increment操作,很自然地,我们会想到借助这个特性来实现这个功能。Flicker在解决全局ID生成方案里就采用了MySQL自增长ID的机制(auto_increment + replace into + MyISAM)。一个生成位ID方案具体就是这样的:
先创建单独的数据库(eg:ticket),然后创建一个表:

CREATE TABLE Tickets (
 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 Tickets,查询结果就是这样的:

+-------------------+------+
| id | stub |
+-------------------+------+
| 72157623227190423 | a |
+-------------------+------+

在我们的应用端需要做下面这两个操作,在一个事务会话里提交:

REPLACE INTO Tickets (stub) VALUES ('a');
SELECT LAST_INSERT_ID(); 

这样我们就能拿到不断增长且不重复的ID了。
到上面为止,我们只是在单台数据库上生成ID,从高可用角度考虑,
接下来就要解决单点故障问题:Flicker启用了两台数据库服务器来生成ID,
通过区分auto_increment的起始值和步长来生成奇偶数的ID。

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2 

最后,在客户端只需要通过轮询方式取ID就可以了。

•优点:充分借助数据库的自增ID机制,提供高可靠性,生成的ID有序。

•缺点:占用两个的MySQL实例,有些浪费资源,成本较高。

以上内容是小编给大家分享的Mysql全局ID生成方法,希望大家喜欢。

下载本文
显示全文
专题