前言
在应用程序中,唯一ID是一个系统必备的功能。体现在:
- 单体应用:单体应用用于唯一标识用户、订单等的唯一标识需要保证应用内唯一。
- 半分布式系统:分区分服的游戏应用,在合服时为了避免数据冲突需要保证所有的ID全局唯一。
- 全分布式系统:大型的电商、外卖、金融系统等的订单ID、用户ID等需要全局唯一。
单体应用的唯一ID可以简单使用数据库的自增ID来实现即可,在这里我们不予以讨论。我们讨论的是在半分布式和全分布式系统中的唯一ID生成的方案。
ID生成系统的需求
- 全局唯一性:不能出现重复的ID,这是最基本的要求。
- 趋势递增:MySQL InnoDB引擎使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应尽量使用有序的主键保证写入性能。
- 单调递增:保证下一个ID一定大于上一个ID。几乎所有的关系性数据库都使用B+Tree来进行数据在硬盘上的存储,为了避免叶分裂,需要保证新的ID总是大于之前所有的ID。
- 信息安全:如果ID是连续递增的,恶意用户就可以很容易的窥见订单号的规则,从而猜出下一个订单号,如果是竞争对手,就可以直接知道我们一天的订单量。所以在某些场景下,需要ID无规则。
技术方案
1. UUID
UUID是指在一台机器在同一时间中生成的数字在所有机器中都是唯一的。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字。
UUID由以下几部分的组成:
(1)当前日期和时间。
(2)时钟序列。
(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以连字号分为五段形式的36个字符,示例:550e8400-e29b-41d4-a716-446655440000
很多语言中都提供了原生的生成UUID的方法。比如:
C#
new GUID()
JAVA
UUID.randomUUID()
优点
- 性能非常高:本地生成,没有网络消耗。
缺点
- 不易存储:UUID太长,16字节128位,通常以36长度的字符串表示。
- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
- ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,效率太低。
2. MongoDB
MongoDB的ObjectId 采用12字节的存储空间,每个字节两位16进制数字,是一个24位的字符串。
生成规则如下:
[0,1,2,3] [4,5,6] [7,8] [9,10,11]
时间戳 |机器码 |PID |计数器
- 前四字节是时间戳,可以提供秒级别的唯一性。
- 接下来三字节是所在主机的唯一标识符,通常是机器主机名的散列值。
- 接下来两字节是产生ObjectId的PID,确保同一台机器上并发产生的ObjectId是唯一的。
前九字节保证了同一秒钟不同机器的不同进程产生的ObjectId时唯一的。 - 最后三字节是自增计数器,确保相同进程同一秒钟产生的ObjectId是唯一的。
优点
缺点
- 不易存储:ObjectId 太长,12字节96位,通常以24长度的字符串表示。
- ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,效率太低。
- ID作为索引时,导致索引的数据量太大,浪费磁盘空间。
3. 数据库
使用数据库的ID自增策略,如 MySQL 的 auto_increment。
优点
- 数据库生成的ID绝对有序
缺点
- 需要独立部署数据库实例
- 有网络请求,速度慢
- 有单点故障的风险。要解决这个问题,就得引入集群,进一步增加系统的复杂度。
4. Redis
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。
优点
- 不依赖于数据库,灵活方便,且性能优于数据库
缺点
- 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
- 有单点故障的风险。要解决这个问题,就得引入集群,进一步增加系统的复杂度。
5. 百度UidGenerator
UidGenerator是百度开源的分布式ID生成器,基于于Snowflake算法的实现。
具体可以参考官网:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
6. 美团Leaf
Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,但也需要依赖关系数据库、Zookeeper等中间件。
具体可以参考官网:https://tech.meituan.com/2017/04/21/mt-leaf.html
7. Snowflake
Twitter 实现了一个全局ID生成的服务 Snowflake:https://github.com/twitter-archive/snowflake
如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:
- 1位符号位:
最高位为 0,表示正数;因为所有的ID都是正数。 - 41位时间戳(毫秒级):
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 起始时间戳),这里的起始时间戳由程序来指定,所以41位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年。 - 10位数据机器位:
包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024 个节点。超过这个数量,生成的ID就有可能会冲突。 - 12位毫秒内的序列:
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID。
加起来刚好64位,为一个Int64型的整数。
优点
- 简单高效,没有网络请求,生成速度快。
- 时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增。
- 灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求。
- 生成的是一个Int64的整形,作为数据库的索引,效率非常高。
缺点
- 依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成。
- 在分布式环境上,每个服务器的时钟不可能完全同步,有时会出现不是全局递增的情况。
在大多数情况下,Snowflake算法能够满足应用需求。以下是我使用Go和C#实现的可以灵活配置各个部分所占位数的Snowflake算法。
Go
/*
用于生成唯一的、递增的Id。生成的规则如下:
1、生成的Id包含一个固定前缀值
2、为了生成尽可能多的不重复数字,所以使用int64来表示一个数字,其中:
0 000000000000000 0000000000000000000000000000 00000000000000000000
第一部分:1位,固定为0
第二部分:共PrefixBitCount位,表示固定前缀值。范围为[0, math.Pow(2, PrefixBitCount))
第三部分:共TimeBitCount位,表示当前时间距离基础时间的秒数。范围为[0, math.Pow(2, TimeBitCount)),以2019-1-1 00:00:00为基准则可以持续到2025-07-01 00:00:00
第四部分:共SeedBitCount位,表示自增种子。范围为[0, math.Pow(2, SeedBitCount))
3、总体而言,此规则支持每秒生成math.Pow(2, SeedBitCount)个不同的数字,并且在math.Pow(2, TimeBitCount)/60/60/24/365年的时间范围内有效
*/
package idUtil
import (
"fmt"
"math"
"sync"
"time"
)
// Id生成器
type IdGenerator struct {
prefixBitCount uint // 前缀所占的位数
minPrefix int64 // 最小的前缀值
maxPrefix int64 // 最大的前缀值
timeBitCount uint // 时间戳所占的位数
baseTimeUnix int64 // 基础时间
maxTimeDuration int64 // 最大的时间范围
seedBitCount uint // 自增种子所占的位数
currSeed int64 // 当前种子值
minSeed int64 // 最小的种子值
maxSeed int64 // 最大的种子值
mutex sync.Mutex // 锁对象
}
func (this *IdGenerator) getTimeStamp() int64 {
return time.Now().Unix() - this.baseTimeUnix
}
func (this *IdGenerator) generateSeed() int64 {
this.mutex.Lock()
defer this.mutex.Unlock()
if this.currSeed >= this.maxSeed {
this.currSeed = this.minSeed
} else {
this.currSeed = this.currSeed + 1
}
return this.currSeed
}
// 生成新的Id
// prefix:Id的前缀值。取值范围必须可以用创建对象时指定的前缀值的位数来表示,否则会返回参数超出范围的错误
// 返回值:
// 新的Id
// 错误对象
func (this *IdGenerator) GenerateNewId(prefix int64) (int64, error) {
if prefix < this.minPrefix || prefix > this.maxPrefix {
return 0, fmt.Errorf("前缀值溢出,有效范围为【%d,%d】", this.minPrefix, this.maxPrefix)
}
stamp := this.getTimeStamp()
seed := this.generateSeed()
id := prefix<<(this.timeBitCount+this.seedBitCount) | stamp< 63 {
return nil, fmt.Errorf("总位数%d超过63位,请调整所有值的合理范围。", prefixBitCount+timeBitCount+seedBitCount)
}
obj := &IdGenerator{
prefixBitCount: prefixBitCount,
timeBitCount: timeBitCount,
seedBitCount: seedBitCount,
}
obj.minPrefix = 0
obj.maxPrefix = int64(math.Pow(2, float64(prefixBitCount))) - 1
obj.baseTimeUnix = time.Date(2019, time.January, 1, 0, 0, 0, 0, time.Local).Unix()
obj.maxTimeDuration = (int64(math.Pow(2, float64(timeBitCount))) - 1) / 86400 / 365
obj.currSeed = 0
obj.minSeed = 0
obj.maxSeed = int64(math.Pow(2, float64(seedBitCount))) - 1
fmt.Printf("Prefix:[%d, %d], Time:%d Year, Seed:[%d, %d]\n", obj.minPrefix, obj.maxPrefix, obj.maxTimeDuration, obj.minSeed, obj.maxSeed)
return obj, nil
}
C#
/*
用于生成唯一的、递增的Id。生成的规则如下:
1、生成的Id包含一个固定前缀值
2、为了生成尽可能多的不重复数字,所以使用int64来表示一个数字,其中:
0 000000000000000 0000000000000000000000000000 00000000000000000000
第一部分:1位,固定为0
第二部分:共PrefixBitCount位,表示固定前缀值。范围为[0, math.Pow(2, PrefixBitCount))
第三部分:共TimeBitCount位,表示当前时间距离基础时间的秒数。范围为[0, math.Pow(2, TimeBitCount)),以2019-1-1 00:00:00为基准则可以持续到2025-07-01 00:00:00
第四部分:共SeedBitCount位,表示自增种子。范围为[0, math.Pow(2, SeedBitCount))
3、总体而言,此规则支持每秒生成math.Pow(2, SeedBitCount)个不同的数字,并且在math.Pow(2, TimeBitCount)/60/60/24/365年的时间范围内有效
*/
using System;
namespace Util
{
///
/// Id生成助手类
///
public class IdUtil
{
///
/// 前缀所占的位数
///
public Int32 PrefixBitCount { get; set; }
///
/// 最小的前缀值
///
private Int64 MinPrefix { get; set; }
///
/// 最大的前缀值
///
private Int64 MaxPrefix { get; set; }
///
/// 时间戳所占的位数
///
public Int32 TimeBitCount { get; set; }
///
/// 自增种子所占的位数
///
public Int32 SeedBitCount { get; set; }
///
/// 当前种子值
///
private Int64 CurrSeed { get; set; }
///
/// 最小的种子值
///
private Int64 MinSeed { get; set; }
///
/// 最大的种子值
///
private Int64 MaxSeed { get; set; }
///
/// 锁对象
///
private Object LockObj { get; set; }
///
/// 创建Id助手类对象(为了保证Id的唯一,需要保证生成的对象全局唯一)
/// prefixBitCount + timeBitCount + seedBitCount <= 63
///
/// 表示id前缀的位数
/// 表示时间的位数
/// 表示自增种子的位数
public IdUtil(Int32 prefixBitCount, Int32 timeBitCount, Int32 seedBitCount)
{
// 之所以使用63位而不是64,是为了保证值为正数
if (prefixBitCount + timeBitCount + seedBitCount > 63)
{
throw new ArgumentOutOfRangeException(String.Format("总位数{0}超过63位,请调整所有值的合理范围。", (prefixBitCount + timeBitCount + seedBitCount).ToString()));
}
this.PrefixBitCount = prefixBitCount;
this.TimeBitCount = timeBitCount;
this.SeedBitCount = seedBitCount;
this.MinPrefix = 0;
this.MaxPrefix = (Int64)System.Math.Pow(2, this.PrefixBitCount) - 1;
this.CurrSeed = 0;
this.MinSeed = 0;
this.MaxSeed = (Int64)System.Math.Pow(2, this.SeedBitCount) - 1;
Console.WriteLine(String.Format("Prefix:[{0},{1}], Time:{2} Year, Seed:[{3},{4}]", this.MinPrefix, this.MaxPrefix, (Int64)(System.Math.Pow(2, this.TimeBitCount) / 60 / 60 / 24 / 365), this.MinSeed, this.MaxSeed));
this.LockObj = new Object();
}
private Int64 GetTimeStamp()
{
DateTime startTime = new DateTime(2019, 1, 1);
DateTime currTime = DateTime.Now;
return (Int64)System.Math.Round((currTime - startTime).TotalSeconds, MidpointRounding.AwayFromZero);
}
private Int64 GenerateNewSeed()
{
lock (this.LockObj)
{
if (this.CurrSeed >= this.MaxSeed)
{
this.CurrSeed = this.MinSeed;
}
else
{
this.CurrSeed += 1;
}
return this.CurrSeed;
}
}
///
/// 生成新的Id
///
/// Id的前缀值。取值范围必须可以用初始化时指定的前缀值的位数来表示,否则会抛出ArgumentOutOfRangeException
/// 新的Id
public Int64 GenerateNewId(Int64 prefix)
{
if (prefix < this.MinPrefix || prefix > this.MaxPrefix)
{
throw new ArgumentOutOfRangeException(String.Format("前缀的值溢出,有效范围为【{0},{1}】", this.MinPrefix.ToString(), this.MaxPrefix.ToString()));
}
Int64 stamp = this.GetTimeStamp();
Int64 seed = this.GenerateNewSeed();
return (prefix << (this.TimeBitCount + this.SeedBitCount)) | (stamp << this.SeedBitCount) | seed;
}
}
}