Mysql全局ID生成方法
生产系统随着业务增长总会经历一个业务量由小变大的过程,可扩展性是考量数据库系统高可用性的一个重要指标;在单表/数据库数据量过大,更新量不断飙涨时,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)。一个生成64位id方案具体就是这样的:
先创建单独的数据库(eg:ticket),然后创建一个表:
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,查询结果就是这样的:
+-------------------+------+
| id | stub |
+-------------------+------+
| 72157623227190423 | a |
+-------------------+------+
在我们的应用端需要做下面这两个操作,在一个事务会话里提交:
replace into tickets64 (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生成方法,希望大家喜欢。