您的位置:首页 > 编程语言 > PHP开发

【Zanuck 镇】编写php高性能snowflake算法插件(分布式64位唯一性自增id生成算法)

2016-09-01 11:21 411 查看
snowflake算法是个啥?首先我来提出个问题,怎么在分布式系统中生成唯一性id并保持该id大致自增?在twitter中这是最重要的业务场景,于是twitter推出了一种snowflake算法。参考地址:https://github.com/twitter/snowflake

小镇为什么想要写这么个算法呢,因为开发环境是php并且需要生成唯一性自增id,因为业务逻辑的原因不能通过获取mysql的auto increment主键来获取该值。于是小镇想到了三种解决办法

第一种,使用php官方类库中的uniqid()方法,但是在随后的多进程压力测试中造成了非常多的重复值,这证明了php的uniqid()并不能实现id的唯一性(也很容易理解,根据毫秒生成的id当然不可能唯一而且不可能在分布式中运用,其次生成的是string类型,最重要的一点,速度太慢)
第二种,为了保证唯一性,使用md5算法将当前毫秒值加上随机数值散列出去。但是理论上不能严格保证id的唯一性,其次不能保证大致的自增性。
第三种,使用php原生语言编写snowflake算法。缺点也非常的明显,php 写这种相对来说比较复杂的算法性能太差,在高频使用的情况下线程和进程锁的问题不是特别的乐观。(其实性能问题也不是大问题,因为本身web的瓶颈也在数据库上,但是通过编写php扩展的方式可以将这种问题一劳永逸的解决,何乐而不为呢?)

可以看出以上三种方法都有缺点,在高并发的情况会出现各种各样的问题。于是,小镇在这里使用了C语言编写了snowflake算法并作为了php的插件使用。(给大家提前透露一下,性能非常好)
好了,现在开始,先用C语言实现snowflake算法,用C语言实现非常简单,只要按照snowflake算法的规则来就行了,我摘抄了csdn上一个比较好的算法,地址如下:http://blog.csdn.net/wallwind/article/details/49701397,但是博主没有做注释,但是我看了下,就是单纯的按照规则实现了下,于是小镇决定结合snowflake算法来一句句讲解。
首先先介绍snowflake算法的规则,详细地址如下:http://www.lanindex.com/twitter-snowflake%EF%BC%8C64%E4%BD%8D%E8%87%AA%E5%A2%9Eid%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3/(觉得我讲的不够仔细的自己去看,或者直接看snowflake twitter的源码)



通过该图可以看出该id是一个64bit的long long int除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。

那好,第一步我们来生成第一组的41bit时间戳。在Linux下还是比较容易实现的,大家粘贴代码即可,相信大家也能理解。

<span style="font-size:18px;">uint64_t generateStamp()
{
timeval tv;
gettimeofday(&tv, 0);
return (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000;
}</span>


第二步,我们来生成workerid,workerid为10bit。这个就有很多种方法了,比如说在单机上,每个进程用自己的pid的低10位作为workerid(注意进程的低10bit重复问题,很多办法可以解决,我不列举),或者通过文件读写的方式获取一个workerid,在分布式上可以通过mac地址获取一个workerid。总归有很多的方法,这个请大家发挥自己的想象力。(请记住保持workerid的唯一性)(多进程状态下无法保持id的唯一性,所以多进程状态下也要通过不同的workerid来标记不同的进程)

第三步,生成最后12bit的序列号,在Linux中,我们可以利用linux的无锁化编程,__sync_add_and_fetch()函数确保了在多线程的环境中的需要自增的序列号的原子性操作,snowflake的理念是这样的,在同一毫秒内,序列号从0开始进行原子性递增来满足序列号的唯一性。(注意,因为多线程使用的是同一全局变量空间所以可以用该方法保证序列号的唯一性,但是在多进程的环境中是不可以的,因为多进程的变量空间是相互分割开的,而在php环境基本上是多进程,所以需要通过worker

id来区分不同进程)


代码如下:
if (nowtime == g_info.last_stamp) {
g_info.seqid = atomic_incr(g_info.seqid) & 0xfff;
if (g_info.seqid == 0) {
nowtime = wait_next_ms(g_info.last_stamp);
}
} else {
g_info.seqid = 0;
}


其中atomic_incr(g_info.seqid) & 0xfff是取seqid的低12位的值,位运算大家应该熟悉的,如果不熟悉也可以不用看这篇文章了,先把C语言学会再说。
可以看出来,当毫秒数相同的时候我们需要对seqid进行自增。当生成速度过快当seqid超过了0xfff,那么超过的那一个值&0xfff==0,那我们就需要等待下一毫秒,等待的函数如下:

uint64_t wait_next_ms(uint64_t lastStamp)
{
uint64_t cur = 0;
do {
cur = get_curr_ms();
} while (cur <= lastStamp);
return cur;
}


好了,这三个id都生成完毕了,最后我们来将三个id黏在一起就行了,代码如下:
uint64_t get_unique_id()
{
uint64_t uniqueId = 0;
uint64_t nowtime = get_curr_ms();
uniqueId = nowtime << 22;
uniqueId |= (g_info.workid & 0x3ff) << 12;
if (nowtime < g_info.last_stamp) {
perror("error");
exit(-1);
}
if (nowtime == g_info.last_stamp) {
g_info.seqid = atomic_incr(g_info.seqid) & 0xfff;
if (g_info.seqid == 0) {
nowtime = wait_next_ms(g_info.last_stamp);
}
} else {
g_info.seqid = 0;
}
g_info.last_stamp = nowtime;
uniqueId |= g_info.seqid;
return uniqueId;
}


首先将41位的时间戳左移22位,为workerid和seqid留出位置。将workerid留低10位左移12位再与时间戳 | 运算将workerid黏到时间戳上。然后seqid&0xfff 留低12位再跟uniqueid取|就行了。

生成为php扩展

好了,C语言的实现完毕了,全部代码请去http://blog.csdn.net/wallwind/article/details/49701397 copy。(毕竟用的别人的)

下面将C语言生成为php的扩展,这也是个重点。小镇这里使用的是Linux系统,如果使用Windows系统的操作应该略有不同,这里小镇就不去研究了。

第一步,确定你要扩展的方法,这里小镇定义为zty_id()。扩展的模块名称定义为zty。首先创建zty.def文件,将zty_id()写入文件,进入php源码目录下的ext目录,运行./ext_skel --extname=zty --proto=/root/zty.def ,这将会生成zty目录,进入zty目录,里面是php扩展的框架,如下图所示:



首先我们将snowflake.c和snowflake.h放入zty文件并在zty.c文件的头部加上#include "snowflake.h"。然后在zty.c中找到PHP_FUNCTION(zty_id)方法。并写入如下代码:



修改config.m4文件,这个是生成编译文件的关键,去除如下三行前的dnl(PHP_ARG_ENABLE的那三行)。然后在最后的PHP_NEW_EXTENSION中加入snowflake.c,如下图所示:





然后输入如下命令:
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/php/bin/php-config
make && make install

最后在php.ini的最后添加extension=zty.so就大功告成了,赶紧写个php文件试下该zty_id()函数吧。
小镇测试了下,循环100w次只需要0.4秒,性能真的很好,如果是uniqid(),100w次不知道要等到什么时候,而且不能保证唯一性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: