在一个分布式环境中,我们习惯使用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. Java map笔记

    Map 是一个键值对的集合 花和尚 豹子头 鲁智深 林冲 如果想要从map中获得值,可以根据键 Map<Key,velue> Map虽然是集合,但是和collection的接口无关 我们可 ...

  2. C语言入门6-选择结构--f语句-switch

    一.     什么是选择结构? 选择结构,也称为分支结构!! 选择结构就是根据    给定的判定条件,判断结果, 并根据  判断的结果   来控制程序的流程 (流程图中,   菱形框 是有来判断的 , ...

  3. css inline-block 水平居中

    给父元素添加text-align: center即可. body { text-align: center; background-color: black; } #outer { margin: 1 ...

  4. 在windows中使用 nvm 实现node多版本管理

    所谓 NVM 就是 Node Version Manager 的缩写,即 node的版本管理工具,我们可以在电脑中安装多个不同版本的node,并借由 NVM 来实现自由切换,详情可点击查看 NVM 官 ...

  5. layer设置maxWidth及maxHeight解决方案

    layer介绍 layer是一款近年来备受青睐的web弹层组件,她具备全方位的解决方案,致力于服务各水平段的开发人员,您的页面会轻松地拥有丰富友好的操作体验.下载及使用访问官方网站. area属性 l ...

  6. 计数排序and基数排序

    1 计数排序,稳定    复杂度o(k + n) public static int[] countingSort(int[] nums) { int n = nums.length; ; ; i & ...

  7. 内容汇总(c语言)

    一,内容 常量(整型,浮点型,字符型,字符串型,符号常量) 变量(基本类型:整形,浮点型,字符型,枚举型:构造类型:数组,结构体,共用体:另外还有指针类型和NULL) 顺序结构 分支结构 循环结构 当 ...

  8. 3. 源码分析---SOFARPC客户端服务调用

    我们首先看看BoltClientProxyInvoker的关系图 所以当我们用BoltClientProxyInvoker#invoke的时候实际上是调用了父类的invoke方法 ClientProx ...

  9. java+springBoot+Thymeleaf+vue分页组件的定义

    导读 本篇着重介绍java开发环境下,如何写一个vue分页组件,使用到的技术点有java.springBoot.Thymeleaf等: 分页效果图 名称为vuepagerbasic的分页组件,只包含上 ...

  10. Serilog 自定义Enricher 来增加记录的信息

    Serilog 自定义Enricher 来增加记录的信息 Intro Serilog 是 .net 里面非常不错的记录日志的库,结构化日志记录,而且配置起来很方便,自定义扩展也很方便 Serilog ...