解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
8.45 KiB
X
# Snowflake 雪花算法使用手册 本文档基于源码 `NewLife.Core/Data/Snowflake.cs` 与测试 `XUnitTest.Core/Data/SnowflakeTests.cs`,用于说明 `Snowflake`(雪花算法分布式 Id 生成器)的设计、用法与注意事项。 > 关键词:单例、WorkerId、时间戳、序列号、时间回拨、集群分配(Redis)。 --- ## 1. 概述 `Snowflake` 用一个 64 bit 的 `Int64` 作为全局唯一 Id。 位分配: - 1 bit:保留(符号位) - 41 bit:时间戳(毫秒) - 10 bit:工作节点(`WorkerId`,0~1023) - 12 bit:序列号(0~4095) 生成的 Id 具备以下特点: - 大体趋势自增(按时间推进) - 同一毫秒内可生成最多 4096 个 Id - **在同一个 `Snowflake` 实例内**可保证不重复 重要约束: - **业务内必须确保单例**。若并发场景存在多个 `Snowflake` 实例,并且 `WorkerId` 相同,则可能产生重复 Id。 --- ## 2. 核心概念 ### 2.1 `StartTimestamp`(起始时间戳) - 属性:`public DateTime StartTimestamp { get; set; }` - 默认值:`UTC 1970-01-01` 转为本地时间(`DateTimeKind.Local`) 语义要点: - `Snowflake` 会把参与计算的时间转换到 `StartTimestamp` 所属时区,然后做差得到毫秒数。 - 默认使用本地时间,是为了方便解析雪花 Id 时直接得到本地时间,并最大兼容已有业务(尤其是按本地日期分表/分区的场景)。 使用建议: - `StartTimestamp` **必须在首次调用 `NewId()` 前设置**,首次生成后再修改不会影响已初始化实例(因为初始化过程只做一次)。 ### 2.2 `WorkerId`(工作节点 Id) - 属性:`public Int32 WorkerId { get; set; }` - 范围:0~1023(10 位) 说明: - 在分布式系统内,`WorkerId` 的全局唯一性决定了跨节点是否会产生重复 Id。 - 仅依靠默认算法(IP/进程/线程)**无法绝对保证唯一**,高要求场景建议外部显式分配。 ### 2.3 `Sequence`(序列号) - 属性:`public Int32 Sequence => _sequence;` - 范围:0~4095(12 位) 说明: - 同一毫秒内通过递增序列号保证唯一。 - 序列溢出(超过 4095)时,算法会逻辑上推进到下一毫秒继续生成。 --- ## 3. WorkerId 初始化优先级 `Snowflake` 在首次生成 Id 时会自动执行一次初始化(`Initialize()`),按优先级决定 `WorkerId`: 1. 如果实例 `WorkerId > 0`:使用实例值 2. 否则如果 `Snowflake.GlobalWorkerId > 0`:使用 `GlobalWorkerId & 1023` 3. 否则如果 `Snowflake.Cluster != null`:调用 `JoinCluster(Cluster)` 从集群分配 4. 否则:使用默认算法生成(基于 IP 派生的实例号 + 进程/线程) 注意: - 代码中使用的是判断 `WorkerId <= 0`,因此 **`WorkerId=0` 会被视为“未设置”并继续走后续策略**。若你希望固定为 0,需要自行确保不要触发初始化覆盖(一般不建议)。 --- ## 4. API 速查 ### 4.1 `Int64 NewId()` 基于当前时间生成下一个 Id。 行为要点: - 使用 `DateTime.Now`(本地时间),并通过 `ConvertKind` 转换到 `StartTimestamp` 的时区。 - 处理时间回拨: - 若检测到时间回拨且回拨幅度大于 `MaxClockBack`(约 1 小时 + 10 秒),抛出 `InvalidOperationException` - 否则使用上一次时间戳继续生成(保持单实例唯一性) 适用场景: - 常规业务主键生成(最常用)。 ### 4.2 `Int64 NewId(DateTime time)` 基于指定时间生成 Id(携带当前实例的 `WorkerId` 与序列号)。 注意: - 若你为同一个“指定毫秒时间”生成超过 4096 个 Id,则可能重复(因为序列号只有 12 位,会取模)。 适用场景: - 需要用业务时间构造插入 Id(例如按采集时间落库)。 ### 4.3 `Int64 NewId(DateTime time, Int32 uid)` 基于指定时间生成 Id,使用 `uid` 的低 10 位作为 `WorkerId`(1024 分组),仍保留 12 位序列号。 适用场景: - 物联网数据采集:每 1024 个传感器为一组,每组每毫秒可生成最多 4096 个 Id。 注意: - 若同一分组同一毫秒生成超过 4096 个 Id,则可能重复。 ### 4.4 `Int64 NewId22(DateTime time, Int32 uid)` 基于指定时间生成 Id,使用 22 位业务 Id(`uid & ((1<<22)-1)`),不再保留序列号。 适用场景: - 物联网数据采集:每 4,194,304 个传感器一组,每组每毫秒最多 1 个 Id。 - 常用于配合 upsert:同一毫秒同一传感器写多行数据时只保留一行。 注意: - 同一业务 id 在同一毫秒内生成多个 Id 会重复(因为没有序列号)。 ### 4.5 `Int64 GetId(DateTime time)` 把时间转换为“仅包含时间部分”的 Id(不带 `WorkerId` 与序列号)。 适用场景: - 构造时间片段查询:将时间区间转换为雪花 Id 区间进行范围查询。 ### 4.6 `Boolean TryParse(Int64 id, out DateTime time, out Int32 workerId, out Int32 sequence)` 解析雪花 Id,得到时间、`WorkerId`、序列号。 说明: - 解析得到的 `time` 是 `StartTimestamp` 所属时区的时间。 ### 4.7 `DateTime ConvertKind(DateTime time)` 把输入时间转换为与 `StartTimestamp` 相同的时区,便于相减。 规则: - `time.Kind == DateTimeKind.Unspecified`:直接返回(不做转换) - `StartTimestamp.Kind == Utc`:返回 `time.ToUniversalTime()` - `StartTimestamp.Kind == Local`:返回 `time.ToLocalTime()` --- ## 5. 集群模式:确保 WorkerId 绝对唯一 ### 5.1 `static ICache? Cluster` `Snowflake.Cluster` 用于配置一个缓存实例作为 WorkerId 分配器,建议使用 Redis。 - 当 `Cluster != null` 且实例 `WorkerId` 未显式设置时,初始化阶段会调用 `JoinCluster(Cluster)`。 ### 5.2 `void JoinCluster(ICache cache, String key = "SnowflakeWorkerId")` 通过自增键从集群获取 WorkerId: - `workerId = (Int32)cache.Increment(key, 1)` - `WorkerId = workerId & 1023` 使用建议: - 在多进程/多节点场景,无脑优先采用集群分配 WorkerId。 - 需要区分环境(dev/test/prod)时,建议为不同环境使用不同的 `key`,避免 WorkerId 分配交叉。 --- ## 6. 正确使用姿势 ### 6.1 在应用内保持单例 关键点: - 每个应用/服务内,尽量只创建一个全局 `Snowflake` 实例。 - 若使用 ORM/中间件(例如 XCode),需确保同一张表(或同一业务域)不要各自 new 一个 `Snowflake`。 示例: ```csharp using NewLife.Data; public static class IdGenerator { public static readonly Snowflake Instance = new() { // 如有需要,在首次调用 NewId 前设置 // StartTimestamp = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local), // WorkerId = 1, }; } var id = IdGenerator.Instance.NewId(); ``` ### 6.2 显式指定 WorkerId(推荐) ```csharp var snow = new Snowflake { WorkerId = 1 }; var id = snow.NewId(); ``` ### 6.3 使用集群分配 WorkerId(推荐,分布式场景) ```csharp using NewLife.Caching; using NewLife.Data; Snowflake.Cluster = /* RedisCache 实例 */; var snow = new Snowflake(); var id = snow.NewId(); ``` --- ## 7. 常见问题与坑 ### 7.1 为什么强调“单例”? `Snowflake` 只保证“本实例”生成的 Id 唯一。 - 只要出现多个实例,并且 `WorkerId` 一样,并发下就可能产生重复。 ### 7.2 默认 WorkerId 是否可靠? 默认策略使用 IP 派生实例 Id + 进程/线程信息,目的是降低同机冲突概率,但无法在所有环境下绝对唯一。 - 容器环境、NAT、同一局域网 IP 变化、进程重启等都可能引入冲突。 - 高要求场景强烈建议:显式 WorkerId 或使用 Redis 自增分配。 ### 7.3 时间回拨会发生什么? - 小范围回拨:会沿用上次时间戳继续生成,保持单实例不重复。 - 回拨过大:抛出异常拒绝生成(防止大概率碰撞)。 ### 7.4 使用 `NewId(DateTime time)` 是否一定唯一? 不一定。 - 在同一毫秒内,序列号只有 12 位(4096),超出会取模导致重复风险。 - 这类 API 更适合“按时间落库/分区”的业务需求,而不是高并发写入同一个业务时间点。 --- ## 8. 兼容性说明 - `Snowflake` 属于基础库,面向 `net45` ~ `net10` 多目标框架。 - 算法核心基于 `DateTime`、`Interlocked`、`Volatile`、`ICache` 等通用 API。 --- ## 9. 相关链接 - 在线文档:`https://newlifex.com/core/snow_flake` - 源码:`NewLife.Core/Data/Snowflake.cs` - 单元测试:`XUnitTest.Core/Data/SnowflakeTests.cs`