GUID做主键真的合适吗
在一个分布式环境中,我们习惯使用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做主键真的合适吗的更多相关文章
- 使用Guid做主键和int做主键性能比较
使用Guid做主键和int做主键性能比较 在数据库的设计中我们常常用Guid或int来做主键,根据所学的知识一直感觉int做主键效率要高,但没有做仔细的测试无法 说明道理.碰巧今天在数据库的优化过程中 ...
- SQLSERVER如何使用递增排序的GUID做主键
场景: 产品表数据量较大想用Guid做表的主键,并在此字段上建立聚簇索引. 因为Guid是随机生成的,生成的值大小是不确定的,每次生成的数可能很大,也可能很小.这样会影响插入的效率 1.NEWSEQU ...
- int 和guid做主键的时候性能的区别
1.在经常需要做数据迁移的系统中,建议用Guid.并且在相应的外键字段,也就是用来做连接查询的字段添加非聚集索引,对于改善性能有极大的好处.where条件的字段也可以适当添加非聚集索引. 2.在使用G ...
- SQL GUID和自增列做主键的优缺点
我们公司的数据库全部是使用GUID做主键的,很多人习惯使用int做主键.所以呢,这里总结一下,将两种数据类型做主键进行一个比较. 使用INT做主键的优点: 1.需要很小的数据存储空间,仅仅需要4 by ...
- Guid和Sequence做主键的比较
记得A项目组是一个物流管理系统,后台采用了Oracle数据库.在系统中的核心表托运单表中,关于主键采用何种数据类型,是 sequence 还是用GUID , 大家起了争论. 从网络搜索得到的结论看,一 ...
- (转)mysql中InnoDB表为什么要建议用自增列做主键
InnoDB引擎表的特点 1.InnoDB引擎表是基于B+树的索引组织表(IOT) 关于B+树 (图片来源于网上) B+ 树的特点: (1)所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关 ...
- 开发反模式(GUID) - 伪键洁癖
一.目标:整理数据 有的人有强迫症,他们会为一系列数据的断档而抓狂. 一方面,Id为3这一行确实发生过一些事情,为什么这个查询不返回Id为3的这一行?这条记录数据丢失了吗?那个Column到底是什么? ...
- 扩展ASP.NET Identity使用Int做主键
当我们默认新建一个ASP.NET MVC项目的时候,使用的身份认证系统是ASP.NET Identity.但是这里的Identity使用的主键为String类型的GUID.当然这是大多数系统首先类型. ...
- mysql中InnoDB表为什么要建议用自增列做主键
InnoDB引擎表的特点 1.InnoDB引擎表是基于B+树的索引组织表(IOT) 关于B+树 (图片来源于网上) B+ 树的特点: (1)所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关 ...
随机推荐
- [分享] 一款极简单的 BaseEntity CRUD 方法
前言 尝试过 ado.net.dapper.ef,以及Repository仓储,甚至自己还写过生成器工具,以便做常规CRUD操作. 它们日常操作不方便之处: 每次使用前需要声明,再操作: 很多人一个实 ...
- 推荐 2 款超牛逼、炫酷、实用的Docker管理工具!
Docker技术的火热程度,想必每个互联网IT技术人员都能时时感受的到,的确,近些年,国内对于Docker容器技术的应用需求越来越强烈!! 人均年薪80万以上,docker到底是什么?为什么这么火? ...
- android实现倒计时,最简单实现RecyclerView倒计时+SwipeRefreshLayout下拉刷新
先上效果图: RecyclerView + SwipeRefreshLayout 实现倒计时效果 MainActivity.java package top.wintp.counttimedemo1; ...
- 【译】WebAPI,Autofac,以及生命周期作用域
说明 原文地址:http://decompile.it/blog/2014/03/13/webapi-autofac-lifetime-scopes/ 介绍 这是一篇关于AutoFac的生命周期作用域 ...
- flink入门实战总结
随着大数据技术在各行各业的广泛应用,要求能对海量数据进行实时处理的需求越来越多,同时数据处理的业务逻辑也越来越复杂,传统的批处理方式和早期的流式处理框架也越来越难以在延迟性.吞吐量.容错能力以及使用便 ...
- SpringBoot(18)---通过Lua脚本批量插入数据到Redis布隆过滤器
通过Lua脚本批量插入数据到布隆过滤器 有关布隆过滤器的原理之前写过一篇博客: 算法(3)---布隆过滤器原理 在实际开发过程中经常会做的一步操作,就是判断当前的key是否存在. 那这篇博客主要分为三 ...
- Hadoop自学系列集(二) ---- CentOS下安装JDK
上篇我们讲述了如何使用VMware安装CentOS系统,接下来就看如何安装我们最为熟悉的jdk吧!安装前先看看系统上有没有安装过jdk,输入java -version,如果查询出了其他版本的jdk版本 ...
- Thinkphp5.0快速入门笔记(1)
学习来源与说明 https://www.kancloud.cn/thinkphp/thinkphp5_quickstart 测试与部署均在windows10下进行学习. Composer安装和更新 C ...
- 如何在docker下安装elasticsearch(上)
一 环境 VMware® Workstation 15 Pro centos7 (1810) docker19.03.1 二 进入centos7启动dcoker systemctl start doc ...
- 【kafka】一、消息队列
在高并发的应用场景中,由于来不及同步处理请求,接收到的请求往往会发生阻塞.例如,大量的插入.更新请求同时到达数据库,这会导致行或表被锁住,最后会因为请求堆积过多而触发“连接数过多的异常” 的错误.因此 ...