高并发分布式系统中生成全局唯一订单号

来源:http://www.tabletreviewspecs.com 作者: 2017-09-26 02:37

  1)在大表做水平分表时,就不能使用自增Id,因为Insert的记录插入到哪个分表依分表规则判定决定,若是自增Id,各个分表中Id就会重复,在做查询、删除时就会有异常。

  2)在对表进行高并发单记录插入时需要加入事物机制,否则会出现Id重复的问题。

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

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

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

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

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

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

  详细可参考:《使用MaxId表存储各表的MaxId值,以获取全局唯一Id》

  (@土豆烤肉)存储过程中不使用事物,一旦使用到事物性能就急剧下滑。直接使用UPDATE获取到的更新锁,即SQL SERVER会UPDATE的顺序执行。(已在用户过千万的并发系统中使用)

  @maxId = key_value,key_value = key_value + 1

  结论:适用中型应用,此方案解决了分表,关联表插入记录的问题。但是无法满足高并发性能要求。同时也存在单点问题,如果这个数据库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思想)

  但是要注意:1、多服务器就必须面临负载均衡的问题;2、倘若添加新节点,需要对原有数据重新根据步长计算迁移数据。

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

  而且无法满足高并发性能要求。同时也存在单点问题,如果这个数据库cash掉的话4、通过数据库集群编号+集群内的自增类型两个字段共同组成唯一主键

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

  5、通过设置每个集群中自增ID起始点(auto_increment_offset),将各个集群的ID进行绝对的分段来实现全局唯一。当遇到某个集群数据增长过快后,通过命令调整下一个ID起始跳过可能存在的冲突。

  优点:实现简单,且比较容易根据ID大小直接判断出数据处在哪个集群,对应用透明。缺点:相对较复杂,需要高度关注各个集群ID增长状况。

  GUID制定的算法中使用到用户的网卡MAC地址,以在计算机集群中生成唯一GUID;在相同计算机上随机生成两个相同GUID的可能性常小的,但并不为0。所以,用于生成GUID的算法通常都加入了非随机的参数(如时间),以这种重复的情况不会发生。

  优点:GUID是最简单的方案,跨平台,跨语言,跨业务逻辑,全局唯一的Id,数据间同步、迁移都能简单实现。

  2)占用了珍贵的聚集索引,一般我们不会根据GUID去查单据,并且插入时因为GUID是无需的,在聚集索引的排序规则下可能移动大量的记录。

  @徐少侠GUID无序在并发下效率高,并且一个数据页内添加新行,是在B树内增加,本质没有什么数据被移动,

  ,需要拆页。而GUID方案导致的拆页比顺序ID要低太多了(数据库不是很懂,暂时无法断定,大家自己认识)@无色我们要明白id是什么,是身份标识,标识身份是id最大的业务逻辑,不要引入什么时间,什么用户业务逻辑,

  Id,那么将导致大量的改动,所以这个方案的最佳状态是一开始就设计业务Id,当然业务Id的唯一性也是我们要考虑的。

  在指定计算机上创建大于先前通过该函数生成的任何GUID的GUID。newsequentialid产生的新的值是有规律的,则索引B+树的变化是有规律的,就不会导致索引列插入时移动大量记录的问题。

  但一旦服务器重新启动,其再次生成的GUID可能反而变小(但仍然保持唯一)。这在很大程度上提高了索引的性能,但并不能所生成的GUID一直增大。

  的这个函数产生的GUID很简单就可以预测,因此不适合用于安全目的。a)只能做为数据库列的DEFAULT VALUE,不能执行类似SELECT NEWSEQUENTIALID()的语句.

  如果生成的GUID所在字段做为外键要被其他表使用,我们就需要得到这个生成的值。通常,PK是一个IDENTITY字段,我们可以在INSERT之后执行SELECT SCOPE_IDENTITY()来获得新生成的ID,但是由于NEWSEQUENTIALID()不是一个INDETITY类型,这个办法是做不到了,而他本身又只能在默认值中使用,不可以事先SELECT好再插入,那么我们如何得到呢?有以下两种方法:

  无序特性导致索引列插入移动大量记录的问题。但是在关联表插入时需要返回数据库中生成的GUID;生成的Id不够友好;占据了32位。2)“COMB”(combined guid/timestamp,意思是:组合GUID/时间截)

  COMB数据类型的基本设计思是这样的:既然GUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么能不能通过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime),这样我们将时间信息与GUID组合起来,在保留GUID的唯一性的同时增加了有序性,以此来提高索引效率。

  GUID的唯一性的同时增加了GUID有序性,提高了索引效率;解决了关联表业务问题;生成的Id不够友好;占据了32位。(强烈推荐)3)长度问题,使用Base64或Ascii85编码解决。(要注意的是上述有序性方案在进行编码后也会变得无序)

  当需要使用更少的字符表示GUID时,可能会使用Base64或Ascii85编码。Base64编码的GUID有22-24个字符,如:

  的长度。生成的Id不够友好;索引效率较低。7、GUID TO Int64

  不过我的小伙伴说ToInt64后就不唯一了。因此我专门写了个并发测试程序,后文将给出测试结果截图及代码简单说明。

  (唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案)

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

  缺陷:根据具体编码规则实现而不同;还要考虑倘若主键在业务上允许改变的,会带来外键同步的麻烦。

  1)12位年月日时分秒+5位随机码+3位服务器编码(这样就完全单机完成生成全局唯一编码)---共20位

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

  2)12位年月日时分秒+5位流水码+3位服务器编码(这样流水码就需要结合数据库和缓存)---共20位 (将影响顺序权重大的“流水码”放前面,影响顺序权重小的服务器编码放后)

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

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

  根据内核数使用多线程并发生成Guid后再转为Int64位值,放入集合A、B、N,多少个线程就有多少个集合。

  再使用Dictionary字典高效查key的特性,将步骤1中生成的多个集合全部加到Dictionary中,看是否有重复值。

  示例注解:测了Dictionarylong,bool最大容量就在5999470左右,所以每次并发生成的唯一值总数控制在此范围内,让测试达到最有效话。

  的专业回答:(大家分析下,我数学比较差,稍后再说自己的理解)GUID桶数量:(2 ^ 4) ^ 32 = 2 ^ 128

  (唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案)

  (唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案)

  我使用了五种Random生成方案,要Random生成唯一主要因素就是种子参数要唯一。(这是比较久以前写的测试案例了,一直找不到合适的博文放,今天终于找到合适的地方了)

  不过该测试是在单线程下的,多线程应使用不同的Random实例,所以对结果影响不会太大。

  数据库文件体积只是一个参考值,可水平扩展系统性能(如nosql,缓存系统)并不和文件体积有高指数的线性相关。

  如taobao/qq的系统比拼byte系统慢,关键在于索引的命中率,缓存,系统的水平扩展。

  如果数据库要求伸缩性,你搞这么多byte,需要不断改程序,不是自找苦吗?

  如果数据库要求移植性,你搞这么多byte,移植起来不如重新设计,这是不是很多公司不断加班的原因?

  不依赖于数据存储系统是分层设计思想的精华,实现战略性能最大化,而不是追求战术单机性能最大化。

  不要数据库性能,不要三范式,不要使用外键,不要使用byte,不要使用自增id,不要使用存储过程,不要使用内部函数,不要使用非标准sql,存储系统只做存储系统的事。当出现系统性能时,如此设计的数据库可以更好的实现迁移数据库(如mysql-oracle),实现nosql((mongodb/hadoop),实现key-value缓存(redis,memcache)。

  很多程序员有对性能认识有误区,如使用存储过程代替正常程序,其实使用存储过程只是追求单服务器的高性能,当需要服务器水平扩展时,存储过程中的业务逻辑就是你的噩运。

  除数字日期,能用字符串存储的字段尽量使用字符串存储,不要为节省那不值钱的1个g的硬盘而使用类似字节之类的字段,进而大幅系统可伸缩性和可扩展性。

  不要为了追求所谓的性能,引入byte,使用byte注定是短命和难于移植,想想为什么html,email一直流行,因为它们使用的是字符串表示法,只要有人类永远都能解析,如email把二进制转成base64存储。除了实时系统,视频外,使用字符串来存储数据,系统性能的关键在于分布式,在于水平扩展。