又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文,后续再奉上。最近还写了一个发邮件的组件以及性能测试会在这个月整理出来,还弄了个MSSQL参数化语法生成器,会在9月整理出来,有兴趣的园友可以关注下我的博客。

 

分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案。我和我的小伙伴们也讨论了这个主题,我受益匪浅啊……

 

今天分享的主题是:如何在高并发分布式系统中生成全局唯一Id。

但这篇博文实际上是“半分享半讨论”的博文:

1)

2)

 

我了解的方案如下……………………………………………………………………

1、

优势:编码简单,无需考虑记录唯一标识的问题。

缺陷:

1)

2)

3)         在业务上操作父、子表(即关联表)插入时,需要在插入数据库之前获取max(id)用于标识父表和子表关系,若存在并发获取max(id)的情况,max(id)会同时被别的线程获取到。

4)

结论:适合小应用,无需分表,没有高并发性能要求。

2、

1)

专门一个数据库,生成序列号。开启事物,每次操作插入时,先将数据插入到序列表并返回自增序列号用于做为唯一Id进行业务数据插入。

注意:需要定期清理序列表的数据以保证获取序列号的效率;插入序列表记录时要开启事物。

使用此方案的问题是:每次的查询序列号是一个性能损耗;如果这个序列号列暴了,那就杯具了,你不知道哪个表使用了哪个序列,所以就必须换另一种唯一Id方式如GUID。

2)

专门一个数据库,记录各个表的MaxId值,建一个存储过程来取Id,逻辑大致为:开启事物,对于在表中不存在记录,直接返回一个默认值为1的键值,同时插入该条记录到table_key表中。而对于已存在的记录,key值直接在原来的key基础上加1更新到MaxId表中并返回key。

使用此方案的问题是:每次的查询MaxId是一个性能损耗;不过不会像自增序列表那么容易列暴掉,因为是摆表进行划分的。

详细可参考:

                   我截取此文中的sql语法如下:


第一步:创建表       

       createtable 
        table_key


        (       



        table_name  
        varchar
        (50) 
        notnullprimarykey
        ,



        key_value   
        intnotnull 

        )       


                


                


        第二步:创建存储过程来取自增ID       

       createprocedure 
        up_get_table_key


        (       



        @table_name    
        varchar
        (50),



        @key_value     
        intoutput 

        )       

       as 
       begin 


        begin 
        tran



        declare 
        @
        keyint 

                



        --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 


        insertinto 
        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=@
        keywhere 
        table_name=@table_name



        end 


        --set ouput value 


        set 
        @key_value=@
        key 

                



        --commit tran 


        commit 
        tran


                if @@error>0       



        rollback 
        tran

       end


感谢园友的好建议:

1.         (@辉_辉)建议给table_key中为每个表初始化一条key为1的记录,这样就不用每次if来判断了。

2.         (@乐活的CodeMonkey)建议给存储过程中数据库事物隔离级别提高一下,因为出现在CS代码层上使用如下事物代码会导致并发重复问题.

TransactionOptions option =  
        new 
        TransactionOptions();


        option.IsolationLevel = IsolationLevel.ReadUncommitted;       


        option.Timeout = 
        new 
        TimeSpan(0, 10, 0);


                

       using 
        (TransactionScope transaction =
        new 
        TransactionScope(TransactionScopeOption.RequiresNew, option))


        {       



        //调用存储过程 

        }


在咨询过DBA后,这个存储过程提高数据库隔离级别会加大数据库访问压力,导致响应超时问题。所以这个建议我们只能在代码编写宣导上做。

结论:适用中型应用,此方案解决了分表,关联表插入记录的问题。但是无法满足高并发性能要求。同时也存在单点问题,如果这个数据库cash掉的话……

我们目前正头痛这个问题,因为我们的高并发常常出现数据库访问超时,瓶颈就在这个MaxId表。我们也有考虑使用分布式缓存(eg:memcached)缓存第一次访问MaxId表数据,以提高再次访问速度,并定时用缓存数据更新一次MaxId表,但担心的问题是:倘若缓存失效或暴掉了,那缓存的MaxId没有更新到数据库导致数据丢失,必须停掉站点来执行Select max(id)各个表来同步MaxId表。

         网上找到的特别妙的改进方案,帅呆了……

整体思想:建立两台以上的数据库ID生成服务器,每个服务器都有一张记录各表当前ID的MaxId表,但是MaxId表中Id的增长步长是服务器的数量,起始值依次错开,这样相当于把ID的生成散列到每个服务器节点上。例如:如果我们设置两台数据库ID生成服务器,那么就让一台的MaxId表的Id起始值为1(或当前最大Id+1),每次增长步长为2,另一台的MaxId表的ID起始值为2(或当前最大Id+2),每次步长也为2。这样就将产生ID的压力均匀分散到两台服务器上,同时配合应用程序控制,当一个服务器失效后,系统能自动切换到另一个服务器上获取ID,从而解决的单点问题保证了系统的容错。(Flickr思想)

结论:适合大型应用,生成Id较短,友好性比较好。(强烈推荐)

3、

这个特性在SQL Server 2012、Oracle中可用。这个特性是数据库级别的,允许在多个表之间共享序列号。它可以解决分表在同一个数据库的情况,但倘若分表放在不同数据库,那将共享不到此序列号。(eg:Sequence使用场景:你需要在多个表之间公用一个流水号。以往的做法是额外建立一个表,然后存储流水号)

相关Sequence特性资料:

SQL Server2012中的SequenceNumber尝试

SQL Server 2012 开发新功能——序列对象(Sequence)

identity和sequence的区别

Difference between Identity and Sequence in SQL Server 2012

结论:适用中型应用,此方案不能完全解决分表问题,并且存在“单独开一个数据库,获取全局唯一的自增序列号或各表的MaxId”方案中不能解决的问题

4、

优点:实现简单,维护也比较简单。

缺点:关联表操作相对比较复杂,需要两个字段。并且业务逻辑必须是一开始就设计为处理复合主键的逻辑,倘若是到了后期,由单主键转为复合主键那改动成本就太大了。

结论:适合大型应用,但需要业务逻辑配合处理复合主键。

5、

优点:实现简单,且比较容易根据

结论:适合大型应用,但需要高度关注各个集群

6、

优点:GUID是最简单的方案,全局唯一的Id,数据间同步、迁移都能简单实现。

缺点:存储占了32位,且无可读性,返回GUID给客户显得很不专业;还占用了珍贵的聚集索引,一般我们不会根据GUID去查单据,并且插入时因为GUID是无需的,在聚集索引的排序规则下可能移动大量的记录。

结论:适合大型应用,生成的Id不够友好。(推荐)

改进:(@dudu建议)在SQL Server 2005中新增了NEWSEQUENTIALID函数。

详细请看:《理解newid()和newsequentialid()》

在指定计算机上创建大于先前通过该函数生成的任何

1)

2)

3)

         如果生成的GUID所在字段做为外键要被其他表使用,我们就需要得到这个生成的值

         通常,PK是一个IDENTITY字段,我们可以在INSERT之后执行 SELECT SCOPE_IDENTITY()来获得新生成的ID

         但是由于NEWSEQUENTIALID()不是一个INDETITY类型,这个办法是做不到了,而他本身又只能在默认值中使用,不可以事先SELECT好再插入,那么我们如何得到呢?有以下两种方法:


--1. 定义临时表变量 
       DECLARE 
        @outputTable
        TABLE
        (ID uniqueidentifier)

       INSERTINTO 
        TABLE1(col1, col2)

       OUTPUT 
        INSERTED.ID
        INTO 
        @outputTable

       VALUES
        (
        'value1'
        ,
        'value2'
        )

       SELECT 
        ID
        FROM 
        @outputTable


                

       --2. 标记ID字段为ROWGUID(一个表只能有一个ROWGUID) 
       INSERTINTO 
        TABLE1(col1, col2)

       VALUES
        (
        'value1'
        ,
        'value2'
        )

       --在这里,ROWGUIDCOL其实相当于一个别名 
       SELECT 
        ROWGUIDCOL
        FROM 
        TABLE1


结论:适合大型应用,并解决了GUID中无序导致索引列插入移动大量记录的问题。(推荐)

7、

对于GUID的可读性,有友给出如下方案:(感谢:@黑色的羽翼)

/// <summary> 
       /// 根据GUID获取19位的唯一数字序列 
       /// </summary> 
       publicstaticlong 
        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问题。(推荐)

8、

优点:全局唯一Id,符合业务后续长远的发展(可能具体业务需要自己的编码规则等等)。

缺陷:根据具体编码规则实现而不同。

我这边写两个编码规则方案:(可能不唯一,只是个人方案,也请大家提出自己的编码规则)

1)

缺陷:因为附带随机码,所以编码缺少一定的顺序感。(生成高唯一性随机码的方案稍后给给出程序)

2)

缺陷:因为使用到流水码,流水码的生成必然会遇到和MaxId、序列表、Sequence方案中类似的问题

(为什么没有毫秒?毫秒也不具备业务可读性,我改用5位随机码、流水码代替,推测1秒内应该不会下99999[五位]条语法)

 

结论:适合大型应用,从业务上来说,有一个规则的编码能体现产品的专业成度。(强烈推荐)

 

 

GUID生成Int64值后是否还具有唯一性测试

测试环境

如何在高并发分布式系统中生成全局唯一Id_序列号

主要测试思路:

1.

2.

示例注解:测了

主要代码:


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]);       


        }


测试数据截图:

如何在高并发分布式系统中生成全局唯一Id_自增_02

 

数据一(循环1000次)

如何在高并发分布式系统中生成全局唯一Id_序列号_03

数据二(循环5000次)--跑了一个晚上……

如何在高并发分布式系统中生成全局唯一Id_序列号_04

 

(唯一性是可以权衡的,这个唯一性肯定比不过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);
        }

测试结果:

如何在高并发分布式系统中生成全局唯一Id_数据库_05

结论:随机码使用RNGCryptoServiceProvider或Guid.NewGuid().GetHashCode()生成的唯一性较高。