又一个多月没冒泡了,其实最近学了些东西,可是没有安排时间整理成博文,后续再奉上。最近还写了一个发邮件的组件以及性能测试请看 《NET开发邮件发送功能的全面教程(含邮件组件源码)》 ,还弄了个MSSQL参数化语法生成器,会在9月整理出来,有兴趣的园友能够关注下个人博客。html
分享起因,最近公司用到,而且在找最合适的方案,但愿你们多参与讨论和提出新方案。我和个人小伙伴们也讨论了这个主题,我受益不浅啊……java
博文示例:mysql
今天分享的主题是:如何在高并发分布式系统中生成全局惟一Id。程序员
但这篇博文其实是“半分享半讨论”的博文:redis
1) 半分享是我将说下我所了解到的关于今天主题所涉及的几种方案。算法
2) 半讨论是我但愿你们对各个方案都说说本身的看法,更加但愿你们能提出更好的方案。(我还另外提问在此:http://q.cnblogs.com/q/53552/上面已有几位园友回复(感谢dudu站长的参与),若大家有看法和新方案就在本博文留言吧,方便我整理更新到博文中,谢谢!)sql
我了解的方案以下……………………………………………………………………mongodb
一、 使用数据库自增Id数据库
优点:编码简单,无需考虑记录惟一标识的问题。缓存
缺陷:
1) 在大表作水平分表时,就不能使用自增Id,由于Insert的记录插入到哪一个分表依分表规则断定决定,如果自增Id,各个分表中Id就会重复,在作查询、删除时就会有异常。
2) 在对表进行高并发单记录插入时须要加入事物机制,不然会出现Id重复的问题。
3) 在业务上操做父、子表(即关联表)插入时,须要在插入数据库以前获取max(id)用于标识父表和子表关系,若存在并发获取max(id)的状况,max(id)会同时被别的线程获取到。
4) 等等。
结论:适合小应用,无需分表,没有高并发性能要求。
二、 单独开一个数据库,获取全局惟一的自增序列号或各表的MaxId
1) 使用自增序列号表
专门一个数据库,生成序列号。开启事物,每次操做插入时,先将数据插入到序列表并返回自增序列号用于作为惟一Id进行业务数据插入。
注意:须要按期清理序列表的数据以保证获取序列号的效率;插入序列表记录时要开启事物。
使用此方案的问题是:每次的查询序列号是一个性能损耗;若是这个序列号列暴了,那就杯具了,你不知道哪一个表使用了哪一个序列,因此就必须换另外一种惟一Id方式如GUID。
2) 使用MaxId表存储各表的MaxId值
专门一个数据库,记录各个表的MaxId值,建一个存储过程来取Id,逻辑大体为:开启事物,对于在表中不存在记录,直接返回一个默认值为1的键值,同时插入该条记录到table_key表中。而对于已存在的记录,key值直接在原来的key基础上加1更新到MaxId表中并返回key。
使用此方案的问题是:每次的查询MaxId是一个性能损耗;不过不会像自增序列表那么容易列暴掉,由于是摆表进行划分的。
详细可参考:《使用MaxId表存储各表的MaxId值,以获取全局惟一Id》
我截取此文中的sql语法以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
第一步:建立表
create
table
table_key
(
table_name
varchar
(50)
not
null
primary
key
,
key_value
int
not
null
)
第二步:建立存储过程来取自增ID
create
procedure
up_get_table_key
(
@table_name
varchar
(50),
@key_value
int
output
)
as
begin
begin
tran
declare
@
key
int
--initialize the key with 1
set
@
key
=1
--whether the specified table is exist
if
not
exists(
select
table_name
from
table_key
where
table_name=@table_name)
begin
insert
into
table_key
values
(@table_name,@
key
)
--default key vlaue:1
end
-- step increase
else
begin
select
@
key
=key_value
from
table_key
with
(nolock)
where
table_name=@table_name
set
@
key
=@
key
+1
--update the key value by table name
update
table_key
set
key_value=@
key
where
table_name=@table_name
end
--set ouput value
set
@key_value=@
key
--commit tran
commit
tran
if @@error>0
rollback
tran
end
|
感谢园友的好建议:
1
2
3
4
5
6
7
8
|
TransactionOptions option =
new
TransactionOptions();
option.IsolationLevel = IsolationLevel.ReadUncommitted;
option.Timeout =
new
TimeSpan(0, 10, 0);
using
(TransactionScope transaction =
new
TransactionScope(TransactionScopeOption.RequiresNew, option))
{
//调用存储过程
}
|
在咨询过DBA后,这个存储过程提升数据库隔离级别会加大数据库访问压力,致使响应超时问题。因此这个建议咱们只能在代码编写宣导上作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
create
procedure
[dbo].[up_get_table_key]
(
@table_name
varchar
(50),
@key_value
int
output
)
as
begin
SET
NOCOUNT
ON
;
DECLARE
@maxId
INT
UPDATE
table_key
SET
@maxId = key_value,key_value = key_value + 1
WHERE
table_name=@table_name
SELECT
@maxId
end
|
结论:适用中型应用,此方案解决了分表,关联表插入记录的问题。可是没法知足高并发性能要求。同时也存在单点问题,若是这个数据库cash掉的话……
咱们目前正头痛这个问题,由于咱们的高并发经常出现数据库访问超时,瓶颈就在这个MaxId表。咱们也有考虑使用分布式缓存(eg:memcached)缓存第一次访问MaxId表数据,以提升再次访问速度,并定时用缓存数据更新一次MaxId表,但咱们担忧的问题是:
a) 假若缓存失效或暴掉了,那缓存的MaxId没有更新到数据库致使数据丢失,必须停掉站点来执行Select max(id)各个表来同步MaxId表。
b) 分布式缓存不是一保存下去,其余服务器上就立马能够获取到的,即数据存在不肯定性。(其实也是缓存的一个误用,缓存应该用来存的是频繁访问而且不多改动的内容)
改进方案:
总体思想:创建两台以上的数据库ID生成服务器,每一个服务器都有一张记录各表当前ID的MaxId表,可是MaxId表中Id的增加步长是服务器的数量,起始值依次错开,这样至关于把ID的生成散列到每一个服务器节点上。例如:若是咱们设置两台数据库ID生成服务器,那么就让一台的MaxId表的Id起始值为1(或当前最大Id+1),每次增加步长为2,另外一台的MaxId表的ID起始值为2(或当前最大Id+2),每次步长也为2。这样就将产生ID的压力均匀分散到两台服务器上,同时配合应用程序控制,当一个服务器失效后,系统能自动切换到另外一个服务器上获取ID,从而解决的单点问题保证了系统的容错。(Flickr思想)
可是要注意:一、多服务器就必须面临负载均衡的问题;二、假若添加新节点,须要对原有数据从新根据步长计算迁移数据。
结论:适合大型应用,生成Id较短,友好性比较好。(强烈推荐)
三、 Sequence特性
这个特性在SQL Server 20十二、Oracle中可用。这个特性是数据库级别的,容许在多个表之间共享序列号。它能够解决分表在同一个数据库的状况,但假若分表放在不一样数据库,那将共享不到此序列号。(eg:Sequence使用场景:你须要在多个表之间公用一个流水号。以往的作法是额外创建一个表,而后存储流水号)
相关Sequence特性资料:
SQL Server2012中的SequenceNumber尝试
SQL Server 2012 开发新功能——序列对象(Sequence)
Difference between Identity and Sequence in SQL Server 2012
结论:适用中型应用,此方案不能彻底解决分表问题,并且没法知足高并发性能要求。同时也存在单点问题,若是这个数据库cash掉的话……
四、 经过数据库集群编号+集群内的自增类型两个字段共同组成惟一主键
优势:实现简单,维护也比较简单。
缺点:关联表操做相对比较复杂,须要两个字段。而且业务逻辑必须是一开始就设计为处理复合主键的逻辑,假若是到了后期,由单主键转为复合主键那改动成本就太大了。
结论:适合大型应用,但须要业务逻辑配合处理复合主键。
五、 经过设置每一个集群中自增 ID 起始点(auto_increment_offset),将各个集群的ID进行绝对的分段来实现全局惟一。当遇到某个集群数据增加过快后,经过命令调整下一个 ID 起始位置跳过可能存在的冲突。
优势:实现简单,且比较容易根据 ID 大小直接判断出数据处在哪一个集群,对应用透明。缺点:维护相对较复杂,须要高度关注各个集群 ID 增加情况。
结论:适合大型应用,但须要高度关注各个集群 ID 增加情况。
六、 GUID(Globally Unique Identifier,全局惟一标识符)
GUID一般表示成32个16进制数字(0-9,A-F)组成的字符串,如:{21EC2020-3AEA-1069-A2DD-08002B30309D},它实质上是一个128位长的二进制整数。
GUID制定的算法中使用到用户的网卡MAC地址,以保证在计算机集群中生成惟一GUID;在相同计算机上随机生成两个相同GUID的可能性是很是小的,但并不为0。因此,用于生成GUID的算法一般都加入了非随机的参数(如时间),以保证这种重复的状况不会发生。
优势:GUID是最简单的方案,跨平台,跨语言,跨业务逻辑,全局惟一的Id,数据间同步、迁移都能简单实现。
缺点:
1) 存储占了32位,且无可读性,返回GUID给客户显得很不专业;
2) 占用了珍贵的汇集索引,通常咱们不会根据GUID去查单据,而且插入时由于GUID是无需的,在汇集索引的排序规则下可能移动大量的记录。
有两位园友主推GUID,无须顺序GUID方案缘由以下:
@徐少侠 GUID无序在并发下效率高,而且一个数据页内添加新行,是在B树内增长,本质没有什么数据被移动,惟一可能的,是页填充因子满了,须要拆页。而GUID方案致使的拆页比顺序ID要低太多了(数据库不是很懂,暂时没法判定,你们本身认识)
@无色 咱们要明白id是什么,是身份标识,标识身份是id最大的业务逻辑,不要引入什么时间,什么用户业务逻辑,那是另一个字段干的事,使用base64(guid,uuid),是通盘考虑,彻底能够更好的兼容nosql,key-value存储。
(推荐),可是假若你系统一开始没有规划一个业务Id,那么将致使大量的改动,因此这个方案的最佳状态是一开始就设计业务Id,固然业务Id的惟一性也是咱们要考虑的。
结论:适合大型应用;生成的Id不够友好;占据了32位;索引效率较低。
改进:
1) (@dudu提点)在SQL Server 2005中新增了NEWSEQUENTIALID函数。
详细请看:《理解newid()和newsequentialid()》
在指定计算机上建立大于先前经过该函数生成的任何 GUID 的 GUID。 newsequentialid 产生的新的值是有规律的,则索引B+树的变化是有规律的,就不会致使索引列插入时移动大量记录的问题。
但一旦服务器从新启动,其再次生成的GUID可能反而变小(但仍然保持惟一)。这在很大程度上提升了索引的性能,但并不能保证所生成的GUID一直增大。SQL的这个函数产生的GUID很简单就能够预测,所以不适合用于安全目的。
a) 只能作为数据库列的DEFAULT VALUE,不能执行相似SELECT NEWSEQUENTIALID()的语句.
b) 如何得到生成的GUID.
若是生成的GUID所在字段作为外键要被其余表使用,咱们就须要获得这个生成的值。一般,PK是一个IDENTITY字段,咱们能够在INSERT以后执行 SELECT SCOPE_IDENTITY()来得到新生成的ID,可是因为NEWSEQUENTIALID()不是一个INDETITY类型,这个办法是作不到了,而他自己又只能在默认值中使用,不能够事先SELECT好再插入,那么咱们如何获得呢?有如下两种方法:
1
2
3
4
5
6
7
8
9
10
11
12
|
--1. 定义临时表变量
DECLARE
@outputTable
TABLE
(ID uniqueidentifier)
INSERT
INTO
TABLE1(col1, col2)
OUTPUT
INSERTED.ID
INTO
@outputTable
VALUES
(
'value1'
,
'value2'
)
SELECT
ID
FROM
@outputTable
--2. 标记ID字段为ROWGUID(一个表只能有一个ROWGUID)
INSERT
INTO
TABLE1(col1, col2)
VALUES
(
'value1'
,
'value2'
)
--在这里,ROWGUIDCOL其实至关于一个别名
SELECT
ROWGUIDCOL
FROM
TABLE1
|
结论:适合大型应用,解决了GUID无序特性致使索引列插入移动大量记录的问题。可是在关联表插入时须要返回数据库中生成的GUID;生成的Id不够友好;占据了32位。
2) “COMB”(combined guid/timestamp,意思是:组合GUID/时间截)
(感谢:@ ethan-luo ,@lcs-帅 )
COMB数据类型的基本设计思路是这样的:既然GUID数据因毫无规律可言形成索引效率低下,影响了系统的性能,那么能不能经过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime),这样咱们将时间信息与GUID组合起来,在保留GUID的惟一性的同时增长了有序性,以此来提升索引效率。
在NHibernate中,COMB型主键的生成代码以下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/// <summary> /// Generate a new <see cref="Guid"/> using the comb algorithm.
/// </summary>
private
Guid GenerateComb()
{
byte
[] guidArray = Guid.NewGuid().ToByteArray();
DateTime baseDate =
new
DateTime(1900, 1, 1);
DateTime now = DateTime.Now;
// Get the days and milliseconds which will be used to build
//the byte string
TimeSpan days =
new
TimeSpan(now.Ticks - baseDate.Ticks);
TimeSpan msecs = now.TimeOfDay;
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a
// millisecond so we divide by 3.333333
byte
[] daysArray = BitConverter.GetBytes(days.Days);
byte
[] msecsArray = BitConverter.GetBytes((
long
)
(msecs.TotalMilliseconds / 3.333333));
// Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
// Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray,
guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray,
guidArray.Length - 4, 4);
return
new
Guid(guidArray);
}
|
结论:适合大型应用。即保留GUID的惟一性的同时增长了GUID有序性,提升了索引效率;解决了关联表业务问题;生成的Id不够友好;占据了32位。(强烈推荐)
3) 长度问题,使用Base64或Ascii85编码解决。(要注意的是上述有序性方案在进行编码后也会变得无序)
如:
GUID:{3F2504E0-4F89-11D3-9A0C-0305E82C3301}
当须要使用更少的字符表示GUID时,可能会使用Base64或Ascii85编码。Base64编码的GUID有22-24个字符,如:
7QDBkvCA1+B9K/U0vrQx1A
7QDBkvCA1+B9K/U0vrQx1A==
Ascii85编码后是20个字符,如:
5:$Hj:Pf\4RLB9%kU\Lj
代码如:
Guid guid = Guid.NewGuid();
byte[] buffer = guid.ToByteArray();
var shortGuid = Convert.ToBase64String(buffer);
结论:适合大型应用,缩短GUID的长度。生成的Id不够友好;索引效率较低。
七、 GUID TO Int64
对于GUID的可读性,有园友给出以下方案:(感谢:@黑色的羽翼)
1
2
3
4
5
6
7
8
|
/// <summary>
/// 根据GUID获取19位的惟一数字序列
/// </summary>
public
static
long
GuidToLongID()
{
byte
[] buffer = Guid.NewGuid().ToByteArray();
return
BitConverter.ToInt64(buffer, 0);
}
|
即将GUID转为了19位数字,数字反馈给客户能够必定程度上缓解友好性问题。EG:
GUID: cfdab168-211d-41e6-8634-ef5ba6502a22 (不友好)
Int64: 5717212979449746068 (友好性还行)
不过个人小伙伴说ToInt64后就不惟一了。所以我专门写了个并发测试程序,后文将给出测试结果截图及代码简单说明。
(惟一性、业务适合性是能够权衡的,这个惟一性确定比不过GUID的,通常程序上都会安排错误处理机制,好比异常后执行一次重插的方案……)
结论:适合大型应用,生成相对友好的Id(纯数字)------因简单和业务友好性而推荐。
八、 本身写编码规则
优势:全局惟一Id,符合业务后续长远的发展(可能具体业务须要本身的编码规则等等)。
缺陷:根据具体编码规则实现而不一样;还要考虑假若主键在业务上容许改变的,会带来外键同步的麻烦。
我这边写两个编码规则方案:(可能不惟一,只是我的方案,也请你们提出本身的编码规则)
1) 12位年月日时分秒+5位随机码+3位服务器编码 (这样就彻底单机完成生成全局惟一编码)---共20位
缺陷:由于附带随机码,因此编码缺乏必定的顺序感。(生成高惟一性随机码的方案稍后给给出程序)
2) 12位年月日时分秒+5位流水码+3位服务器编码 (这样流水码就须要结合数据库和缓存)---共20位 (将影响顺序权重大的“流水码”放前面,影响顺序权重小的服务器编码放后)
缺陷:由于使用到流水码,流水码的生成必然会遇到和MaxId、序列表、Sequence方案中相似的问题
(为何没有毫秒?毫秒也不具有业务可读性,我改用5位随机码、流水码代替,推测1秒内应该不会下99999[五位]条语法)
结论:适合大型应用,从业务上来讲,有一个规则的编码能体现产品的专业成度。(强烈推荐)
GUID生成Int64值后是否还具备惟一性测试
测试环境
主要测试思路:
示例注解:测了 Dictionary<long,bool> 最大容量就在5999470左右,因此每次并发生成的惟一值总数控制在此范围内,让测试达到最有效话。
主要代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
for
(
int
i = 0; i <= Environment.ProcessorCount - 1; i++)
{
ThreadPool.QueueUserWorkItem(
(list) =>
{
List<
long
> tempList = list
as
List<
long
>;
for
(
int
j = 1; j < listLength; j++)
{
byte
[] buffer = Guid.NewGuid().ToByteArray();
tempList.Add(BitConverter.ToInt64(buffer, 0));
}
barrier.SignalAndWait();
}, totalList[i]);
}
|
测试数据截图:
数据一(循环1000次,测试数:1000*5999470)
数据二(循环5000次,测试数:5000*5999470)--跑了一个晚上……
感谢@Justany_WhiteSnow的专业回答:(你们分析下,我数学比较差,稍后再说本身的理解)
GUID桶数量:(2 ^ 4) ^ 32 = 2 ^ 128
Int64桶数量: 2 ^ 64
假若每一个桶的机会是均等的,则每一个桶的GUID数量为:
(2 ^ 128) / (2 ^ 64) = 2 ^ 64 = 18446744073709551616
也就是说,其实重复的机会是有的,只是几率问题。
楼主测试数是29997350000,发生重复的几率是:
1 - ((1 - (1 / (2 ^ 64))) ^ 29997350000) ≈ 1 - ((1 - 1 / (2 ^ 64)) ^ (2 ^ 32)) < 1 - 1 + 1 / (2 ^ 32) = 1 / (2 ^ 32) ≈ 2.3283064e-10
(惟一性、业务适合性是能够权衡的,这个惟一性确定比不过GUID的,通常程序上都会安排错误处理机制,好比异常后执行一次重插的方案……)
(惟一性、业务适合性是能够权衡的,这个惟一性确定比不过GUID的,通常程序上都会安排错误处理机制,好比异常后执行一次重插的方案……)
结论:GUID转为Int64值后,也具备高惟一性,可使用与项目中。
Random生成高惟一性随机码
我使用了五种Random生成方案,要Random生成惟一主要因素就是种子参数要惟一。(这是比较久之前写的测试案例了,一直找不到合适的博文放,今天终于找到合适的地方了)
不过该测试是在单线程下的,多线程应使用不一样的Random实例,因此对结果影响不会太大。
即:
static int GetRandomSeed()
{
byte[] bytes = new byte[4];
System.Security.Cryptography.RNGCryptoServiceProvider rng
= new System.Security.Cryptography.RNGCryptoServiceProvider();
rng.GetBytes(bytes);
return BitConverter.ToInt32(bytes, 0);
}
测试结果:
结论:随机码使用RNGCryptoServiceProvider或Guid.NewGuid().GetHashCode()生成的惟一性较高。
一些精彩评论(部分更新到原博文对应的地方)
1、
数据库文件体积只是一个参考值,可水平扩展系统性能(如nosql,缓存系统)并不和文件体积有高指数的线性相关。
如taobao/qq的系统比拼byte系统慢,关键在于索引的命中率,缓存,系统的水平扩展。
若是数据库不多,你搞这么多byte能提升性能?
若是数据库很大,你搞这么多byte不兼容索引不兼容缓存,不是害自已吗?
若是数据库要求伸缩性,你搞这么多byte,须要不断改程序,不是自找苦吗?
若是数据库要求移植性,你搞这么多byte,移植起来不如从新设计,这是否是不少公司不断加班的缘由?
不依赖于数据存储系统是分层设计思想的精华,实现战略性能最大化,而不是追求战术单机性能最大化。
不要迷信数据库性能,不要迷信三范式,不要使用外键,不要使用byte,不要使用自增id,不要使用存储过程,不要使用内部函数,不要使用非标准sql,存储系统只作存储系统的事。当出现系统性能时,如此设计的数据库能够更好的实现迁移数据库(如mysql->oracle),实现nosql改造((mongodb/hadoop),实现key-value缓存(redis,memcache)。
2、
不少程序员有对性能认识有误区,如使用存储过程代替正常程序,其实使用存储过程只是追求单服务器的高性能,当须要服务器水平扩展时,存储过程当中的业务逻辑就是你的噩运。
3、
除数字日期,能用字符串存储的字段尽可能使用字符串存储,不要为节省那不值钱的1个g的硬盘而使用相似字节之类的字段,进而大幅牺牲系统可伸缩性和可扩展性。
不要为了追求所谓的性能,引入byte,使用byte注定是短命和难于移植,想一想为何html,email一直流行,由于它们使用的是字符串表示法,只要有人类永远都能解析,如email把二进制转成base64存储。除了实时系统,视频外,建议使用字符串来存储数据,系统性能的关键在于分布式,在于水平扩展。
本次博文到此结束,但愿你们对本次主题“如何在高并发分布式系统中生成全局惟一Id”多提出本身宝贵的意见。另外看着感受舒服,还请多帮推荐…推荐……