Snowflake雪花算法

参考链接:https://zhuanlan.zhihu.com/p/65095562

在应用程序中,经常需要全局唯一的ID作为数据库主键。如何生成全局唯一ID?

首先,需要确定全局唯一ID是整型还是字符串?如果是字符串,那么现有的UUID就完全满足需求,不需要额外的工作。缺点是字符串作为ID占用空间大,索引效率比整型低。

如果采用整型作为ID,那么首先排除掉32位int类型,因为范围太小,必须使用64位long型。

采用整型作为ID时,如何生成自增、全局唯一且不重复的ID?

方案一:利用数据库的自增ID,从1开始,基本可以做到连续递增。Oracle可以用SEQUENCE,MySQL可以用主键的AUTO_INCREMENT,虽然不能保证全局唯一,但每个表唯一,也基本满足需求。

数据库自增ID的缺点是数据在插入前,无法获得ID。数据在插入后,获取的ID虽然是唯一的,但一定要等到事务提交后,ID才算是有效的。有些双向引用的数据,不得不插入后再做一次更新,比较麻烦。

第二种方式是采用一个集中式ID生成器,它可以是Redis,也可以是ZooKeeper,也可以利用数据库的表记录最后分配的ID。

这种方式最大的缺点是复杂性太高,需要严重依赖第三方服务,而且代码配置繁琐。一般来说,越是复杂的方案,越不可靠,并且测试越痛苦。

第三种方式是类似Twitter的Snowflake算法,它给每台机器分配一个唯一标识,然后通过时间戳+标识+自增实现全局唯一ID。这种方式好处在于ID生成算法完全是一个无状态机,无网络调用,高效可靠。缺点是如果唯一标识有重复,会造成ID冲突。

Snowflake算法采用41bit毫秒时间戳,加上10bit机器ID,加上12bit序列号,理论上最多支持1024台机器每秒生成4096000个序列号,对于Twitter的规模来说够用了。

但是对于绝大部分普通应用程序来说,根本不需要每秒超过400万的ID,机器数量也达不到1024台,所以,我们可以改进一下,使用更短的ID生成方式:

53bitID由32bit秒级时间戳+16bit自增+5bit机器标识组成,累积32台机器,每秒可以生成65万个序列号

Snowflake算法采用41bit毫秒时间戳,加上10bit机器ID,加上12bit序列号,理论上最多支持1024台机器每秒生成4096000个序列号,

* 对于Twitter的规模来说够用了。

* 但是对于绝大部分普通应用程序来说,根本不需要每秒超过400万的ID,机器数量也达不到1024台,所以,我们可以改进一下,使用更短的ID生成方式:

* 53bitID由32bit秒级时间戳+16bit自增+5bit机器标识组成,累积32台机器,每秒可以生成6.5万个序列号

* 53 bits unique id:

*

* |--------|--------|--------|--------|--------|--------|--------|--------|

* |00000000|00011111|11111111|11111111|11111111|11111111|11111111|11111111|

* |--------|---xxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxx-----|--------|--------|

* |--------|--------|--------|--------|--------|---xxxxx|xxxxxxxx|xxx-----|

* |--------|--------|--------|--------|--------|--------|--------|---xxxxx|

*

* Maximum ID = 11111_11111111_11111111_11111111_11111111_11111111_11111111

*

* Maximum TS = 11111_11111111_11111111_11111111_111

*

* Maximum NT = ----- -------- -------- -------- ---11111_11111111_111 = 65535

*

* Maximum SH = ----- -------- -------- -------- -------- -------- ---11111 = 31

*

* It can generate 64k unique id per IP and up to 2106-02-07T06:28:15Z.

算法C++代码实现

uuid.h头文件

#ifndef _UUID_H_

#define _UUID_H_

#include <string>

#include <stdint.h>

using namespace std;

#define LLong long long

class CUuid

{

    public:

        CUuid();

        ~CUuid();

        LLong getServerIdAsLong();

        LLong nextId(LLong epochSecond);

        LLong generateId(LLong epochSecond, LLong next, LLong shardId);

        string GenUnid();

    private:

        LLong m_offset;

        LLong m_lastEpoch;

        LLong m_SHARD_ID;

};

#endif

uuid.cpp

#include "uuid.h"

#include <sys/time.h>

#include <unistd.h>/* gethostname */

#include <netdb.h> /* struct hostent */

#include <arpa/inet.h> /* inet_ntop */

#include <sstream>

#include <climits>

#define MAX_NEXT 0b1111111111111111L

#define OFFSET 946656000L //开始时间从2000-01-01 开始

CUuid::CUuid()

{

    m_offset = 0;

    m_lastEpoch = 0;

    m_SHARD_ID = getServerIdAsLong();

    m_SHARD_ID = m_SHARD_ID & 0x1F; // SH控制在0~31

}

CUuid::~CUuid()

{

}



LLong CUuid::getServerIdAsLong()

{

    LLong lIp = 0;

    char name[256];

    gethostname(name, sizeof(name)); 

    struct hostent* host = gethostbyname(name);

    char szIp[32];

    const char* ret = inet_ntop(host->h_addrtype, 
                              host->h_addr_list[0], szIp,  sizeof(szIp));

    if (NULL==ret) {

        cout<<"hostname transform to ip failed"<<endl;

        return -1;

    }

    lIp = htonl(inet_addr(szIp));

    return lIp;

}


LLong CUuid::nextId(LLong epochSecond)

{

    if(epochSecond < m_lastEpoch)

    {

        // warning: clock is turn back:时间回拨

        cout<<"clock is back: "<<epochSecond <<"from previous:"<<m_lastEpoch<<endl;

        epochSecond = m_lastEpoch;

    }

    if(m_lastEpoch != epochSecond)

    {

        m_lastEpoch = epochSecond;

        m_offset = 0;

    }

    m_offset++;

    LLong next = m_offset & MAX_NEXT;

    if(next == 0) 
    {

        cout<<"maximum id reached in 1 second in epoch: "<<epochSecond<<endl;

        return nextId(epochSecond + 1);

    }

    return generateId(epochSecond, next, m_SHARD_ID);

}



LLong CUuid::generateId(LLong epochSecond, LLong next, LLong shardId)

{

    //OFFSET起始时间戳,用于用当前时间戳减去这个时间戳,算出偏移量

    //16bit自增+5bit机器标识->21

    return ((epochSecond - OFFSET) << 21) | (next << 5) | shardId;

}



int main()

{

    struct timeval tv;

    gettimeofday(&tv, NULL);
    CUuid uuid;

    LLong lRet = uuid.nextId(tv.tv_sec);

    ostringstream os;

    os << lRet;

    string result;

    istringstream is(os.str());

    is >> result;

    cout<<result<<endl;

    return 0;

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值