php: redis + lua 实现发号器服务

一、背景

最近看到新浪微博的发号器算法,就想着自己也研究一番其中的原理,然后各种查资料,为此自己又巩固了一次位运算、计算机原码、补码、反码等相关知识。

感觉很不错,因为有那么一件事情或者目标推动你学习,我感觉是挺好的,如果漫无目的反而很难坚持下去,所以在此也推荐给大家。
 

二、为什么要实现发号器

很多地方我们都需要一个全局唯一的编号,也就是uuid。举一个常见的场景,电商系统产生订单的时候,需要有一个对应的订单编号。在composer上我们也可以看到有很多可以产生uuid的优秀组件。那么,为什么我们还要自己实现发号器,来产生uuid呢?想了一下,主要有两个原因吧:

1、我希望uuid是可反解的,通过反解uuid可以得出和我业务相关的数据。而我看到的composer关于uuid的相关组件,生成的都是一串指定格式的字符串,我很难将它同具体的业务关联起来。

2、我希望通过uuid是可以随着并放量进行调整的。比如说原有支持1秒钟可以产生1000个uuid,但随着业务规模增长,我希望变成可以支持1秒钟产生一万个。而且,最好改下配置就可以了。

出于以上两个原因,我们需要自己的发号器来产生uuid。那么,下一个问题是,我们应该如何实现发号器,实现发号器的原理又是什么呢?
 

三、snowFlake算法

关于发号器的实现原理,可能大家都听过鼎鼎大名的snowflake算法 -- 雪花算法,Twitter的分布式自增Id算法,

推特的分布式自增ID算法,使用long (8 × 8 = 64 byte)来保存uuid。其中1bit留给固定符号位0,41bit留给毫秒时间戳,10bit给MachineID,也就是机器要预先配置,剩下12位留Sequence(可支持1毫秒内4096个请求)。

也许有的人会问如果超过了1毫秒4096个请求怎么办?一般的做法是,让它等上1毫秒,促使41bit的时间戳变化。

这里我们将MachineId进行了拆分,5byte留给机器(最多可以支持32机器),5byte留给了业务号(最多可支持32种业务)

这里的时间戳保存的是当前时间与固定过去时间得一个差值,不是当前时间。这样的好处是能使用更长时间,而且不受年份限制,只取决于从什么时候开始用的,2^41 / 1000360024*365=69年。

如果保存的是当前时间戳,最多只能使用到2039年。2^41=2199023255552=2039/9/7 23:47:35

理论上单机速度:2^12*1000 = 4 096 000/s

四、如何保证在单位时间内持续递增

通过对snowflake的初步了解,发现,其实发号器也是建立在时间戳基础之上的,因为时间是天然的唯一元素。但是,如何在单位时间内,比如说一秒钟或者一毫秒之内,保证Sequence持续递增才是发号器实现的关键。

这里我们实现的方式比较简单,直接使用redis的incr进行计数,对应的key就是毫秒时间戳。出于redis内存回收的考虑,我们需要将每一个key设置过期时间。如果key是秒级别的时间戳,那么过期时间就是1秒;如果key毫秒级别的时间戳,那么过期时间就是1毫秒。

与此同时,为了保证执行incr,expire(pexpire)具有原子性,我们使用lua来进行实现。

五、demo

<?php 
class SignGenerator
{
    CONST BITS_FULL = 64;
    CONST BITS_PRE = 1;//固定
    CONST BITS_TIME = 41;//毫秒时间戳 可以最多支持69年
    CONST BITS_SERVER = 5; //服务器最多支持32台
    CONST BITS_WORKER = 5; //最多支持32种业务
    CONST BITS_SEQUENCE = 12; //一毫秒内支持4096个请求

    CONST OFFSET_TIME = "2021-11-12 00:00:00";//时间戳起点时间

    /**
     * 服务器id
     */
    protected $serverId;

    /**
     * 业务id
     */
    protected $workerId;

    /**
     * 实例
     */
    protected static $instance;

    /**
     * redis 服务
     */
    protected static $redis;

    /**
     * 获取单个实例
     */
    public static function getInstance($redis)
    {
        if(isset(self::$instance)) {
            return self::$instance;
        } else {
            return self::$instance = new self($redis);
        }
    }

    /**
     * 构造初始化实例
     */
    protected function __construct($redis)
    {
        if($redis instanceof \Redis || $redis instanceof \Predis\Client) {
            self::$redis = $redis;
        } else {
            throw new \Exception("redis service is lost");
        }
    }

    /**
     * 获取唯一值
     */
    public function getNumber()
    {
        if(!isset($this->serverId)) {
            throw new \Exception("serverId is lost");
        }
        if(!isset($this->workerId)) {
            throw new \Exception("workerId is lost");
        }

        do{
            $id = pow(2,self::BITS_FULL - self::BITS_PRE) << self::BITS_PRE;
            echo "id=",$id,"\n";

            //时间戳 41位
            $nowTime = (int)(microtime(true) * 1000);
            $startTime = (int)(strtotime(self::OFFSET_TIME) * 1000);
            $diffTime = $nowTime - $startTime;
            $shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
            $id |= $diffTime << $shift;
            echo "diffTime=",$diffTime,"\n";
            echo "id=",$id,"\n";

            //服务器
            $shift = $shift - self::BITS_SERVER;
            $id |= $this->serverId << $shift;
            echo "serverId=",$this->serverId,"\n";
            echo "id=",$id,"\n";

            //业务
            $shift = $shift - self::BITS_WORKER;
            $id |= $this->workerId << $shift;
            echo "workerId=",$this->workerId,"\n";
            echo "id=",$id,"\n";

            //自增值
            $sequenceNumber = $this->getSequence($id);
            echo "sequenceNumber=",$sequenceNumber,"\n";
            if($sequenceNumber > pow(2, self::BITS_SEQUENCE)) {
                usleep(1000);
            } else {
                $id |= $sequenceNumber;
                return $id;
            }
        } while(true);
    }

    /**
     * 反解获取业务数据
     */
    public function reverseNumber($number)
    {
        $uuidItem = [];
        $shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
        $uuidItem['diffTime'] = ($number >> $shift) & (pow(2, self::BITS_TIME) - 1);

        $shift -= self::BITS_SERVER;
        $uuidItem['serverId'] = ($number >> $shift) & (pow(2, self::BITS_SERVER) - 1);

        $shift -= self::BITS_WORKER;
        $uuidItem['workerId'] = ($number >> $shift) & (pow(2, self::BITS_WORKER) - 1);

        $shift -= self::BITS_SEQUENCE;
        $uuidItem['sequenceNumber'] = ($number >> $shift) & (pow(2, self::BITS_SEQUENCE) - 1);

        $time = (int)($uuidItem['diffTime']/1000) + strtotime(self::OFFSET_TIME);
        $uuidItem['generateTime'] = date("Y-m-d H:i:s", $time);

        return $uuidItem;
    }

    /**
     * 获取自增序列
     */
    protected function getSequence($id)
    {
        $lua = <<<LUA
        local sequenceKey = KEYS[1]
        local sequenceNumber = redis.call("incr", sequenceKey);
        redis.call("pexpire", sequenceKey, 1);
        return sequenceNumber
LUA;
        $sequence = self::$redis->eval($lua, [$id], 1);    
        $luaError = self::$redis->getLastError();
        if(isset($luaError)) {
            throw new \ErrorException($luaError);
        } else {
            return $sequence;
        }
    }

    /**
     * @return mixed
     */
    public function getServerId()
    {
        return $this->serverId;
    }

    /**
     * @param mixed $serverId
     */
    public function setServerId($serverId)
    {
        $this->serverId = $serverId;
        return $this;
    }

    /**
     * @return mixed
     */
    public function getWorkerId()
    {
        return $this->workerId;
    }

    /**
     * @param mixed $workerId
     */
    public function setWorkerId($workerId)
    {
        $this->workerId = $workerId;
        return $this;
    }
}

$redis = new Redis;

$redis->connect("127.0.0.1", 6379);
 
$instance = SignGenerator::getInstance($redis);
 
$instance->setWorkerId(2)->setServerId(1);

$number = $instance->getNumber();
echo "number".$number."\n";

$item = $instance->reverseNumber($number);
var_dump($item);
?>

运行看看,发现生成的参数和反解析出来的是一样的。

id=0
diffTime=54767209
id=229710323777536
serverId=1
id=229710323908608
workerId=2
id=229710323916800
sequenceNumber=1
number229710323916801
array(5) {
  ["diffTime"]=>
  int(54767209)
  ["serverId"]=>
  int(1)
  ["workerId"]=>
  int(2)
  ["sequenceNumber"]=>
  int(1)
  ["generateTime"]=>
  string(19) "2021-11-12 15:12:47"
}

六、参考资料

  1. Sina Visitor System
  2. 转发参考的知识文章
  3. 位运算知识
  4. 原码、补码、反码

猜你喜欢

转载自blog.csdn.net/panjiapengfly/article/details/121291247