您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

分布式系统全局唯一ID的几种实现方式

时间:2020-12-01 09:16:57  来源:  作者:

现如今可谓是微服务、分布式、IoT(物联网)横行的时代,作为一名开发者始终还是要保持一定的危机意识,特别是在日常的项目开发中,若是有机会接触到一些关于微服务、分布式下的应用场景,应当硬着头皮、排除万难,主动应承下来上去大干一场;这期间不管结果如何,积累下来的经验将会让自己受益匪浅;而本文要介绍的“分布式全局唯一ID”便是一种典型的分布式应用场景!!!
话不多说,咱们直接进入正题~~~

说起这个全局唯一ID,你可能会第一时间想到“数据库的自增主键”、“UUID”、“雪花算法”等等,更有甚者,还能说出一些大厂开源的组件,比如滴滴的IDWorker、美团的Leaf等等,没错,这些确实是可以实现全局唯一ID的方案,你能想到这些点,那涉猎其实还是挺广的。

而对于“全局唯一ID/编号/编码”的应用场景,在现实生活中还是比较多的,比如电商平台中“订单系统”的订单编号,“进销存系统”中的商品编号、订单编号,“支付”过程中订单流水号等等;接下来debug将会总结性的介绍下目前市面上比较流行的“全局唯一ID”的几种实现方式,并针对分布式场景下的几种实现方式进行代码实战。

话不多说,直接进入正题,先贴张思维导图吧,总结性地概括下目前网上比较流行的几种方式(当然啦,图片来源于互联网哈,因为debug懒得去制作了!)

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 

▲点击查看大图(若图片不清晰可点击文章底部阅读原文)

结合上图几种方式,debug再概括性的介绍下吧:

1、 数据库的自增主键

简介:这一点相信写过代码的小伙伴都晓得,主要利用主键ID的auo_increment特性,每进来一条数据时数据库自动为其生成当前最大的ID并作为该条记录的主键;

优点:简单、便捷;

缺点:只能限于单机,严重依赖于DB,仅可限于DB相关的业务,可用性还是有点差;

 2、批量预生成ID

简介:DB只存储当前最大的ID值,每次需要ID时,则按照顺序批量生成N个有序的ID列表,并将最大的ID值 + N。

优点:相对于第一种方式性能还是提高了不少;

缺点:只能限于单机,还是仍然得依赖于DB,可用性还是有点差;而且批量生成的ID可能断层(比如服务挂了然后重启就可能跳过部分ID,如果服务有多个,将难以保证其有序性)

3、 UUID的方式

简介:通用唯一识别码,这个估计众所周知啦,不作过多的介绍了!
优点:简单,直接 UUID.randomUUID().toString()即可搞定;

缺点:比较长、占用空间大;无序且不利于索引,在实际项目中不建议使用;特别是在插入数据库时如果用UUID生成的ID作为主键的话,很可能会引起B+树的不断重平衡;

4、基于时间戳

简介:比如按照规则:yyyyMMdd+ N位随机数 或者 yyyyMMddHHmmss + N位随机数。

优点:可行,而且生成的ID编号前半段有序,有一定的业务意义;

缺点:当并发产生的数据量比较大时,那N位随机数会出现重复的可能(虽然可以通过各种方式去重,比如redis的Set,但代价还是相当高的,因为得不断的 while判断是否重复…)

5、SnowFlake算法

简介:Twitter开源的一种分布式ID生成算法,结果是一个Long型的64位的ID;其核心思想是将64位划分为各个段,其中0号位不用,连续41位表示时间戳,连续10位表示工作机器ID,最后12位则表示毫秒级别的序列号,如下图所示:

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 


优点:可以说是分布式场景下生成全局唯一ID的一种经典算法吧,采用JAVA生成,对于咱们Java的小伙伴来说可以说是相当接地气的了;

缺点:目前倒没发现有啥缺陷,如果硬要说有,那就是“时钟回播”的问题了,但其实没啥事的话别乱重置系统时钟或者乱调系统时钟则一般是没啥问题的!如果还说它仍然有缺点的话,那就是它的算法实现逻辑,即nextId()方法里面的代码还真的挺复杂,一堆位运算 理解起来确实比较消耗脑细胞(除此之外,那就是它最终生成的ID长度有点长啦)!

6、 原子操作类AtomlcXX

简介:JUC包下经典的原子操作类,可以基于它生成自增、有序且全局唯一的编号

优点:底层采用CAS(Compare And Swap)机制实现,并发场景下可以保证“自增”代码逻辑的安全性;

缺点:依赖于JDK,只适合单机环境

7、Redis的INCRBY操作

简介:熟悉这个命令的应该都知道它是啥意思,不知道的 自己打开redis-cli执行下该命令就可以了!

优点:可行,分布式场景下是适用的;

缺点:基本上没想到有啥缺陷,如果要挑刺的话,那就是依赖于中间件服务,如果Redis挂掉,那基本上该ID生成服务就不可用了(其实,这有点杠的嫌疑哈,年轻人 不要搞内斗哈 ~ 你不会做Redis集群部署保证其高可用吗?)

 8、基于ZooKeeper的节点版本号生成ID

简介:这个大家可能有点陌生,其实就是利用ZooKeeper底层树形节点ZNode(类似于windows的文件目录数)的有序性,循环不断生成其对应的版本号或者节点本身的数据

优点:可行,分布式场景下是适用的;

缺点:基本上没想到有啥缺陷,跟第七点类似吧,需要保证ZK服务的高可用即可

啰里啰嗦介绍了这么多,接下来咱们还是得进入代码实战,其中的场景可以暂设定为:生成全局唯一的、数据格式为:yyyyMMddHHmmss + N位的数值码(N=4或者N=6比较常见),其中要求最终生成的码全局唯一、有序且最好有一定的业务意义,那废话少说,咱们直接开干吧!

1、基于原子操作类AtomicXX

(1)需求分析:对于前半部分 yyyyMMddHHmmss 这个还是比较简单的,基于SimpleDateFormat即可解决(但要注意它本身并非线程安全),而至于后面的 N位数值码,在这里可以用 AtomicLong 进行实现,假设 N=6,则其初始值可以设定为100000,也就是说在一段时间内可以生成999999个编码,在秒级并发场景下这应该足够了;

要注意的是,当系统运行到一段时间后,如达到999999时需要将其重置回100000,或者也可以通过不定时上线、重启亦可以达到效果。

(2)为了测试生成的ID/编码是否全局唯一,我们建了一个简单的数据库表qr_code,其DDL如下所示,后续可以通过group by等sql语句统计相同的code出现的次数:

CREATE TABLE `qr_code` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '编码',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

紧接着,创建一控制器类QrCodeController,并在其中创建相应的请求方法,用于JMeter压力测试,如下代码所示:

@Autowired
private QrCodeMApper qrCodeMapper;

//随机的后6位商品编号,毫秒级上限为999999,应该是满足的 (只要间隔一定的频率重新发布/重//启应用时,则当前计数器将重置为 100000)
private static AtomicLong atomicLong=new AtomicLong(100000);

@PostMapping("generate/code/v2")
public BaseResponse generateCodeV2(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        //方式一:原子操作数(单体应用系统架构)
        qrCodeMapper.insertSelective(new QrCode(generateCodeInV1()));
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}
//全局唯一编码 - 正常情况 - 单体应用系统架构下可用 “原子操作数” 控制并发(加上本身就有//计数功能)
private String generateCodeInV1(){
    SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
    return format.format(new Date()) + atomicLong.getAndIncrement();
}

之后,将项目运行起来并打开JMeter建立一测试计划,直接设定1秒内并发线程数为10000,如下图所示:

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 

完了之后,查看数据库表,先看下总数会发现一共10000条数据完美进入DB,与此同时执行下下面的SQL查看下是否有相同的code出现2次或者2次以上的,如下图所示:

SELECT
	`code`,
	COUNT(id) AS total
FROM
	qr_code
GROUP BY
	`code`
HAVING total > 1
干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 

OK,此种实现方式基本上就没啥问题了,但有点要注意的话,这种方式依赖于JDK,只适用于单体应用系统架构,如果是传统的企业级应用系统需要生成全局唯一的ID/编号,那这种方式应该没啥问题!

2、基于SnowFlake算法

SnowFlake算法就不作过多介绍了,完整的介绍可以到开源网站github上进行阅览:https://github.com/souyunku/SnowFlake,其核心思想在于“分段”,并基于高效的位操作加以实现,感兴趣的小伙伴可以去研究研究它的源码,在此debug就简单介绍介绍吧:

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

共64位,第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年),然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点) ,最后12位是毫秒内的计数(12位的计数顺序可以支持每个节点每毫秒产生4096个ID序号);一共加起来刚好64位,为一个Long型(转换成字符串长度为18)

SnowFlake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高,据说:snowflake每秒能够产生26万个ID;下面简单进入实战吧(借助Hutool工具即可):

@Autowired
private QrCodeMapper qrCodeMapper;

//随机的后6位商品编号,毫秒级上限为999999,应该是满足的 (只要间隔一定的频率重新发布/重启应用时,则当前计数器将重置为 100000)
private static AtomicLong atomicLong=new AtomicLong(100000);

@PostMapping("generate/code/v2")
public BaseResponse generateCodeV2(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        //方式二:雪花算法(单体/分布式)
        qrCodeMapper.insertSelective(new QrCode(generateCodeInV2()));
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}

private static final Snowflake snowflake=new Snowflake(5,5);

//全局唯一编码 - 雪花算法
private String generateCodeInV2(){
    //为了凑够20位
    return snowflake.nextIdStr()+RandomStringUtils.randomNumeric(1);
}

直接压测一番,然后看结果吧:

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 


干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 

3、Redis的INCRBY操作

我们仍然假设 N=6,即可以将其初始值设定为99999,然后通过INCRBY命令对应的操作不断进行 +1 操作;此种方式主要是利用Redis的命令具有原子操作的特性(单线程,但支持并发),因此在分布式高并发的场景下这一方式是顶得住的;

只不过仍然需要设定一个检测机制,判断是否已经达到了 999999 ,如果是,则需要将其重置回 99999 (由于前半段的数据格式精确到毫秒ms,因此可能会出现的差错也是毫秒级别的出错概率);

如下代码所示:

@Autowired
private RedisTemplate redisTemplate;

private static final String RedisKeyCode="sb:technology:code:v1";

private static final Long LimitMaxCode=1000L;

private static final Long InitKeyCode=99L;

//每次项目重启都可以将其重置为初始值
@PostConstruct
public void init(){
    redisTemplate.delete(RedisKeyCode);

    redisTemplate.opsForValue().set(RedisKeyCode,InitKeyCode);
}

//全局唯一编码 - Redis:要使用 Redis的 INCRBY命令,需要设置缓存中key的序列化机制为://StringRedisSerializer;
//不然会出现:ERR value is not an integer or out of range
private String generateCodeInV3(){
    SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
    Long currCode=redisTemplate.opsForValue().increment(RedisKeyCode,1L);
    //编码上限/阈值检测机制
    if (Objects.equals(LimitMaxCode,currCode)){
        redisTemplate.opsForValue().set(RedisKeyCode,InitKeyCode);
        currCode=redisTemplate.opsForValue().increment(RedisKeyCode,1L);
    }
    return format.format(new Date()) + currCode;
}

为了压测方便,debug将其中的N=3,即初始值为99,从100开始计数,最大编码上限为1000(不能取到),这样的话,JMeter压测时QPS设置10000时就会有很多次达到1000,触发重置机制,如下图所示为QPS=10000时的压测结果:

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 


干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 

至此,也完成了此种方式的代码实战,感兴趣的小伙伴可以撸一撸!!!

4、基于ZooKeeper的节点版本号生成ID

这种方式的话得需要大致知晓ZooKeeper底层的系统架构和数据的存储结构,其数据存储结构可以简单地理解为“类Windows操作系统的文件目录结构树”,即多节点ZNode树形结构,节点与节点之间串成一路径Path,以此用于区分、标识存储的唯一数据;

在这里debug是基于zk本身节点的版本号来构成全局唯一ID、编码的,话不多说,直接上代码吧:

//zookeeper生成全局唯一标志符的方式
private static final String ID_NODE = "/QRCodeV2";

//zk客户端实例
@Autowired
private CuratorFramework client;

//全局唯一编码 - zookeeper
private String generateCodeInV4() throws Exception{
    if (null == client.checkExists().forPath(ID_NODE)) {
        //PERSISTENT(0, false, false) 持久型节点; PERSISTENT_SEQUENTIAL(2, false, true) 持久顺序型节点;
        //EPHEMERAL(1, true, false) 临时型节点;EPHEMERAL_SEQUENTIAL(3, true, true) 临时顺序型节点;
        client.create().withMode(CreateMode.PERSISTENT).forPath(ID_NODE, new byte[0]);
}

    //根据节点的版本号-从0开始递增的,因此位数也是不断在变化的(只要path不变)
    Stat stat = client.setData().forPath(ID_NODE,new byte[0]);
    SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");

    return format.format(new Date()) + stat.getVersion();
}

其中,要注意的是ZooKeeper客户端实例CuratorFramework相关属性的自定义注入与配置,如下所示:

@Configuration
public class ZooKeeperConfig {
    private static final String ZK_ADDRESS = "127.0.0.1:2181";

    @Bean
    public CuratorFramework curatorFramework(){
        //如果获取链接失败,则重试3次,每次间隔2s
        RetryPolicy policy=new RetryNTimes(3,2000);
        //获取链接到zk服务的客户端实例
        CuratorFramework curatorFramework= CuratorFrameworkFactory.builder()
                .connectString(ZK_ADDRESS).sessionTimeoutMs(5000).connectionTimeoutMs(10000)
                .retryPolicy(policy).build();
        //启动客户端
        curatorFramework.start();
        return curatorFramework;
}
}

需要在本地127.0.0.1这里将zookeeper服务开起来,如果是windows环境下的,可以来这里下载:http://www.fightjava.com/web/index/resource/10,双击bin目录里面的zkServer.cmd 即可开心的耍起来!!!

如下所示为最终的压测结果:

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 


干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

 

总结:

(1)代码下载:关注“程序员实战基地”微信公众号,回复“分布式id”,即可获取代码下载链接。

(2)至此,我们已经介绍完了N种“全局唯一ID/编码”实现方式的介绍以及在分布式场景下的代码实战实现;具体要选择哪一种,还是那句老话:视具体的业务场景、服务器配置以及技术人员的技术掌握程度进行抉择;在本文debug有提供了一种单体下的实现方式、也提供了多种分布式场景下的实现方式(当然啦,既然是分布式,也就适用于单体的场景啦),话不多说,诸位年轻人还是亲自上去撸一撸吧!



Tags:分布式   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
随着移动互联网技术的快速发展,在新业务、新领域、新场景的驱动下,基于传统大型机的服务部署方式,不仅难以适应快速增长的业务需求,而且持续耗费高昂的成本,从而使得各大生产厂商...【详细内容】
2021-12-08  Tags: 分布式  点击:(23)  评论:(0)  加入收藏
概述以前参加过一个库存系统,由于其业务复杂性,搞了很多个应用来支撑。这样的话一份库存数据就有可能同时有多个应用来修改库存数据。比如说,有定时任务域xx.cron,和SystemA域...【详细内容】
2021-11-05  Tags: 分布式  点击:(31)  评论:(0)  加入收藏
本月 12 日,谷歌召开了 Google Cloud Next '21 年度大会。在这场大会上,谷歌宣布推出Google Distributed Cloud(谷歌分布式云计算),这是一套软硬件结合的解决方案,用于将谷歌...【详细内容】
2021-10-29  Tags: 分布式  点击:(29)  评论:(0)  加入收藏
一、概述手动实现一款轻量,高效的RPC框架,基于TCP的二进制协议实现github源码: https://github.com/wosn00/srpc二、特征 基于netty的主从Reactor模型,NIO通信 支持同步,异步,携带...【详细内容】
2021-10-26  Tags: 分布式  点击:(38)  评论:(0)  加入收藏
一个软件系统随着功能越来越多,调用量急剧增长,整个系统逐渐碎片化,越来越无序,最终无法维护和扩展,所以系统在一段时间的野蛮生长后,也需要及时干预,避免越来越无序。架构的本质就...【详细内容】
2021-09-24  Tags: 分布式  点击:(47)  评论:(0)  加入收藏
作者 | mushishi来源 | urlify.cn/Mry6biredis分布式锁基本原理采用 redis 实现分布式锁,主要是利用其单线程命令执行的特性,一般是 setnx, 只会有一个线程会执行成功,也就是只...【详细内容】
2021-07-07  Tags: 分布式  点击:(105)  评论:(0)  加入收藏
家纯 阿里技术 一 什么是 Netty? 能做什么? Netty 是一个致力于创建高性能网络应用程序的成熟的 IO 框架。 相比较与直接使用底层的 Java IO API,你不需要先成为网络专家就...【详细内容】
2021-06-23  Tags: 分布式  点击:(136)  评论:(0)  加入收藏
今天是六一儿童节,蚂蚁选择在今天开源 OceanBase,想必是给各位分布式数据库用户送上的儿童节礼物吧!昨日凌晨蚂蚁已将代码推送到 GitHub:https://github.com/oceanbase/oceanb...【详细内容】
2021-06-02  Tags: 分布式  点击:(142)  评论:(0)  加入收藏
蚂蚁集团自研数据库OceanBase已经开源,这对国产分布式数据库来说,是一个重磅消息。一直以来OceanBase作为商业数据库,披露的技术细节并不多,以后又多了一个可以拿来研究的优秀...【详细内容】
2021-06-02  Tags: 分布式  点击:(153)  评论:(0)  加入收藏
在我们的服务器上通常会生成各种日志文件,比如系统日志、 应用日志、安全日志。当系统发生故障时,工程师需要登录到服务器上,在日志里查找故障原因。如果定位到处理请求的服务...【详细内容】
2021-06-01  Tags: 分布式  点击:(159)  评论:(0)  加入收藏
▌简易百科推荐
为了构建高并发、高可用的系统架构,压测、容量预估必不可少,在发现系统瓶颈后,需要有针对性地扩容、优化。结合楼主的经验和知识,本文做一个简单的总结,欢迎探讨。1、QPS保障目标...【详细内容】
2021-12-27  大数据架构师    Tags:架构   点击:(3)  评论:(0)  加入收藏
前言 单片机开发中,我们往往首先接触裸机系统,然后到RTOS,那么它们的软件架构是什么?这是我们开发人员必须认真考虑的问题。在实际项目中,首先选择软件架构是非常重要的,接下来我...【详细内容】
2021-12-23  正点原子原子哥    Tags:架构   点击:(7)  评论:(0)  加入收藏
现有数据架构难以支撑现代化应用的实现。 随着云计算产业的快速崛起,带动着各行各业开始自己的基于云的业务创新和信息架构现代化,云计算的可靠性、灵活性、按需计费的高性价...【详细内容】
2021-12-22    CSDN  Tags:数据架构   点击:(10)  评论:(0)  加入收藏
▶ 企业级项目结构封装释义 如果你刚毕业,作为Java新手程序员进入一家企业,拿到代码之后,你有什么感觉呢?如果你没有听过多模块、分布式这类的概念,那么多半会傻眼。为什么一个项...【详细内容】
2021-12-20  蜗牛学苑    Tags:微服务   点击:(8)  评论:(0)  加入收藏
我是一名程序员关注我们吧,我们会多多分享技术和资源。进来的朋友,可以多了解下青锋的产品,已开源多个产品的架构版本。Thymeleaf版(开源)1、采用技术: springboot、layui、Thymel...【详细内容】
2021-12-14  青锋爱编程    Tags:后台架构   点击:(20)  评论:(0)  加入收藏
在了解连接池之前,我们需要对长、短链接建立初步认识。我们都知道,网络通信大部分都是基于TCP/IP协议,数据传输之前,双方通过“三次握手”建立连接,当数据传输完成之后,又通过“四次挥手”释放连接,以下是“三次握手”与“四...【详细内容】
2021-12-14  架构即人生    Tags:连接池   点击:(16)  评论:(0)  加入收藏
随着移动互联网技术的快速发展,在新业务、新领域、新场景的驱动下,基于传统大型机的服务部署方式,不仅难以适应快速增长的业务需求,而且持续耗费高昂的成本,从而使得各大生产厂商...【详细内容】
2021-12-08  架构驿站    Tags:分布式系统   点击:(23)  评论:(0)  加入收藏
本系列为 Netty 学习笔记,本篇介绍总结Java NIO 网络编程。Netty 作为一个异步的、事件驱动的网络应用程序框架,也是基于NIO的客户、服务器端的编程框架。其对 Java NIO 底层...【详细内容】
2021-12-07  大数据架构师    Tags:Netty   点击:(16)  评论:(0)  加入收藏
前面谈过很多关于数字化转型,云原生,微服务方面的文章。虽然自己一直做大集团的SOA集成平台咨询规划和建设项目,但是当前传统企业数字化转型,国产化和自主可控,云原生,微服务是不...【详细内容】
2021-12-06  人月聊IT    Tags:架构   点击:(23)  评论:(0)  加入收藏
微服务看似是完美的解决方案。从理论上来说,微服务提高了开发速度,而且还可以单独扩展应用的某个部分。但实际上,微服务带有一定的隐形成本。我认为,没有亲自动手构建微服务的经历,就无法真正了解其复杂性。...【详细内容】
2021-11-26  GreekDataGuy  CSDN  Tags:单体应用   点击:(35)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条