在一个分布式环境中,我们习惯使用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不稳定会影响系统;在增加数据库后,需要大批量移动数据,且需要成倍增加DB

  方式二,按范围分片

  思路:每台服务器负责一个号段,不够用了就增加服务器,Redis初始化当前最大ID值,之后进行自增,分布式数据访问层根据号段进行路由

  优点:增加数据库可以不迁移数据,可以一个一个的增加数据库

  缺点:需要尽量保证Redis和数据库的一致性;Redis不稳定会影响系统;数据分布严重不均匀,严重的热点问题

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 = ; /// <summary>
/// 数据中心Id占用5位
/// </summary>
const int DatacenterIdBits = ; /// <summary>
/// 序列号占用12位
/// </summary>
const int SequenceBits = ; /// <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 < )
{
throw new ArgumentException(String.Format("worker Id can't be greater than {0} or less than 0", MaxWorkerId));
}
if (datacenterId > MaxDatacenterId || datacenterId < )
{
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 + ) & SequenceMask;
//毫秒内序列溢出
if (_sequence == )
{
//阻塞到下一个毫秒,获得新的时间戳
timestamp = TilNextMillis(_lastTimestamp);
}
} //时间戳改变,毫秒内序列重置
else
{
_sequence = ;
} //上次生成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(, , , , , , 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(, );
for (int i = ; i < ; i++)
{
Trace.WriteLine(string.Format("{0}-{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:ffffff"), idWorker.NextId()));
}
}
}

  结果:

  总之,GUID能满足大部分需要,但如果想要我们的程序精益求精,也可以考虑使用本文提到的方法,感谢阅读。

GUID做主键真的合适吗的更多相关文章

  1. 使用Guid做主键和int做主键性能比较

    使用Guid做主键和int做主键性能比较 在数据库的设计中我们常常用Guid或int来做主键,根据所学的知识一直感觉int做主键效率要高,但没有做仔细的测试无法 说明道理.碰巧今天在数据库的优化过程中 ...

  2. SQLSERVER如何使用递增排序的GUID做主键

    场景: 产品表数据量较大想用Guid做表的主键,并在此字段上建立聚簇索引. 因为Guid是随机生成的,生成的值大小是不确定的,每次生成的数可能很大,也可能很小.这样会影响插入的效率 1.NEWSEQU ...

  3. int 和guid做主键的时候性能的区别

    1.在经常需要做数据迁移的系统中,建议用Guid.并且在相应的外键字段,也就是用来做连接查询的字段添加非聚集索引,对于改善性能有极大的好处.where条件的字段也可以适当添加非聚集索引. 2.在使用G ...

  4. SQL GUID和自增列做主键的优缺点

    我们公司的数据库全部是使用GUID做主键的,很多人习惯使用int做主键.所以呢,这里总结一下,将两种数据类型做主键进行一个比较. 使用INT做主键的优点: 1.需要很小的数据存储空间,仅仅需要4 by ...

  5. Guid和Sequence做主键的比较

    记得A项目组是一个物流管理系统,后台采用了Oracle数据库.在系统中的核心表托运单表中,关于主键采用何种数据类型,是 sequence 还是用GUID , 大家起了争论. 从网络搜索得到的结论看,一 ...

  6. (转)mysql中InnoDB表为什么要建议用自增列做主键

    InnoDB引擎表的特点 1.InnoDB引擎表是基于B+树的索引组织表(IOT) 关于B+树 (图片来源于网上) B+ 树的特点: (1)所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关 ...

  7. 开发反模式(GUID) - 伪键洁癖

    一.目标:整理数据 有的人有强迫症,他们会为一系列数据的断档而抓狂. 一方面,Id为3这一行确实发生过一些事情,为什么这个查询不返回Id为3的这一行?这条记录数据丢失了吗?那个Column到底是什么? ...

  8. 扩展ASP.NET Identity使用Int做主键

    当我们默认新建一个ASP.NET MVC项目的时候,使用的身份认证系统是ASP.NET Identity.但是这里的Identity使用的主键为String类型的GUID.当然这是大多数系统首先类型. ...

  9. mysql中InnoDB表为什么要建议用自增列做主键

    InnoDB引擎表的特点 1.InnoDB引擎表是基于B+树的索引组织表(IOT) 关于B+树 (图片来源于网上) B+ 树的特点: (1)所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关 ...

随机推荐

  1. redis 发布与订阅原理分析

    前言:用了redis也有一段时间了,但是发布与订阅的使用频率也不高,趁着这次空闲,深究下redis的发布与订阅模式. 一.订阅频道和信息发布 功能说明:Redis 的 SUBSCRIBE 命令可以让客 ...

  2. pytest自定义动态添加描述信息

    先上效果图: 修改pytest-html报告,分三部分. pytest执行目录新建conftest.py文件 import pytest from py._xmlgen import html fro ...

  3. Java简单公式计算器

    最近给公司开发业务代码时,碰到一个场景,简单描述是这样的: 客户要向咱们公司定制一件产品,这个产品呢,有很多属性,那公司得根据这些属性报价呀,怎么报价呢?公司针对某种类型的产品有一个基准价,在同类产品 ...

  4. 优化 Ubuntu

    优化Ubuntu 1. 更换 apt 源 echo 'deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe mul ...

  5. Flutter学习笔记(12)--列表组件

    如需转载,请注明出处:Flutter学习笔记(12)--列表组件 在日常的产品项目需求中,经常会有列表展示类的需求,在Android中常用的做法是收集数据源,然后创建列表适配器Adapter,将数据源 ...

  6. docker部署xxl-job 通用反射执行器

    原因 最近在公司写一些job,公司使用的是spring boot提供的注解形式实现的. 这样在自测的时候很麻烦,而且测试提测的时候需要修改cron表达式->提交git->jenkins打包 ...

  7. css3系列之transform 详解rotate

    rotate rotateX rotateY rotateZ rotate3d rotate: 旋转该元素,配合着transform-origin属性,transform-origin 是设置旋转点的 ...

  8. 从源码看java线程状态

    关于java线程状态,网上查资料很混乱,有的说5种状态,有的说6种状态,初学者搞不清楚这个线程状态到底是怎么样的,今天我讲一下如何看源码去解决这个疑惑. 直接上代码: public class Thr ...

  9. leetcode并发题目解题报告JAVA版

    一.Print in Order Suppose we have a class: public class Foo { public void first() { print("first ...

  10. Android 虹软人脸识别SDK-人脸对比

    准备 : 登录官方网站,获取SDK,进行个人验证后新建项目,获取APP_ID,和SDK_KEY: https://ai.arcsoft.com.cn/ucenter/resource/build/in ...