本文博客地址我们项目之前的id生成是直接用的php的uniqid
存在的问题有:
    1. 高并发下容易重复:当高并发适用uniqid来产生唯一id的时候,我的测试数据是:1000并发,每并发产生1000次,实际输出63万uid(可能是标准输出原因未全部输出),5次重复
    2. mysql中使用字符串类型的key存在性能问题
使用这里创建的ud_uniqid带来的改变:
    优点1. 高并发重复几率降低:我的测试数据是:1000并发,每并发产生1000次,实际输出93万,无重复
    优点2. 使用过程中,可以产生唯一的递增数值id,作为mysql主键有性能优势
    缺点1. 性能比uniqid略低,耗时约为uniqid的1.5倍(不过在10e-5 -- 10e-4级别的时间差,1.5倍应该可以忽略)
    缺点2. windows不能使用(windows不支持usleep)
    缺点3. 由于使用了共享内存,所以需要在服务器开启的时候读取上一次的最大值,服务器关闭的时候写入当前的最大值
下面直接上代码了, 代码里面注释很清楚的:<?php
/**
 * functions: 产生64位唯一自增随机数,用于游戏唯一数值id生成
 *  1. 数值分布:9位区号 3位服号 32位id号 10位随机码 10位随机码
 *  2. 可提供范围:512区,每区8个游戏服务器,提供42亿id
 *  3. 重复性测试:100并发,每个并发进程产生1000次随机数,无重复
 *  4. 性能测试:与系统自带uniqid函数相比,耗时为其1.5倍
 * author: selfimpr
 * blog: http://blog.csdn.net/lgg201
 * mail: [email protected]
 *//**
 * 产生子进程
 * $func_name: 子进程处理的过程函数
 * 可接不定参数,为子进程过程函数需要的参数
 */
function new_child($func_name) {
$args = func_get_args();
unset($args[0]);
$pid = pcntl_fork();
if($pid == 0) {
function_exists($func_name) and exit(call_user_func_array($func_name, $args)) or exit(-1);
} else if($pid == -1) {
echo "Couldn’t create child process.";
} else {
return $pid;
}
}
/**
 * 计数生成器
 * 采用共享内存生成
 * $key: 每个独立的$key标记为一个计数器
 * $length: 分配内存大小
 */
function counter($key = 0x1, $length = 256) {
$segment_id = shmop_open($key, 'c', 0777, $length);
$now = intval(shmop_read($segment_id, 0, shmop_size($segment_id))) + 1;
shmop_write($segment_id, (string)$now, 0);
shmop_close($segment_id);
return $now;
}
/**
 * 自定义唯一id生成器
 * $group_id: 区号
 * $server_id: 服号
 */
function ud_uniqid($group_id = 1, $server_id = 1) {
$rand_key1 = rand(0, 1023);
usleep(rand(0, 16));
$rand_key2 = rand(0, 1023);
$id = counter();
return ($group_id << 55) | ($server_id << 52) | ($id << 20) | ($rand_key1 << 10) | ($rand_key2);
}
/**
 * 用于测试产生id重复性时的比较
 */
function ud_sort($a, $b) {
return intval($a) - intval($b);
}
/**
 * 检查重复性
 * $file_name: 产生的uid所在文件
 */
function check_repeat($file_name) {
$f = file($file_name);
usort($f, ud_sort);
$l = count($f);
$l = $l - 2;
while($l >=0) {
if($f[$l] == $f[$l + 1]) echo $f[$l + 1]."\n";
$l --;
}
}
/**
 * 生成uid并输出到标准输出
 * $times: 产生次数
 */
function check_repeat_out($times) {
while($i ++ < $times) {
echo ud_uniqid()."\n";
}
}
/**
 * 测试重复性(并发生成uid并输出到标准输出)
 * $concurrent: 并发数
 * $times: 每个并发中产生uid次数
 */
function test_repeat($concurrent, $times) {
while($i ++ < $concurrent) {
new_child(check_repeat_out, $times);
}
}
/**
 * 与系统自带uniqid性能对比测试
 * $times: 运行多少次进行时间比对
 */
function test_performence($times) {
$time = 0;
while($i ++ < $times) {
$start = microtime(true);
ud_uniqid();
$end = microtime(true);
$time += $end - $start;
}
echo "ud_uniqid($times): $time.\n";
$time = 0;
$i = 0;
while($i ++ < $times) {
$start = microtime(true);
uniqid();
$end = microtime(true);
$time += $end - $start;
}
echo "uniqid($times): $time.\n";
}
/**
 * 重复性测试方法(下面提到的ud_uniqid.php即为此文件):
 * 1. 打开test_repeat(100, 1000); 设定期望的并发数和重复次数
 * 2. CLI模式下运行程序, 并将标准输出重定向到文件,比如:php ud_uniqid.php > repeat.check.dat
 * 3. 关闭test_repeat();方法(注释)
 * 4. 打开check_repeat('repeat.check.dat'); (测试时请注意文件路径),运行:php ud_uniqid.php,如果不产生输出,则标明无重复
 * 性能测试:
 * 打开test_performence(); CLI模式运行即可
 */
//test_performence(10000);
//test_repeat(100, 1000);
//check_repeat('repeat.check.dat');
?>

解决方案 »

  1.   

    大概看了下~~~主要是usleep,以及共享内存的使用!降低重复的概率,用于处理大并发量产生唯一id还是挺不错的!
      

  2.   

    敢情lz也是做web游戏的,呵呵!~~
      

  3.   


    呵呵 恩哈哈,刚看了你回帖,突然想到一个问题的解决方案了
    存在的问题是多台gameserver进行自增的时候会产生重复,因为互相无法通信。
    解决方法:利用服id,自增的时候,增加服id个,而不是固定的1.....Thank you, 灵感
      

  4.   

    共享内存和内存hash表唯一的区别在于后者资源消耗大,有并发数限制。
    不懂你的web游戏开发,不过除非你那里不涉及数据库,否则,其实都差不多。我想这方面不会成为瓶颈。
      

  5.   

    呵呵,性能当然不会成为瓶颈,只是因为程序都快搞完了,突然要改,现在的方案相对来说很多代码不用动阿unique转出来的int会占用52个位,另外需要增加区,服信息,又由于高并发重复,需要增加随机数,位就不够用了,而mysql最长就要64位,所以就另外找路了。其实另外一个解决方案应该可行,就是专门有个数据库表来维护自增主键,据朋友说性能影响不大,按照profile来看,其实比uniqid最多也就慢个3倍左右。
      

  6.   

    恩,方案选择还是以改动小,速度快优先为好。没必要大手术。呵呵,实际需求往往没有太高的性能需求,达到水平线以上就可以了。至于维护一个自增字段,你的共享内存,hash表不都是做这个么。
    如果在你那里连普通的表都没有问题的话,那其实就不是问题了:)