在一个分布式环境中, 我们习惯使用 GUID 做主键, 来保证全局唯一, 然后, GUID 做主键真的合适吗?
其实 GUID 做主键本身没有问题, 微软的很多项目自带 DB 都是使用 GUID 做主键的, 显然, 这样做是没有问题的. 然而, SQL Server 默认会将主键设置为聚集索引, 使用 GUID 做聚集索引就有问题了. 很多时候程序员容易接受 SQL Server 这一默认设置, 但无序 GUID 做聚集索引显然是低效的.
那么, 我们在项目中如何避免这一问题呢?
主要的思路还是两方面 -- 方案一, 选择合适的列作为聚集索引; 方案二, 使用有序的主键.
1 方案一, 选择合适的列做聚集索引
选择原则很简单 -- 字段值尽量唯一, 字段占用字节尽量小, 字段值很少修改, 字段多用于查询范围数据或排序的数据.
之所以是根据以上原则选择, 主要还是基于 B + 树数据索引问题, 这部分内容都比较基础, 这里就不举例验证了, 以上原则还是比较公认的, 即便读者不太理解其中原理, 也请记住这一选择规则.
常见的备选项 -- 自增列 (Id) 和时间列(CreateTime).
聚集索引的最大用处就是帮助范围查询快速定位, 从而减小数据库 IO 的消耗来提升查询效率. 对于范围查询我们更多的应用在自增列和时间列上, 因为这两列本身反应了数据的创建顺序, 符合多数范围查询的场景需要.
大部分时候, 我们仍然可以使用 GUID 做主键, 只需要重新设置聚集索引就行.
2 方案二, 有序的主键
对于一个分布式环境, 保证唯一和有序性, 实际上有多种方法, 各有利弊.
2.1 分布式数据库
对于分布式数据库, 简单使用自增主键即可, 比如 Tidb.
TiDB 中, 自增列只保证自增且唯一, 并不保证连续分配. TiDB 目前采用批量分配 ID 的方式, 所以如果在多台 TiDB 上同时插入数据, 分配的自增 ID 会不连续. TiDB 实现自增 ID 的原理是每个 tidb-server 实例缓存一段 ID 值用于分配(目前会缓存 30000 个 ID), 用完这段值再去取下一段.
优点: 简单好用
缺点: 不能设置 ID, 需要使用数据库的; ID 不保证连续分配, 也无法根据 ID 来判断数据创建的先后; 负载不均匀, 有数据热点问题
2.2 基于 Redis 等中间件的
根据数据库分片方式不同, 又有两种情形.
方式一, 取模分片
思路: Redis 初始化当前最大 ID 值, 之后进行自增, 分布式数据访问层根据取模进行路由
优点: 数据库负载比较均匀
缺点: 需要尽量保证 Redis 和数据库的一致性; Redis 不稳定会影响系统, 可能会出现重复 ID 在插入数据库, 主键重复会抛出异常; 在增加数据库后, 需要大批量移动数据, 且需要成倍增加 DB
方式二, 按范围分片
思路: 每台服务器负责一个号段, 不够用了就增加服务器, Redis 初始化当前最大 ID 值, 之后进行自增, 分布式数据访问层根据号段进行路由
优点: 增加数据库可以不迁移数据, 可以一个一个的增加数据库
缺点: 需要尽量保证 Redis 和数据库的一致性; Redis 不稳定会影响系统, 可能会出现重复 ID 在插入, 主键重复会抛出异常; 数据分布严重不均匀, 严重的热点问题
2.3 基于算法实现
这里介绍下 Twitter 的 Snowflake 算法 --snowflake, 它把时间戳, 工作机器 id, 序列号组合在一起, 以保证在分布式系统中唯一性和自增性.
snowflake 生成的 ID 整体上按照时间自增排序, 并且整个分布式系统内不会产生 ID 碰撞, 在同一毫秒内最多可以生成 1024 X 4096 = 4194304 个全局唯一 ID.
优点: 不依赖数据库, 完全内存操作速度快
缺点: 不同服务器需要保证系统时钟一致
snowflake 的 C# 版本的简单实现:
- public class SnowflakeIdWorker
- {
- /// <summary>
- /// 开始时间截
- /// 1288834974657 是(Thu, 04 Nov 2010 01:42:54 GMT) 这一时刻到 1970-01-01 00:00:00 时刻所经过的毫秒数.
- /// 当前时刻减去 1288834974657 的值刚好在 2^41 里, 因此占 41 位.
- /// 所以这个数是为了让时间戳占 41 位才特地算出来的.
- /// </summary>
- public const long Twepoch = 1288834974657L;
- /// <summary>
- /// 工作节点 Id 占用 5 位
- /// </summary>
- const int WorkerIdBits = 5;
- /// <summary>
- /// 数据中心 Id 占用 5 位
- /// </summary>
- const int DatacenterIdBits = 5;
- /// <summary>
- /// 序列号占用 12 位
- /// </summary>
- const int SequenceBits = 12;
- /// <summary>
- /// 支持的最大机器 Id, 结果是 31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
- /// </summary>
- const long MaxWorkerId = -1L ^ (-1L <<WorkerIdBits);
- /// <summary>
- /// 支持的最大数据中心 Id, 结果是 31
- /// </summary>
- const long MaxDatacenterId = -1L ^ (-1L <<DatacenterIdBits);
- /// <summary>
- /// 机器 ID 向左移 12 位
- /// </summary>
- private const int WorkerIdShift = SequenceBits;
- /// <summary>
- /// 数据标识 id 向左移 17 位(12+5)
- /// </summary>
- private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
- /// <summary>
- /// 时间截向左移 22 位(5+5+12)
- /// </summary>
- public const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
- /// <summary>
- /// 生成序列的掩码, 这里为 4095 (0b111111111111=0xfff=4095)
- /// </summary>
- private const long SequenceMask = -1L ^ (-1L <<SequenceBits);
- /// <summary>
- /// 毫秒内序列(0~4095)
- /// </summary>
- private long _sequence = 0L;
- /// <summary>
- /// 上次生成 Id 的时间截
- /// </summary>
- private long _lastTimestamp = -1L;
- /// <summary>
- /// 工作节点 Id
- /// </summary>
- public long WorkerId { get; protected set; }
- /// <summary>
- /// 数据中心 Id
- /// </summary>
- public long DatacenterId { get; protected set; }
- /// <summary>
- /// 构造器
- /// </summary>
- /// <param name="workerId">工作 ID (0~31)</param>
- /// <param name="datacenterId">数据中心 ID (0~31)</param>
- public SnowflakeIdWorker(long workerId, long datacenterId)
- {
- WorkerId = workerId;
- DatacenterId = datacenterId;
- if (workerId> MaxWorkerId || workerId <0)
- {
- throw new ArgumentException(String.Format("worker Id can't be greater than {0} or less than 0", MaxWorkerId));
- }
- if (datacenterId> MaxDatacenterId || datacenterId <0)
- {
- throw new ArgumentException(String.Format("datacenter Id can't be greater than {0} or less than 0", MaxDatacenterId));
- }
- }
- private static readonly object _lockObj = new Object();
- /// <summary>
- /// 获得下一个 ID (该方法是线程安全的)
- /// </summary>
- /// <returns></returns>
- public virtual long NextId()
- {
- lock (_lockObj)
- {
- // 获取当前时间戳
- var timestamp = TimeGen();
- // 如果当前时间小于上一次 ID 生成的时间戳, 说明系统时钟回退过这个时候应当抛出异常
- if (timestamp <_lastTimestamp)
- {
- throw new InvalidOperationException(String.Format(
- "Clock moved backwards. Refusing to generate id for {0} milliseconds", _lastTimestamp - timestamp));
- }
- // 如果是同一时间生成的, 则进行毫秒内序列
- if (_lastTimestamp == timestamp)
- {
- _sequence = (_sequence + 1) & SequenceMask;
- // 毫秒内序列溢出
- if (_sequence == 0)
- {
- // 阻塞到下一个毫秒, 获得新的时间戳
- timestamp = TilNextMillis(_lastTimestamp);
- }
- }
- // 时间戳改变, 毫秒内序列重置
- else
- {
- _sequence = 0;
- }
- // 上次生成 ID 的时间截
- _lastTimestamp = timestamp;
- // 移位并通过或运算拼到一起组成 64 位的 ID
- return ((timestamp - Twepoch) << TimestampLeftShift) |
- (DatacenterId << DatacenterIdShift) |
- (WorkerId << WorkerIdShift) | _sequence;
- }
- }
- /// <summary>
- /// 生成当前时间戳
- /// </summary>
- /// <returns > 毫秒</returns>
- private static long GetTimestamp()
- {
- return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
- }
- /// <summary>
- /// 生成当前时间戳
- /// </summary>
- /// <returns > 毫秒</returns>
- protected virtual long TimeGen()
- {
- return GetTimestamp();
- }
- /// <summary>
- /// 阻塞到下一个毫秒, 直到获得新的时间戳
- /// </summary>
- /// <param name="lastTimestamp">上次生成 Id 的时间截</param>
- /// <returns></returns>
- protected virtual long TilNextMillis(long lastTimestamp)
- {
- var timestamp = TimeGen();
- while (timestamp <= lastTimestamp)
- {
- timestamp = TimeGen();
- }
- return timestamp;
- }
- }
测试:
- [TestClass]
- public class SnowflakeTest
- {
- [TestMethod]
- public void MainTest()
- {
- SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
- for (int i = 0; i < 1000; i++)
- {
- Trace.WriteLine(string.Format("{0}-{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:ffffff"), idWorker.NextId()));
- }
- }
- }
结果:
来源: https://www.cnblogs.com/MeteorSeed/p/11413201.html