现如今可谓是微服务、分布式、IoT(物联网)横行的时代,作为一名开发者始终还是要保持一定的危机意识,特别是在日常的项目开发中,若是有机会接触到一些关于微服务、分布式下的应用场景,应当硬着头皮、排除万难,主动应承下来上去大干一场;这期间不管结果如何,积累下来的经验将会让自己受益匪浅;而本文要介绍的“分布式全局唯一ID”便是一种典型的分布式应用场景!!!
话不多说,咱们直接进入正题~~~
说起这个全局唯一ID,你可能会第一时间想到“数据库的自增主键”、“UUID”、“雪花算法”等等,更有甚者,还能说出一些大厂开源的组件,比如滴滴的IDWorker、美团的Leaf等等,没错,这些确实是可以实现全局唯一ID的方案,你能想到这些点,那涉猎其实还是挺广的。
而对于“全局唯一ID/编号/编码”的应用场景,在现实生活中还是比较多的,比如电商平台中“订单系统”的订单编号,“进销存系统”中的商品编号、订单编号,“支付”过程中订单流水号等等;接下来debug将会总结性的介绍下目前市面上比较流行的“全局唯一ID”的几种实现方式,并针对分布式场景下的几种实现方式进行代码实战。
话不多说,直接进入正题,先贴张思维导图吧,总结性地概括下目前网上比较流行的几种方式
结合上图几种方式,再概括性的介绍下吧:
1、 数据库的自增主键
简介:这一点相信写过代码的小伙伴都晓得,主要利用主键ID的auo_increment特性,每进来一条数据时数据库自动为其生成当前最大的ID并作为该条记录的主键;
优点:简单、便捷;
缺点:只能限于单机,严重依赖于DB,仅可限于DB相关的业务,可用性还是有点差;
TIPS: 生成的id是连续的吗?https://www.jianshu.com/p/369559f399d0
2、批量预生成ID
简介:DB只存储当前最大的ID值,每次需要ID时,则按照顺序批量生成N个有序的ID列表,并将最大的ID值 + N。
优点:相对于第一种方式性能还是提高了不少;
缺点:仍然得依赖于DB,可用性还是有点差;而且批量生成的ID可能断层(比如服务挂了然后重启就可能跳过部分ID,如果服务有多个,将难以保证其有序性)
TIPS:已废弃 20200708 go服务billno解耦方案
3、 UUID的方式
简介:通用唯一识别码,这个估计众所周知啦,不作过多的介绍了!
优点:简单,直接 UUID.randomUUID().toString()即可搞定;
缺点:比较长、占用空间大;无序且不利于索引,在实际项目中不建议使用;特别是在插入数据库时如果用UUID生成的ID作为主键的话,很可能会引起B+树的不断重平衡;
4、基于时间戳
简介:比如按照规则:yyyyMMdd+ N位随机数 或者 yyyyMMddHHmmss + N位随机数。
优点:可行,而且生成的ID编号前半段有序,有一定的业务意义;
缺点:当并发产生的数据量比较大时,那N位随机数会出现重复的可能(虽然可以通过各种方式去重,比如Redis的Set,但代价还是相当高的,因为得不断的 while判断是否重复…)
**TIPS:**linux下生成随机数 https://blog.51cto.com/13236892/2426623
5、SnowFlake算法
简介:Twitter开源的一种分布式ID生成算法,结果是一个Long型的64位的ID;其核心思想是将64位划分为各个段,其中0号位不用,连续41位表示时间戳,连续10位表示工作机器ID,最后12位则表示毫秒级别的序列号,如下图所示:
算法产生的是一个long型 64 比特位的值,第一位未使用。接下来是41位的毫秒单位的时间戳,我们可以计算下:
2^41/1000*60*60*24*365 = 69
也就是这个时间戳可以使用69年不重复,这个对于大部分系统够用了。
很多人这里会搞错概念,以为这个时间戳是相对于一个我们业务中指定的时间(一般是系统上线时间),而不是1970年。这里一定要注意。
10位的数据机器位,所以可以部署在1024个节点。
12位的序列,在毫秒的时间戳内计数。 支持每个节点每毫秒产生4096个ID序号,所以最大可以支持单节点差不多四百万的并发量,这个妥妥的够用了。
优点:可以说是分布式场景下生成全局唯一ID的一种经典算法吧,采用Java生成,对于咱们Java的小伙伴来说可以说是相当接地气的了;
缺点:目前倒没发现有啥缺陷,如果硬要说有,那就是“时钟回播”的问题了。
**TIPS:**时钟回拨问题
- UidGenerator,时间递增(传统的雪花算法实现都是通过System.currentTimeMillis()来获取时间并与上一次时间进行比较,这样的实现严重依赖服务器的时间。而UidGenerator的时间类型是AtomicLong,且通过incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题)(https://zhuanlan.zhihu.com/p/77737855)
- leaf 5ms内等待,如果时钟回拨超过5ms,依然会抛出异常。(https://www.jianshu.com/p/bd6b00e5f5ac)
- sharding jdbc直接拒绝
6、 原子操作类AtomlcXX
简介:JUC(java.util.concurrent)包下经典的原子操作类,可以基于它生成自增、有序且全局唯一的编号
优点:底层采用CAS(Compare And Swap)机制实现,并发场景下可以保证“自增”代码逻辑的安全性;
缺点:依赖于JDK,只适合单机环境
7、Redis的INCRBY操作
简介:熟悉这个命令的应该都知道它是啥意思,不知道的 自己打开redis-cli执行下该命令就可以了!
优点:可行,分布式场景下是适用的;
缺点:基本上没想到有啥缺陷,如果要挑刺的话,那就是依赖于中间件服务,如果Redis挂掉,那基本上该ID生成服务就不可用了
**TIPS:**redis的单机处理是每秒10w左右,如果并发超过10w如何处理?集群式redis如何生成唯一id
8、基于ZooKeeper的节点版本号生成ID
简介:这个大家可能有点陌生,其实就是利用ZooKeeper底层树形节点ZNode(类似于Windows的文件目录数)的有序性,循环不断生成其对应的版本号或者节点本身的数据
优点:可行,分布式场景下是适用的;
缺点:基本上没想到有啥缺陷,跟第七点类似吧,需要保证ZK服务的高可用即可