很多时候我们都需要生成一个全局id用于数据存储的主键,那么如何生成一个全局id呢?有哪些方法?优缺点是啥?
0.redis自增主键
直接用redis原理性的自增命令 incr(key)
,获取一个自增主键.
- 优点:简单快捷,原子性,支持分布式系统
- 缺点:引入第三方中间件,需要考虑数据安全性问题(比如持久化,或者如果内存满了数据被淘汰了,重新计数会不会对你的系统有影响呢?)
1.数据库自增id
我们需要一个单独的表给我们专门生成自增id,每次到这个专门生成id的表里插入一条数据拿回id,带着这个id去新增自己分表数据;
- 优点:方便简单,谁都会用;
- 缺点:
1.单库生成自增id,要是高并发的话,就会有瓶颈的;(对于高并发的负载,InnoDB 在按主键进行插入的时候会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入都发生在这里,并发插入会导致间隙锁竞争)
2.如果别人爬取统计咱们的id,有可能会分析出咱们的业务量和运营情况;
2.uuid
eg:UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf
- 优点:本地生成,不要基于数据库来了;
- 缺点:uuid太长了,作为主键性能太差了
(不像自增主键一样有明显的顺序性,1增加了记录位置计算成本,2增加了页分裂的可能,增加了移动记录的成本),不适合用于主键。
适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的
。
3.获取系统当前时间
这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。如果单独使用基本就不用考虑了。
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id
,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,订单编号,时间戳 + 用户id + 业务含义编码
4.snowflake算法
twitter开源的分布式id生成算法,就是把一个64位的long型的id,1个bit是不用的符号位,剩下的用其中的41 bit作为毫秒数,用10 bit作为工作机器id(5位机房id加五位机器id),12 bit作为序列号,也不是特别复杂,咱们画一下图就知道了
1 bit:不用,为啥呢?因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0
41 bit:表示的是时间戳,单位是毫秒。41 bit可以表示的数字多达2^41 - 1,也就是可以标识2 ^ 41 - 1个
毫秒值,换算成年就是表示69年的时间。
10 bit:记录工作机器id,代表的是这个服务最多可以部署在2^10台机器上哪,也就是1024台机器。
但是10 bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表2 ^ 5个机房(32个机房),
每个机房里可以代表2 ^ 5个机器(32台机器)。
12 bit:这个是用来记录同一个毫秒内产生的不同id,12 bit可以代表的最大正整数是2 ^ 12 - 1 = 4096,
也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id
64位的long型的id,64位的long -> 二进制
4.1 雪花算法的坑:
- 1.id的时间戳部分只能表示69年,不过一般一个系统也很难超过这个限制。
- 2.雪花算法依赖系统的时间一致性,如果系统时间被回调,可能会出现id重复导致主键冲突的情况。另外分布式系统中,各节点的时间并不能保证完全同步,所以有可能出现分布式系统中不能出现完全递增的情况。
第一个问题就不说了,直接说第二个问题解决方案;
- 1.先采用
惰性
方式,即我们会保存一个系统已使用的最新的时间戳,如果当前时间小于已使用的时间戳(即时钟回拨)那就睡个几毫秒
,然后再次获取当前时间,如果还是有回拨现象采用下面2 - 2.备用workid方案,主要思路,snowflake算法给
workerId
预留了10位,即workId的取值范围为[0, 1023],事实上实际生产环境不大可能需要部署1024个分布式ID服务,所以:将workerId取值范围缩小为[0, 511],[512, 1023]这个范围的workerId当做备用workerId
。workId为0的备用workerId是512,workId为1的备用workerId是513,以此类推……,如果我们0号服务产生了时钟回拨的问题,我们就用其备用服务512去生成id,如果我们认为两套备用服务依然不可靠,可以继续分区
,但是这样的话我们可以用的workid就变少
了。 时钟回拨问题原文可以看=====>雪花算法增强