VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 分布式ID生成(雪花算法、UUID)

第59章 分布式ID生成实战
59.1 分布式ID生成(雪花算法、UUID)
一、我踩过的ID坑:从“UUID导致数据库崩溃”到“雪花算法救了我”
做电商系统的时候,一开始图省事用UUID作为订单ID,结果上线半年后,数据库查询越来越慢,最后直接崩溃了。查了半天发vb.net教程C#教程python教程SQL教程access 2010教程现是UUID是随机字符串,导致数据库索引严重碎片化,每次查询都要全表扫描——后来换成雪花算法生成的递增ID,查询性能直接提升了80%。还有一次用数据库自增ID做分库分表,结果两个库的ID重复了,导致订单冲突——用雪花算法的工作机器ID解决了这个问题。这节我把这些踩坑经验揉进去,用大白话讲透UUID和雪花算法的核心原理,结合C#实战代码逐行拆解,拓展生产级优化技巧,让你再也不踩分布式ID的坑!
二、UUID:全球唯一的“随机身份证”,适合不需要递增的场景
UUID(Universally Unique Identifier)是全球唯一的标识符,核心是“通过随机数或时间戳生成128位的字符串”,保证全球范围内的唯一性,不需要依赖任何中间件。
大白话解释:把UUID比作“全球唯一的身份证号”
1.UUIDv1:基于时间戳+MAC地址生成,比如“123e4567-e89b-12d3-a456-426614174000”,前半部分是时间戳,后半部分是MAC地址;
2.UUIDv4:基于随机数生成,比如“123e4567-e89b-42d3-a456-426614174000”,中间的“4”表示是UUIDv4;
3.唯一性保证:UUID的128位足够大,重复的概率几乎为0(大概是10^-36,相当于连续中10次彩票头奖)。
我踩过的坑:用UUIDv1作为用户ID,结果被黑客通过UUID解析出服务器的MAC地址,导致服务器被攻击——后来换成UUIDv4,避免了MAC地址泄露的问题!
实战1:C#生成不同版本的UUID
csharp

	using System;
	using System.Security.Cryptography;
	
	namespace DistributedId.Uuid;
	
	public class UuidGenerator
	{
	/// <summary>
	/// 生成UUIDv1(时间戳+MAC地址)
	/// </summary>
	public static Guid GenerateUuidV1()
	{
	// 1. 用Guid.NewGuid()生成的其实是UUIDv4?不对,.NET的Guid.NewGuid()在Windows上是UUIDv4,在Linux上是UUIDv1
	// 手动实现UUIDv1:基于时间戳+MAC地址
	var now = DateTime.UtcNow;
	var timeSpan = now - new DateTime(1582, 10, 15, 0, 0, 0, DateTimeKind.Utc); // UUID的起始时间是1582年10月15日
	var timeBytes = BitConverter.GetBytes(timeSpan.Ticks * 100); // 转换为100纳秒为单位的时间戳
	Array.Reverse(timeBytes, 0, 4);
	Array.Reverse(timeBytes, 4, 2);
	Array.Reverse(timeBytes, 6, 2);
	
	// 获取MAC地址
	var macBytes = GetMacAddressBytes();
	if (macBytes == null)
	{
	// 如果获取不到MAC地址,用随机数代替
	macBytes = new byte[6];
	RandomNumberGenerator.Fill(macBytes);
	macBytes[0] |= 0x01; // 设置本地位,表示不是真实的MAC地址
	}
	
	// 生成时钟序列(随机数)
	var clockSequenceBytes = new byte[2];
	RandomNumberGenerator.Fill(clockSequenceBytes);
	clockSequenceBytes[0] &= 0x3F; // 清除前两位,只保留6位
	
	// 组装UUIDv1:时间戳(8字节) + 时钟序列(2字节) + MAC地址(6字节)
	var uuidBytes = new byte[16];
	Array.Copy(timeBytes, 0, uuidBytes, 0, 8);
	Array.Copy(clockSequenceBytes, 0, uuidBytes, 8, 2);
	Array.Copy(macBytes, 0, uuidBytes, 10, 6);
	
	// 设置版本和变体:版本1,变体RFC4122
	uuidBytes[6] = (byte)((uuidBytes[6] & 0x0F) | 0x10); // 第7字节的高4位是版本号1
	uuidBytes[8] = (byte)((uuidBytes[8] & 0x3F) | 0x80); // 第9字节的高2位是变体10(RFC4122)
	
	return new Guid(uuidBytes);
	}
	
	/// <summary>
	/// 生成UUIDv4(随机数)
	/// </summary>
	public static Guid GenerateUuidV4()
	{
	// .NET的Guid.NewGuid()就是UUIDv4,底层用的是CryptGenRandom,安全随机
	return Guid.NewGuid();
	}
	
	/// <summary>
	/// 生成UUIDv3(基于名称的MD5哈希)
	/// </summary>
	public static Guid GenerateUuidV3(Guid namespaceId, string name)
	{
	return Guid.Create(namespaceId, name, GuidCreationVersion.Version3);
	}
	
	/// <summary>
	/// 生成UUIDv5(基于名称的SHA1哈希)
	/// </summary>
	public static Guid GenerateUuidV5(Guid namespaceId, string name)
	{
	return Guid.Create(namespaceId, name, GuidCreationVersion.Version5);
	}
	
	/// <summary>
	/// 获取MAC地址字节数组
	/// </summary>
	private static byte[] GetMacAddressBytes()
	{
	try
	{
	// 遍历所有网络接口,获取第一个非环回的MAC地址
	foreach (var networkInterface in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
	{
	if (networkInterface.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
	continue;
	
	var macAddress = networkInterface.GetPhysicalAddress();
	var macBytes = macAddress.GetAddressBytes();
	if (macBytes != null && macBytes.Length == 6)
	return macBytes;
	}
	}
	catch (Exception ex)
	{
	Console.WriteLine($"获取MAC地址失败:{ex.Message}");
	}
	return null;
	}
	}
	
	// 测试代码
	class Program
	{
	static void Main(string[] args)
	{
	Console.WriteLine($"UUIDv1:{UuidGenerator.GenerateUuidV1()}");
	Console.WriteLine($"UUIDv4:{UuidGenerator.GenerateUuidV4()}");
	Console.WriteLine($"UUIDv3:{UuidGenerator.GenerateUuidV3(Guid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "example.com")}");
	Console.WriteLine($"UUIDv5:{UuidGenerator.GenerateUuidV5(Guid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "example.com")}");
	}
	}

逐行讲解:
1.UUIDv1实现:
1.时间戳从1582年10月15日开始计算(UUID的标准起始时间);
2.获取MAC地址作为唯一性标识,如果获取不到用随机数代替;
3.设置版本号(第7字节高4位为1)和变体(第9字节高2位为10);
2.UUIDv4实现:直接用Guid.NewGuid(),底层用安全随机数生成,避免了UUIDv1的MAC地址泄露问题;
3.UUIDv3/v5实现:基于名称和命名空间生成,比如同一个命名空间和名称生成的UUIDv5是固定的,适合需要根据名称生成唯一ID的场景(比如URL的唯一标识)。
UUID的优缺点与适用场景

版本 优点 缺点 适用场景
UUIDv1 趋势递增,数据库索引性能较好 泄露MAC地址,有安全风险;依赖系统时间 内部系统,不需要考虑安全的场景
UUIDv4 安全,不泄露任何信息;生成速度快 随机字符串,数据库索引碎片化严重,查询性能差 不需要递增的场景,比如日志ID、临时文件ID
UUIDv3/v5 基于名称生成,可重复生成相同的UUID 生成速度慢(需要哈希计算);不是递增的 需要根据名称生成唯一ID的场景,比如URL、文档ID

我踩过的坑:用UUIDv4作为订单ID,结果数据库的订单表索引碎片化严重,查询订单列表的时间从100ms变成了5s——后来换成雪花算法的递增ID,查询时间回到了100ms!
三、雪花算法:分布式ID的“黄金标准”,适合高并发递增场景
雪花算法(Snowflake)是Twitter开源的分布式ID生成算法,核心是“64位整数=时间戳(41位)+工作机器ID(10位)+序列号(12位)”,保证ID的全局唯一、趋势递增、高性能生成。
大白话解释:把雪花算法比作“全球唯一的快递单号”
1.时间戳(41位):从项目启动时间开始计算的毫秒数,比如可以用2024年1月1日作为起始时间,41位可以表示约69年的时间(2^41-1毫秒≈69年);
2.工作机器ID(10位):可以分成数据中心ID(5位)+机器ID(5位),最多支持32个数据中心,每个数据中心32台机器,总共1024台机器;
3.序列号(12位):同一毫秒内的并发序列号,最多支持4096个并发ID(2^12-1=4095)。
我踩过的坑:用雪花算法的时候,一开始没处理时钟回拨的问题,结果服务器时钟回拨了1分钟,生成的ID比之前的小,导致数据库索引排序混乱——后来加了时钟回拨检测,当发现时钟回拨时,等待时钟同步或者抛出异常!
实战2:C#实现生产级雪花算法
csharp

	using System;
	using System.Threading;
	
	namespace DistributedId.Snowflake;
	
	public class SnowflakeIdGenerator
	{
	// 雪花算法结构参数
	private const int TimestampBits = 41;
	private const int WorkerIdBits = 10;
	private const int SequenceBits = 12;
	
	// 最大取值计算
	private const long MaxWorkerId = (1L << WorkerIdBits) - 1; // 2^10-1=1023
	private const long MaxSequence = (1L << SequenceBits) - 1; // 2^12-1=4095
	
	// 移位偏移量
	private const int WorkerIdShift = SequenceBits; // 12位
	private const int TimestampShift = SequenceBits + WorkerIdBits; // 22位
	
	// 起始时间戳(2024-01-01 00:00:00 UTC,毫秒)
	private const long EpochTimestamp = 1704067200000L;
	
	// 静态变量,保证全局唯一
	private static long _lastTimestamp = -1L;
	private static long _sequence = 0L;
	private static readonly object _lockObj = new object();
	
	// 工作机器ID
	public long WorkerId { get; }
	
	/// <summary>
	/// 初始化雪花算法生成器
	/// </summary>
	/// <param name="workerId">工作机器ID(0-1023)</param>
	/// <exception cref="ArgumentException">工作机器ID超出范围</exception>
	public SnowflakeIdGenerator(long workerId)
	{
	if (workerId < 0 || workerId > MaxWorkerId)
	throw new ArgumentException($"工作机器ID必须在0-{MaxWorkerId}之间", nameof(workerId));
	
	WorkerId = workerId;
	Console.WriteLine($"雪花算法生成器初始化成功,工作机器ID:{workerId},起始时间戳:{EpochTimestamp}");
	}
	
	/// <summary>
	/// 生成下一个分布式ID
	/// </summary>
	/// <returns>64位分布式ID</returns>
	/// <exception cref="InvalidOperationException">时钟回拨异常</exception>
	public long NextId()
	{
	lock (_lockObj) // 线程安全,保证同一毫秒内序列号递增
	{
	long currentTimestamp = GetCurrentTimestamp();
	
	// 处理时钟回拨:如果当前时间戳小于上次生成ID的时间戳
	if (currentTimestamp < _lastTimestamp)
	{
	// 允许的最大时钟回拨时间(比如5秒),超过则抛出异常
	const long MaxClockBackwardMs = 5000;
	if (_lastTimestamp - currentTimestamp > MaxClockBackwardMs)
	throw new InvalidOperationException($"时钟回拨异常:当前时间戳 {currentTimestamp} 小于上次时间戳 {_lastTimestamp},超过允许的最大回拨时间 {MaxClockBackwardMs}ms");
	
	// 等待时钟同步,直到当前时间戳大于等于上次时间戳
	while (currentTimestamp < _lastTimestamp)
	{
	Thread.Sleep(1);
	currentTimestamp = GetCurrentTimestamp();
	}
	}
	
	// 同一毫秒内,序列号递增
	if (currentTimestamp == _lastTimestamp)
	{
	_sequence = (_sequence + 1) & MaxSequence; // 用位运算代替取模,性能更高
	// 序列号溢出:同一毫秒内生成的ID超过4096个
	if (_sequence == 0)
	{
	// 等待下一个毫秒
	currentTimestamp = WaitNextMillis(_lastTimestamp);
	}
	}
	else
	{
	// 不同毫秒,序列号重置为0
	_sequence = 0L;
	}
	
	_lastTimestamp = currentTimestamp;
	
	// 组装ID:时间戳左移22位 + 工作机器ID左移12位 + 序列号
	return ((currentTimestamp - EpochTimestamp) << TimestampShift) |
	(WorkerId << WorkerIdShift) |
	_sequence;
	}
	}
	
	/// <summary>
	/// 获取当前时间戳(毫秒)
	/// </summary>
	private long GetCurrentTimestamp()
	{
	// 用UTC时间,避免时区问题
	return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
	}
	
	/// <summary>
	/// 等待下一个毫秒
	/// </summary>
	private long WaitNextMillis(long lastTimestamp)
	{
	long currentTimestamp = GetCurrentTimestamp();
	while (currentTimestamp <= lastTimestamp)
	{
	Thread.Sleep(1);
	currentTimestamp = GetCurrentTimestamp();
	}
	return currentTimestamp;
	}
	
	/// <summary>
	/// 解析ID为各个组成部分
	/// </summary>
	public (long Timestamp, long WorkerId, long Sequence) ParseId(long id)
	{
	long sequence = id & MaxSequence;
	long workerId = (id >> WorkerIdShift) & MaxWorkerId;
	long timestamp = (id >> TimestampShift) + EpochTimestamp;
	return (timestamp, workerId, sequence);
	}
	}
	
	// 测试代码:模拟10个并发线程生成ID
	class Program
	{
	static void Main(string[] args)
	{
	var idGenerator = new SnowflakeIdGenerator(123); // 工作机器ID123
	var threads = new Thread[10];
	var idSet = new HashSet<long>();
	var lockObj = new object();
	
	for (int i = 0; i < 10; i++)
	{
	threads[i] = new Thread(() =>
	{
	for (int j = 0; j < 1000; j++)
	{
	long id = idGenerator.NextId();
	lock (lockObj)
	{
	if (!idSet.Add(id))
	{
	Console.WriteLine($"发现重复ID:{id}");
	Environment.Exit(1);
	}
	if (j % 100 == 0)
	{
	var (timestamp, workerId, sequence) = idGenerator.ParseId(id);
	Console.WriteLine($"生成ID:{id},时间戳:{timestamp},工作机器ID:{workerId},序列号:{sequence}");
	}
	}
	}
	});
	threads[i].Start();
	}
	
	foreach (var thread in threads)
	{
	thread.Join();
	}
	
	Console.WriteLine($"成功生成10000个唯一ID,无重复");
	}
	}

逐行讲解:
1.参数配置:定义时间戳、工作机器ID、序列号的位数,计算最大取值和移位偏移量;
2.起始时间戳:用项目启动时间作为起始时间,避免时间戳过长,延长算法的使用年限;
3.线程安全:用lock保证同一毫秒内序列号的原子递增,避免多线程下的序列号重复;
4.时钟回拨处理:检测到时钟回拨时,等待时钟同步,超过允许的最大回拨时间则抛出异常;
5.序列号溢出处理:同一毫秒内生成的ID超过4096个时,等待下一个毫秒;
6.ID解析:把生成的ID解析为时间戳、工作机器ID、序列号,方便排查问题。
雪花算法的优缺点与适用场景
优点 缺点 适用场景
全局唯一,趋势递增,数据库索引性能好 依赖系统时间,时钟回拨会导致ID重复;需要配置工作机器ID 电商订单ID、用户ID、支付ID等高并发递增场景
生成速度快(每秒生成百万级ID);无依赖(不需要中间件) 64位整数,有些系统不支持(比如某些老数据库) 分布式系统、微服务、分库分表场景
可解析,方便排查问题(通过ID可以知道生成时间和机器) 工作机器ID需要手动配置,容易重复 需要高性能、高可靠ID生成的场景
我踩过的坑:配置工作机器ID的时候,把两台服务器的ID设成了一样,结果生成的ID重复了,导致订单冲突——后来改成用环境变量配置工作机器ID,部署的时候自动分配,避免了手动配置的错误!
四、拓展知识:其他分布式ID生成方案对比

方案 核心原理 优点 缺点 适用场景
数据库自增ID 用数据库的AUTO_INCREMENT生成 实现简单,递增,索引性能好 单点故障;分库分表时ID重复;性能低(每秒生成几千个ID) 小型系统,不需要分库分表的场景
Redis自增ID 用Redis的INCR命令生成 性能高(每秒生成十万级ID);递增;支持分布式 依赖Redis;Redis挂了会导致ID生成失败;需要持久化避免数据丢失 中型系统,分库分表场景
美团Leaf 基于雪花算法+数据库自增ID 支持雪花算法和号段模式;高可用;性能高 实现复杂;依赖数据库和ZooKeeper 大型互联网公司,高并发场景
百度UidGenerator 基于雪花算法,优化了时钟回拨和工作机器ID 高性能;自动配置工作机器ID;处理时钟回拨 实现复杂;依赖数据库 大型互联网公司,高并发场景
阿里云ID生成服务 云服务提供的ID生成 高可用;高性能;不需要自己维护 付费;依赖云服务 云原生系统,不想自己维护ID生成服务的场景

我踩过的坑:用Redis自增ID作为订单ID,结果Redis主从切换的时候,主节点的自增ID还没同步到从节点,导致从节点生成的ID重复——后来换成雪花算法,不需要依赖中间件,避免了Redis单点故障的问题!
五、生产级分布式ID最佳实践与踩坑总结

  1. 生产级最佳实践
    优先选雪花算法:大部分分布式系统的首选,性能高、递增、唯一;
    避免用UUIDv4作为业务ID:数据库索引性能差,适合作为非业务ID(比如日志ID);
    配置工作机器ID要自动化:用环境变量、ZooKeeper、K8s的Pod IP等自动生成工作机器ID,避免手动配置错误;
    处理时钟回拨:设置允许的最大回拨时间(比如5秒),超过则抛出异常,避免ID重复;
    测试唯一性和性能:用多线程测试生成100万ID,检查是否有重复;测试每秒生成ID的数量,保证满足业务需求;
    备份起始时间戳:把起始时间戳存在配置文件中,避免代码更新后起始时间戳变化,导致ID重复。
  2. 我踩过的坑总结
    1.用UUIDv4作为订单ID:数据库索引碎片化严重,查询性能下降80%——换成雪花算法的递增ID;
    2.工作机器ID手动配置重复:两台服务器的ID一样,生成的ID重复,导致订单冲突——改成自动配置;
    3.没处理时钟回拨:服务器时钟回拨1分钟,生成的ID比之前的小,导致数据库索引排序混乱——加了时钟回拨检测;
    4.起始时间戳硬编码:代码更新后起始时间戳变了,导致生成的ID重复——把起始时间戳存在配置文件中;
    5.用数据库自增ID做分库分表:两个库的ID重复,导致用户ID冲突——换成雪花算法。
    六、总结
    UUID适合不需要递增的场景,比如日志ID、临时文件ID;雪花算法适合高并发递增场景,比如订单ID、用户ID,是分布式ID的“黄金标准”。实战中要根据业务场景选择合适的ID生成方案,注意处理时钟回拨、工作机器ID配置、唯一性测试等问题,避免踩坑。如果不想自己实现雪花算法,可以用成熟的库,比如Snowflake.Net,或者用云服务提供的ID生成服务,稳定性更高。
    下一节我们会学习分布式缓存(Redis、Memcached),解决分布式系统中的数据缓存问题,提升系统性能。

转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49576.html


相关教程