算法原理
SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
41bit-时间戳,用来记录时间戳,毫秒级。
- 41位可以表示2^{41}-1个数字,
- 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^{41}-1,减1是因为可表示的数值范围是从0开始算的,而不是1。
- 也就是说41位可以表示2{41}-1个毫秒的值,转化成单位年则是(2{41}-1) / (1000 * 60 * 60 * 24 *365) = 69年
10bit-工作机器id,用来记录工作机器id。
- 可以部署在2^{10} = 1024个节点,包括5位datacenterId和5位workerId
- 5位(bit)可以表示的最大正整数是2^{5}-1 = 31,即可以用0、1、2、3、…31这32个数字,来表示不同的datecenterId或workerId
12bit-序列号,序列号,用来记录同毫秒内产生的不同id。
- 12位(bit)可以表示的最大正整数是2^{12}-1 = 4095,即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
SnowFlake可以保证:
- 所有生成的id按时间趋势递增
- 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)
c#代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace snowflake
{
public class SnowflakeIDcreator
{
private static long workerId=1; //机器ID
private static long twepoch = 68020L; //唯一时间,这是一个避免重复的随机量,自行设定不要大于当前时间戳
private static long sequence = 0L;
private static int workerIdBits = 4; //机器码字节数。4个字节用来保存机器码(定义为Long类型会出现,最大偏移64位,所以左移64位没有意义)
private static long maxWorkerId = -1L ^ -1L << workerIdBits; //最大机器ID
private static int sequenceBits = 10; //计数器字节数,10个字节用来保存计数码
private static int workerIdShift = sequenceBits; //机器码数据左移位数,就是后面计数器占用的位数
private static int timestampLeftShift = sequenceBits + workerIdBits; //时间戳左移动位数就是机器码和计数器总字节数
private static long sequenceMask = -1L ^ -1L << sequenceBits; //一微秒内可以产生计数,如果达到该值则等到下一微妙在进行生成
private static long lastTimestamp = -1L;
private static object lockObj = new object();
/// <summary>
/// 设置机器码
/// </summary>
/// <param name="id">机器码</param>
public static void SetWorkerID(long id)
{
SnowflakeIDcreator.workerId = Id;
}
public static long nextId()
{
lock (lockObj)
{
long timestamp = timeGen();
if (lastTimestamp == timestamp)
{ //同一微妙中生成ID
SnowflakeIDcreator.sequence = (SnowflakeIDcreator.sequence + 1) & SnowflakeIDcreator.sequenceMask; //用&运算计算该微秒内产生的计数是否已经到达上限
if (SnowflakeIDcreator.sequence == 0)
{
//一微妙内产生的ID计数已达上限,等待下一微妙
timestamp = tillNextMillis(lastTimestamp);
}
}
else
{ //不同微秒生成ID
SnowflakeIDcreator.sequence = 0; //计数清0
}
if (timestamp < lastTimestamp)
{ //如果当前时间戳比上一次生成ID时时间戳还小,抛出异常,因为不能保证现在生成的ID之前没有生成过
throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} milliseconds",
lastTimestamp - timestamp));
}
lastTimestamp = timestamp; //把当前时间戳保存为最后生成ID的时间戳
long nextId = (timestamp - twepoch << timestampLeftShift) | SnowflakeIDcreator.workerId << SnowflakeIDcreator.workerIdShift | SnowflakeIDcreator.sequence;
return nextId;
}
}
/// <summary>
/// 获取下一微秒时间戳
/// </summary>
/// <param name="lastTimestamp"></param>
/// <returns></returns>
private static long tillNextMillis(long lastTimestamp)
{
long timestamp = timeGen();
while (timestamp <= lastTimestamp)
{
timestamp = timeGen();
}
return timestamp;
}
/// <summary>
/// 生成当前时间戳
/// </summary>
/// <returns></returns>
private static long timeGen()
{
return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
}
}
}
用法:
-
全局里定义一次 SnowflakeIDcreator.SetWorkerID(100);
-
然后直接可使用:SnowflakeIDcreator.nextId()
测试
下面实验用100个线程同时并发生成id
1.验证是否会重复;2.测试算法速度;3.与直接lock进行id生成的区别
后台代码:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace snowflake
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
ConcurrentBag<long> ids = new ConcurrentBag<long>();
ManualResetEvent resetEvent = new ManualResetEvent(false);
/// <summary>
/// 用雪花算法生成id测试
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnCreate_Click(object sender, EventArgs e)
{
int count = int.Parse(txtNum.Text);
ids = new ConcurrentBag<long>();
SnowflakeIDcreator.SetWorkerID(100);
resetEvent.Reset();
List<Task> tsks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tsks.Add( Task.Factory.StartNew(new Action(()=>{
taskprocess();
})));
}
DateTime dt1 = DateTime.Now;
resetEvent.Set();
Task.WaitAll(tsks.ToArray());
double takeseconds = (DateTime.Now - dt1).TotalSeconds;
double speed = ids.Distinct().Count() / takeseconds;
txtMsg.Text += "\r\n";
txtMsg.Text += string.Format("使用雪花算法总共生成{0}个号码,耗时{1}秒,速度{2}条/秒", ids.Distinct().Count(), takeseconds,speed.ToString("F2"));
}
private void taskprocess()
{
resetEvent.WaitOne();
for (int i = 0; i < 100000; i++)
{
ids.Add(SnowflakeIDcreator.nextId());
}
}
/// <summary>
/// 直接用lock生成id
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
int count = int.Parse(txtNum.Text);
ids = new ConcurrentBag<long>();
resetEvent.Reset();
List<Task> tsks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tsks.Add(Task.Factory.StartNew(new Action(() => {
taskprocess2();
})));
}
DateTime dt1 = DateTime.Now;
resetEvent.Set();
Task.WaitAll(tsks.ToArray());
double takeseconds = (DateTime.Now - dt1).TotalSeconds;
double speed = ids.Distinct().Count() / takeseconds;
txtMsg.Text += "\r\n";
txtMsg.Text += string.Format("lock方式总共生成{0}个号码,耗时{1}秒,速度{2}条/秒", ids.Distinct().Count(), takeseconds, speed.ToString("F2"));
}
private void taskprocess2()
{
resetEvent.WaitOne();
for (int i = 0; i < 100000; i++)
{
ids.Add(IDCreator.getSeqID());
}
}
}
}
其中lock方法生成的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace snowflake
{
public class IDCreator
{
static long m_curid = 1;
static object mlock = new object();
public static long getSeqID()
{
lock (mlock)
{
if (m_curid > long.MaxValue)
{
m_curid = 1;
}
m_curid++;
return m_curid;
}
}
}
}
测试结论
雪花算法每秒生成100w条左右,而Lock方式直接生成id速度可达到777w条,只是雪花算法可以分布式,带了机器码和时间戳。如果程序基于单机运行,直接lock速度更快。