生产系统随着业务增长总会经历一个业务量由小变大的过程,可扩展性是考量数据库系统高可用性的一个重要指标;在单表/数据库数据量过大,更新量不断飙涨时,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 &#61; 203 where name &#61; &#39;users&#39; and gid <203;
sql语句的 and gid <203 是为了保证并发环境下gid的值只增不减。
如果update语句的影响记录条数为0说明&#xff0c;已经有其他进程提前生成了203这个值&#xff0c;并写入了数据库。需要重复以上步骤从新生成。
代码实现如下&#xff1a;
//$name 表名
function next_id_db($name){
//获取数据库全局sequence对象
$seq_dao &#61; Wk_Sequence_Dao_Sequence::getInstance();
$threshold &#61; 100; //最大尝试次数
for($i &#61; 0; $i <$threshold; $i&#43;&#43;){
$last_id &#61; $seq_dao->get_seq_id($name);//从数据库获取全局id
$id &#61; $last_id &#43;1;
$ret &#61; $seq_dao->set_seq_id($name, $id);
if($ret){
return $id;
break;
}
}
return false;
}
2、使用全局锁
在进行并发编程时&#xff0c;一般都会使用锁机制。其实&#xff0c;全局id的生成也是解决并发问题。
生成思路如下&#xff1a;
在使用redis的setnx方法和memcace的add方法时&#xff0c;如果指定的key已经存在&#xff0c;则返回false。利用这个特性&#xff0c;实现全局锁
每次生成全局id前&#xff0c;先检测指定的key是否存在&#xff0c;如果不存在则使用redis的incr方法或者memcache的increment进行加1操作。这两个方法的返回值是加1后的值&#xff0c;如果存在&#xff0c;则程序进入循环等待状态。循环过程中不断检测key是否还存在&#xff0c;如果key不存在就执行上面的操作。
代码如下&#xff1a;
//使用redis实现
//$name 为 逻辑表名
function next_id_redis($name){
$redis &#61; Wk_Redis_Util::getRedis();//获取redis对象
$seq_dao &#61; Wk_Sequence_Dao_Sequence::getInstance();//获取存储全局id数据表对象
if(!is_object($redis)){
throw new Exception("fail to create redis object");
}
$max_times &#61; 10; //最大执行次数 避免redis不可用的时候 进入死循环
while(1){
$i&#43;&#43;;
//检测key是否存在&#xff0c;相当于检测锁是否存在
$ret &#61; $redis->setnx("sequence_{$name}_flag",time());
if($ret){
break;
}
if($i > $max_times){
break;
}
$time &#61; $redis->get("sequence_{$name}_flag");
if(is_numeric($time) && time() - $time > 1){//如果循环等待时间大于1秒&#xff0c;则不再等待。
break;
}
}
$id &#61; $redis->incr("sequence_{$name}");
//如果操作失败&#xff0c;则从sequence表中获取全局id并加载到redis
if (intval($id) &#61;&#61;&#61; 1 or $id &#61;&#61;&#61; false) {
$last_id &#61; $seq_dao->get_seq_id($name);//从数据库获取全局id
if(!is_numeric($last_id)){
throw new Exception("fail to get id from db");
}
$ret &#61; $redis->set("sequence_{$name}",$last_id);
if($ret &#61;&#61; false){
throw new Exception("fail to set redis key [ sequence_{$name} ]");
}
$id &#61; $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&#xff0c;相当于释放锁
$db &#61; null;
return $id;
}
3、redis和db结合
使用redis直接操作内存&#xff0c;可能性能会好些。但是如果redis死掉后&#xff0c;如何处理呢&#xff1f;把以上两种方案结合&#xff0c;提供更好的稳定性。
代码如下&#xff1a;
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操作&#xff0c;很自然地&#xff0c;我们会想到借助这个特性来实现这个功能。Flicker在解决全局ID生成方案里就采用了MySQL自增长ID的机制(auto_increment &#43; replace into &#43; MyISAM)。一个生成64位ID方案具体就是这样的&#xff1a;
先创建单独的数据库(eg:ticket)&#xff0c;然后创建一个表&#xff1a;
CREATE TABLE Tickets64 (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(1) NOT NULL default &#39;&#39;,
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE&#61;MyISAM
当我们插入记录后&#xff0c;执行SELECT * from Tickets64&#xff0c;查询结果就是这样的&#xff1a;
&#43;-------------------&#43;------&#43;
| id | stub |
&#43;-------------------&#43;------&#43;
| 72157623227190423 | a |
&#43;-------------------&#43;------&#43;
在我们的应用端需要做下面这两个操作&#xff0c;在一个事务会话里提交&#xff1a;
REPLACE INTO Tickets64 (stub) VALUES (&#39;a&#39;);
SELECT LAST_INSERT_ID();
这样我们就能拿到不断增长且不重复的ID了。
到上面为止&#xff0c;我们只是在单台数据库上生成ID&#xff0c;从高可用角度考虑&#xff0c;
接下来就要解决单点故障问题&#xff1a;Flicker启用了两台数据库服务器来生成ID&#xff0c;
通过区分auto_increment的起始值和步长来生成奇偶数的ID。
TicketServer1:
auto-increment-increment &#61; 2
auto-increment-offset &#61; 1
TicketServer2:
auto-increment-increment &#61; 2
auto-increment-offset &#61; 2
最后&#xff0c;在客户端只需要通过轮询方式取ID就可以了。
•优点&#xff1a;充分借助数据库的自增ID机制&#xff0c;提供高可靠性&#xff0c;生成的ID有序。
•缺点&#xff1a;占用两个独立的MySQL实例&#xff0c;有些浪费资源&#xff0c;成本较高。
以上内容是小编给大家分享的Mysql全局ID生成方法&#xff0c;希望大家喜欢。
本文标题: Mysql全局ID生成方法
本文地址: http://www.cppcns.com/shujuku/mysql/137064.html