发号器的常用解决方案及应用
什么是发号器
发号器,通常被叫做ID生成器,是为业务元素生成唯一标示的一组方法或功能。
常见使用场景
- 新浪微博用户ID:http://weibo.com/1346818450
- QQ号:422525199
- 手机充值卡密码:510 3070 5354 0939 5056
- 淘宝订单号:352614560557223645
- Twitter文章Id:https://twitter.com/ayanamist/status/878805596283084800
- 支付宝交易流水号:2017062121001004940256742896
通过发号器生成的ID特点
- ID位数可定制
- 支持多IDC部署
- 单机或多机粗略有序
- 增长步长不固定
- 可反解
- 可制造
常用发号器的实现方式
实现发号器,有几种思路
- 数据库自增id
- UUID
- Snowflake服务
- Flicker的发号器方案
前两种比较常见,下边着重梳理后边两种,这两种方案的实现涉及到一些二进制的存储、运算和转换,用习惯了高级编程语言,二进制运算可能都还给老师了,在看下边之前可以先回顾下二进制相关知识。
Snowflake服务
Snowflake服务生成的ID由四部分二进制编码组成:
- 最高位是符号位,始终为0
- 41位的时间序列(精确到毫秒,41位的长度可以使用69年,1970年~2039年)
- 10位的机器标识(10位的长度最多支持部署1024个节点)
- 12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
分别获取每部分的值并转为二进制:
- 符号位:0 -> 0
- 时间:1470126882000 -> 10101011001001010011000111111110011010000
- 机器码:102 -> 1100110
- 随机码:2289 -> 100011110001
最终获得ID: 770769882710436081
Flicker的发号器方案
首先创建一个数据库表:
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;
在应用中,把如下sql放到一个事务里提交,就能拿到不断增长且不重复的ID了:
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
如上,只是在单台数据库上生成ID,从高可用角度考虑,接下来就要解决单点故障问题: Flicker启用了两台数据库服务器来生成ID,通过区分auto_increment的起始值和步长来生成奇偶数的ID。 在应用中,轮训两台数据库服务器就可以了。
发号器的PHP实现
方案一
<?php
/**
* Long ID Generator
*
* 生成长id,比如订单id、帖子id
*
* @author 小黑
*
* 特点:
* 参照Snowflake service from Twitter。
* id粗略有序。
* 可反解。
* 时间相关。
* 可制造。
* 不能保证绝对唯一,但重复的几率微乎其微,在可容忍范围内。
* 多机器部署灵活、方便。
*
* 四部分组成:
* 最高位是符号位,始终为0
* 41位的时间序列(精确到毫秒,41位的长度可以使用69年)
* 10位的机器标识(包括数据中心5位 + 机器编号5位)(10位的长度最多支持部署1024个节点)
* 12位的毫秒内随机数(理论上12位支持每个节点每毫秒产生4096个ID序号)
*
* 缺点:
* 毫秒内产生相同随机数会造成重复,虽然几率很小!因为id一般作为主键,写DB时如果有重复会写入失败,
* 建议因为id重复写表失败的,重新生成新id重试写表一次(为了防止死循环,建议只重试一次,重试失败即抛错)。
*/
class Api_Longid
{
// ID开始时间(毫秒) 2015-06-15 00:00:00
const INIT_TIME = 1434297600000;
//const time_bits = 41;
const datacenter_bits = 5;
const machine_bits = 5;
const sequence_bits = 12;
/**
* 生成ID
* @author 小黑
*
* @param int $machine 机器编号
* @param int $datacenter IDC编号
* @return int
*/
public static function generate($machine, $datacenter = 0)
{
if ($machine - intval($machine) != 0 || $datacenter - intval($datacenter) != 0)
{
return false;
}
$datacenter_max = -1 ^ (-1 << self::datacenter_bits);
$machine_max = -1 ^ (-1 << self::machine_bits);
if ($machine < 0 || $machine > $machine_max
|| $datacenter < 0 || $datacenter > $datacenter_max)
{
return false;
}
$time_shift = self::sequence_bits + self::machine_bits + self::datacenter_bits;
$datacenter_shift = self::sequence_bits + self::machine_bits;
$machine_shift = self::sequence_bits;
$sequence_max = -1 ^ (-1 << self::sequence_bits);
$time = floor(microtime(true) * 1000);
$long_id = (($time - self::INIT_TIME) << $time_shift)
| ($datacenter << $datacenter_shift)
| ($machine << $machine_shift)
| mt_rand(0, $sequence_max);
Api_Log::id_generate_log('long', $long_id, $time);
return $long_id;
}
/**
* 根据ID,反解获得时间
* @author 小黑
*
* @param int $id
* @return int
*/
public static function get_time($id)
{
return ($id >> self::sequence_bits + self::machine_bits + self::datacenter_bits) + self::INIT_TIME;
}
/**
* 根据ID,反解获得IDC编号
* @author 小黑
*
* @param int $id
* @return int
*/
public static function get_datacenter($id)
{
$shift = self::sequence_bits + self::machine_bits + self::datacenter_bits;
return (($id >> $shift << $shift ^ $id) >> self::sequence_bits + self::machine_bits);
}
/**
* 根据ID,反解获得机器编号
* @author 小黑
*
* @param int $id
* @return int
*/
public static function get_machine($id)
{
$shift = self::sequence_bits + self::machine_bits;
return (($id >> $shift << $shift ^ $id) >> self::sequence_bits);
}
}
方案二
<?php
/**
* User ID Generator
*
* 生成用户id
*
* @author 小黑
*
* 特点:
* id存储长度不超过32bit,以无符号int类型存储。
* id表现长度为8~10位。
* 支持扩展到8台机器。
* id自增,步长随机。
* 单机有序。
* 可反解。
* 时间不相关。
* 不可制造。
* 绝对唯一。
* 多机器部署,需要多个数据库表支持,一机一表,一一对应。
*
* 三部分组成:
* 1. msyql自增id:占19~27bit,单机支持(133,890,046 = 134217727 - 327681)个id
* 2. 随机数:占2bit,相邻两个id的自增步长随机
* 3. 机器编号:占3bit,支持扩展到8台机器
*
* 单机取值范围:
* 最小值:1010000000000000001-00-001 (10485793)
* 最大值:111111111111111111111111111-00-001 (4294967265)
* 说明:1010000000000000001为最小值,是为了保证10进制id最短8位
*
* id自增表:
* CREATE TABLE generate_uid_x (
* id int(10) unsigned NOT NULL auto_increment,
* stub char(1) NOT NULL default '',
* PRIMARY KEY (id),
* UNIQUE KEY stub (stub)
* ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
*
* SQL:
* REPLACE INTO generate_uid_x (stub) VALUES ('a');
* SELECT LAST_INSERT_ID();
*
* 扩展性:
* 假如id超过上限,只需修改用户id存储字段的数据类型为long型。
*/
class Api_Uid
{
// ID基数
const UID_BASE_NUM = 327680;
//const mysqlid_bits = 27;
const random_bits = 2;
const machine_bits = 3;
/**
* 生成ID
* @author 小黑
*
* @param int $type 用户类型 (1.普通 2.会员)
* @param int $machine 机器编号 (0-7)
* @return int
*/
public static function generate($type, $machine = 0)
{
if ($type - intval($type) != 0 || $machine - intval($machine) != 0)
{
return false;
}
$machine_max = -1 ^ (-1 << self::machine_bits);
if ($machine < 0 || $machine > $machine_max)
{
return false;
}
if ($type == 1)
{
$table = 'generate_uid_' . $machine;
$log_type = 'user';
}
elseif ($type == 2)
{
$table = 'generate_did_' . $machine;
$log_type = 'doctor';
}
else
{
return false;
}
$sql = 'replace into ' . $table . " (stub) values ('a')";
$query = DB::query(Database::INSERT, $sql);
list($insert_id, $affect) = $query -> execute();
if (! is_numeric($insert_id) || $insert_id < 1)
{
return false;
}
$mysqlid_shift = self::random_bits + self::machine_bits;
$random_shift = self::machine_bits;
$random_max = -1 ^ (-1 << self::random_bits);
$uid = (self::UID_BASE_NUM + $insert_id) << $mysqlid_shift
| mt_rand(0, $random_max) << $random_shift
| $machine;
Api_Log::id_generate_log($log_type, $uid, $insert_id);
return $uid;
}
/**
* 根据ID,反解获得数据库自增ID
* @author 小黑
*
* @param int $uid
* @return int
*/
public static function get_mysqlid($uid)
{
return ($uid >> self::random_bits + self::machine_bits) - self::UID_BASE_NUM;
}
/**
* 根据ID,反解获得机器编号
* @author 小黑
*
* @param int $uid
* @return int
*/
public static function get_machine($uid)
{
return $uid >> self::machine_bits << self::machine_bits ^ $uid;
}
}