php 生成自增id,一个PHP实现的ID生成器

通常来说,不管使用什么数据库,表里都有一个名为 id 的主键,既然是主键,那么必然要满足唯一性,对于 MySQL 用户来说,它多半是一个 auto_increment 自增字段,也有一些别的用户喜欢使用 UUID 做主键,不过对 MySQL(特别是 InnoDB)来说,UUID 通常不是一个好选择,因为聚簇索引要求物理数据按照主键排序,而 UUID 本身是无序的,所以会带来很多不必要的 IO 消耗。于是乎我们得到一个结论:ID 最好是顺序的唯一值。

如此说来,就用 MySQL 的 auto_increment 自增字段不就好了?问题是这样无法满足高可用性,虽然可以通过多台服务器设置不同的 auto_increment 步长来提升可用性,但数据库本身始终就是那块最短的木板。至于解决方案,网上已经有很多类似的讨论:

最流行的解决方案,当然是 twitter 的 snowflake,其大致含义是说:为了避免单点故障,在多个节点上运行 ID 生成器服务,每个节点都有自己独立的标识,ID 以时间因子为前缀,虽然不同的服务器时间可能存在差异,不能保证绝对的顺序,但是整体的趋势还是可以认为是顺序的,IO 负担可以忽略,同时以一个计数器为后缀,从而保证唯一性。

网上现有的开源 ID 生成器,比如 Chronos,都是运行为服务的形式,不过对我而言,这样有些太重了,于是我用 PHP 实现了一个非服务化的简版 ID 生成器,虽然它很简单,但是它并不简陋,实现了 snowflake 要求的功能:

class Sequence

{

const EPOCH = 1000000000000;

const TIME_BITS = 41;

const NODE_BITS = 10;

const COUNT_BITS = 10;

private $node = 0;

private $ttl = 10;

public function __construct($node)

{

$max = $this->max(self::NODE_BITS);

if (is_int($node) === false || $node > $max || $node < 0) {

throw new \InvalidArgumentException('node');

}

$this->node = $node;

}

public function generate($time = null)

{

if ($time === null) {

$time = (int)(microtime(true) * 1000);

}

return ($this->time($time) << (self::NODE_BITS + self::COUNT_BITS)) |

($this->node << self::COUNT_BITS) |

($this->count($time));

}

public function restore($id)

{

$binary = decbin($id);

$position = -(self::NODE_BITS + self::COUNT_BITS);

return array(

'time' => bindec(substr($binary, 0, $position)) + self::EPOCH,

'node' => bindec(substr($binary, $position, - self::COUNT_BITS)),

'count' => bindec(substr($binary, - self::COUNT_BITS)),

);

}

public function setTTL($ttl)

{

$this->ttl = $ttl;

}

private function time($time)

{

$time -= self::EPOCH;

$max = $this->max(self::TIME_BITS);

if (is_int($time) === false || $time > $max || $time < 0) {

throw new \InvalidArgumentException('time');

}

return $time;

}

private function count($time)

{

$key = "seq:count:" . ($time % ($this->ttl * 1000));

while (!$count = apcu_inc($key)) {

apcu_add($key, mt_rand(0, 9), $this->ttl);

}

$max = $this->max(self::COUNT_BITS);

if ($count > $max) {

throw new \UnexpectedValueException('count');

}

return $count;

}

private function max($bits)

{

return -1 ^ (-1 << $bits);

}

}

?>

本文中的实现利用 apcu 来保存数据,但是并不需要以服务的形式存在。以 41 位毫秒时间为例,理论上最大值可以保存到 2039-09-07,如果考虑到 EPOCH,还可以保存的时间更久远点,以 1000000000000 为例,则可以保存到 2071-05-16,此外我们给节点留了 10 位,计数器留了 10 位,理论上可以容纳最多 1023 个节点,每个节点每毫秒最多 1023 个 ID。这些阈值基本都足够了,多半还没到达上限,系统就已经挂了。

BTW:如果是一些非亲缘性的 PHP 进程共同使用一个 id 生成器的话,比如 php-fpm 和 php-cli 共同使用一个 id 生成器,那么 apcu 并不合适,此时需要使用 libshmcache。

需要说明的是,最初我的设计并不是以毫秒为为单位,而是以秒为单位,但是以秒为单位有一个问题:假设在一秒内重启 php-fpm,那么有可能会产生不唯一的值,虽然可以通过在重启脚本里 sleep 一秒来规避问题,但是毕竟太麻烦了,于是我索性以毫秒为单位来设计,因为我们不可能在一毫秒的间隔内重启 php-fpm,所以这个问题就不存在了。

不过,如果服务器出现时间回退现象,那么依然可能产生不唯一的值,但需要满足几个条件:首先,服务器时间发生了回退;其次,回退后生成 ID 时的时间恰好在以前使用过;最后,服务器因为 LRU 等原因清除了相关的缓存。要满足这些条件,基本是很难的,也就是说,对于绝大部分 PHP 项目而言,本文的代码可以认为是足够强壮的。

此外,生成的 ID 最好别直接用,不然别人可以反解出其中的数据,比如你有多少台服务器等等,解决办法是在应用层用 hashids 编码及解码,如此一来,数据库里保存的还是原始的 ID(Bigint),但是用户看到的却是 HASH ID,从而更好的保护了数据的安全。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值