c# 默认 guid.newguid() 生成无序 uuid v4,导致数据库聚集索引严重碎片化、插入性能骤降;应改用时间有序 uuid(如 v6/v7 或 sequential guid),并以 binary(16) 存储,同时注意时钟精度与分布式时序一致性。

直接说结论:C# 默认的 Guid.NewGuid() 生成的是随机 UUID(v4),用作数据库主键会导致聚集索引严重碎片化、插入性能断崖式下降;要提升性能,必须改用**时间有序的 UUID**,比如 v6/v7 或 Windows 原生的 sequential GUID —— 但注意,.NET 标准库不自带 v6/v7,得靠第三方库或手动构造。
为什么 Guid.NewGuid() 会让数据库变慢
InnoDB(以及 SQL Server 的聚集索引)按主键物理顺序存放数据。而 Guid.NewGuid() 生成的 128 位值完全随机,每次插入都可能落在索引页中间,触发频繁的页分裂和随机 I/O。实测在高并发写入场景下,TPS 可能比自增 ID 低 3–5 倍,且磁盘碎片率快速升至 70%+。
- 现象:SQL Server 中
sys.dm_db_index_physical_stats显示avg_fragmentation_in_percent持续 > 30% - 现象:INSERT 延迟毛刺明显,监控中 Page Splits/sec 指标飙升
- 关键点:问题不在 GUID 本身,而在“无序”——只要保证高位字节随时间递增,就能缓解
三种可用的有序 GUID 实现方式
不是所有“有序”都等价,兼容性、精度、跨平台能力差异很大:
-
Windows 原生 sequential GUID:
UuidCreateSequential(通过 P/Invoke 调用 rpcrt4.dll),前 6 字节含时间戳+MAC 地址,但仅限 Windows,且从 Windows 2000 起已被标记为 legacy,新部署不推荐 -
UUID v6 / v7(RFC 9562):v6 把 UUID v1 时间戳前置重排,v7 直接用毫秒级 Unix 时间戳开头,真正跨平台、可预测、趋势递增;需引入
UUIDNext或Ulid(注意 ULID 是 128 位但非 RFC 标准 UUID) -
手搓时间前置 GUID:如知识库中
GuidEx.GenerateOrderly(),把 DateTime.Now 毫秒拆成字节数组覆盖 GUID 前 6 字节,简单有效,但需自行保证线程安全和时钟回拨容忍
存储时别存字符串,用二进制(byte[16])
即使用了有序 UUID,如果存成 char(36) 字段,索引体积仍比 binary(16) 大 125%,二级索引会额外膨胀,查询性能白优化一半。
- SQL Server:字段类型用
uniqueidentifier(它原生存二进制,显示为字符串只是客户端格式化) - MySQL:用
BINARY(16),插入时用UNHEX(REPLACE(@uuid,'-',''))或 ORM 映射为byte[] - EF Core 示例:
modelBuilder.Entity<t>().Property(e => e.Id).HasColumnType("binary(16)");</t> - 切记:
Guid.ToString("N")是 32 字符无横线字符串,Guid.ToByteArray()才是 16 字节原始数据
最容易被忽略的坑:时钟精度与分布式节点冲突
v6/v7 和手搓方案都依赖本地时间,但 Windows DateTime.Now 默认只有 15ms 精度,高并发下同一毫秒内生成多个 UUID 仍可能乱序;跨服务器部署时若 NTP 不稳,时间回拨会导致 UUID 倒流,破坏聚集索引局部性。
- 解决方案:v7 规范要求使用
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),精度达毫秒级;再配合 72 位随机数防碰撞 - 更稳做法:在 v7 基础上,用
Interlocked.Increment(ref _counter) & 0x7FFFFF补充序列号,确保单机同毫秒不重复 - 终极提醒:不要在容器环境(如 Docker)里直接用 host 时间,务必挂载
/etc/timezone并启用 chrony/NTP 同步










