本文由 简悦 SimpRead 转码, 原文地址 https://mp.weixin.qq.com/s/0RBeWV-any_Rb9JbVPvcfw
点击 “ 程序员内点事 ” 关注,选择 “ 设置星标 ”
坚持学习,好文每日送达!
写在前边
前两天公众号有个粉丝给我留言吐槽最近面试:“四哥,年前我在公司受点委屈一冲动就裸辞了,然后现在疫情严重两个多月还没找到工作,接了几个视频面试也都没下文。好多面试官问完一个问题,紧接着说还会其他解决方法吗?**能干活解决 bug 不就行了吗?那还得会多少种方法?**”
面试官应该是对应聘者的回答不太满意,他想听到一个他认为最优的解决方案,其实这无可厚非。同样一个 bug,能用一行代码解决问题的人和用十行代码解决问题的人,你会选哪个入职?显而易见的事情!所以看待问题还是要从多个角度出发,每种方法都有各自的利弊。
一、为什么要用分布式 ID?
在说分布式 ID 的具体实现之前,我们来简单分析一下为什么用分布式 ID?分布式 ID 应该满足哪些特征?
1、什么是分布式 ID?
拿 MySQL 数据库举个栗子:
在我们业务数据量不大的时候,单库单表完全可以支撑现有业务,数据再大一点搞个 MySQL 主从同步读写分离也能对付。
但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一 ID 来标识一条数据,数据库的自增 ID 显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID
做标识。此时一个能够生成全局唯一ID
的系统是非常必要的。那么这个全局唯一ID
就叫分布式ID
。
2、那么分布式 ID 需要满足那些条件?
全局唯一:必须保证 ID 是全局性唯一的,基本要求
高性能:高可用低延时,ID 生成响应要块,否则反倒会成为业务瓶颈
高可用:100% 的可用性是骗人的,但是也要无限接近于 100% 的可用性
好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单
趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求
二、 分布式 ID 都有哪些生成方式?
今天主要分析一下以下 9 种,分布式 ID 生成器方式以及优缺点:
UUID
数据库自增 ID
数据库多主模式
号段模式
Redis
雪花算法(SnowFlake)
滴滴出品(TinyID)
百度 (Uidgenerator)
美团(Leaf)
那么它们都是如何实现?以及各自有什么优缺点?我们往下看
图片源自网络
以上图片源自网络,如有侵权联系删除
1、基于 UUID
在 Java 的世界里,想要得到一个具有唯一性的 ID,首先被想到可能就是UUID
,毕竟它有着全球唯一的特性。那么UUID
可以做分布式ID
吗?答案是可以的,但是并不推荐!
1 | public static void main(String[] args) { |
UUID
的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718
,但 UUID 却并不适用于实际的业务需求。像用作订单号UUID
这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID
,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID
。
优点:
- 生成足够简单,本地生成无网络消耗,具有唯一性
缺点:
无序的字符串,不具备趋势自增特性
没有具体的业务含义
长度过长 16 字节 128 位,36 位长度的字符串,存储以及查询对 MySQL 的性能消耗较大,MySQL 官方明确建议主键要尽量越短越好,作为数据库主键
UUID
的无序性会导致数据位置频繁变动,严重影响性能。
2、基于数据库自增 ID
基于数据库的auto_increment
自增 ID 完全可以充当分布式ID
,具体实现:需要一个单独的 MySQL 实例用来生成 ID,建表结构如下:
1 | CREATE DATABASE `SEQ_ID`; |
1 | insert into SEQUENCE_ID(value) VALUES ('values'); |
当我们需要一个 ID 的时候,向表中插入一条记录返回主键ID
,但这种方式有一个比较致命的缺点,访问量激增时 MySQL 本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!
优点:
- 实现简单,ID 单调自增,数值类型查询速度快
缺点:
- DB 单点存在宕机风险,无法扛住高并发场景
3、基于数据库集群模式
前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个 Mysql 实例都能单独的生产自增 ID。
那这样还会有个问题,两个 MySQL 实例的自增 ID 都从 1 开始,会生成重复的 ID 怎么办?
解决方案:设置起始值
和自增步长
MySQL_1 配置:
1 | set @@auto_increment_offset = 1; -- 起始值 |
MySQL_2 配置:
1 | set @@auto_increment_offset = 2; -- 起始值 |
这样两个 MySQL 实例的自增 ID 分别就是:
1、3、5、7、9
2、4、6、8、10
那如果集群后的性能还是扛不住高并发咋办?就要进行 MySQL 扩容增加节点,这是一个比较麻烦的事。
在这里插入图片描述
从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点压力的问题,同时为了 ID 生成特性,将自增步长按照机器数量来设置。
增加第三台MySQL
实例需要人工修改一、二两台MySQL实例
的起始值和步长,把第三台机器的ID
起始生成位置设定在比现有最大自增ID
的位置远一些,但必须在一、二两台MySQL实例
ID 还没有增长到第三台MySQL实例
的起始ID
值的时候,否则自增ID
就要出现重复了,必要时可能还需要停机修改。
优点:
- 解决 DB 单点问题
缺点:
- 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
4、基于数据库的号段模式
号段模式是当下分布式 ID 生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增 ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表 1000 个 ID,具体的业务服务将本号段,生成 1~1000 的自增 ID 并加载到内存。表结构如下:
1 | CREATE TABLE id_generator ( |
biz_type :代表不同业务类型
max_id :当前最大的可用 id
step :代表号段的长度
version :是一个乐观锁,每次都更新 version,保证并发时数据的正确性
id | biz_type | max_id | step | version |
---|---|---|---|---|
1 | 101 | 1000 | 2000 | 0 |
等这批号段 ID 用完,再次向数据库申请新号段,对max_id
字段做一次update
操作,update max_id= max_id + step
,update 成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]
。
1 | update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX |
由于多业务端可能同时操作,所以采用版本号version
乐观锁方式更新,这种分布式ID
生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
5、基于 Redis 模式
Redis
也同样可以实现,原理就是利用redis
的 incr
命令实现 ID 的原子性自增。
1 | 127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1 |
用redis
实现需要注意一点,要考虑到 redis 持久化的问题。redis
有两种持久化方式RDB
和AOF
RDB
会定时打一个快照进行持久化,假如连续自增但redis
没及时持久化,而这会 Redis 挂掉了,重启 Redis 后会出现 ID 重复的情况。AOF
会对每条写命令进行持久化,即使Redis
挂掉了也不会出现 ID 重复的情况,但由于 incr 命令的特殊性,会导致Redis
重启恢复的数据时间过长。
6、基于雪花算法(Snowflake)模式
雪花算法(Snowflake)是 twitter 公司内部分布式项目采用的 ID 生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。
在这里插入图片描述
以上图片源自网络,如有侵权联系删除
Snowflake
生成的是 Long 类型的 ID,一个 Long 类型占 8 个字节,每个字节占 8 比特,也就是说一个 Long 类型占 64 个比特。
Snowflake ID 组成结构:正数位
(占 1 比特)+ 时间戳
(占 41 比特)+ 机器ID
(占 5 比特)+ 数据中心
(占 5 比特)+ 自增值
(占 12 比特),总共 64 比特组成的一个 Long 类型。
第一个 bit 位(1bit):Java 中 long 的最高位是符号位代表正负,正数是 0,负数是 1,一般生成 ID 都为正数,所以默认为 0。
时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的 ID 从更小的值开始;41 位的时间戳可以使用 69 年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69 年
工作机器 id(10bit):也被叫做
workId
,这个可以灵活配置,机房或者机器号组合都可以。序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成 4096 个 ID
根据这个算法的逻辑,只需要将这个算法用 Java 语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式 ID,只需保证每个业务应用有自己的工作机器 id 即可,而不需要单独去搭建一个获取分布式 ID 的应用。
Java 版本的Snowflake
算法实现:
1 | /** |
7、百度(uid-generator)
uid-generator
是由百度技术部开发,项目 GitHub 地址 https://github.com/baidu/uid-generator
uid-generator
是基于Snowflake
算法实现的,与原始的snowflake
算法不同在于,uid-generator
支持自定义时间戳
、工作机器ID
和 序列号
等各部分的位数,而且uid-generator
中采用用户自定义workId
的生成策略。
uid-generator
需要与数据库配合使用,需要新增一个WORKER_NODE
表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增 ID 就是该机器的workId
数据由 host,port 组成。
对于uid-generator
ID 组成结构:
workId
,占用了 22 个 bit 位,时间占用了 28 个 bit 位,序列化占用了 13 个 bit 位,需要注意的是,和原始的snowflake
不太一样,时间的单位是秒,而不是毫秒,workId
也不一样,而且同一应用每次重启就会消费一个workId
。
参考文献
https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
8、美团(Leaf)
Leaf
由美团开发,github 地址:https://github.com/Meituan-Dianping/Leaf
Leaf
同时支持号段模式和snowflake
算法模式,可以切换使用。
号段模式
先导入源码 https://github.com/Meituan-Dianping/Leaf ,在建一张表leaf_alloc
1 | DROP TABLE IF EXISTS `leaf_alloc`; |
然后在项目中开启号段模式
,配置对应的数据库信息,并关闭snowflake
模式
1 | leaf.name=com.sankuai.leaf.opensource.test |
启动leaf-server
模块的 LeafServerApplication
项目就跑起来了
号段模式获取分布式自增 ID 的测试 url :http://localhost:8080/api/segment/get/leaf-segment-test
监控号段模式:http://localhost:8080/cache
snowflake 模式
Leaf
的 snowflake 模式依赖于ZooKeeper
,不同于原始snowflake
算法也主要是在workId
的生成上,Leaf
中workId
是基于ZooKeeper
的顺序 Id 来生成的,每个应用在使用Leaf-snowflake
时,启动时都会都在Zookeeper
中生成一个顺序 Id,相当于一台机器对应一个顺序节点,也就是一个workId
。
1 | leaf.snowflake.enable=true |
snowflake 模式获取分布式自增 ID 的测试 url:http://localhost:8080/api/snowflake/get/test
9、滴滴(Tinyid)
Tinyid
由滴滴开发,Github 地址:https://github.com/didi/tinyid。
Tinyid
是基于号段模式原理实现的与Leaf
如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]
在这里插入图片描述
Tinyid
提供http
和tinyid-client
两种方式接入
Http 方式接入
(1)导入 Tinyid 源码:
git clone https://github.com/didi/tinyid.git
(2)创建数据表:
1 | CREATE TABLE `tiny_id_info` ( |
(3)配置数据库:
1 | datasource.tinyid.names=primary |
(4)启动tinyid-server
后测试
1 | 获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c' |
Java 客户端方式接入
重复 Http 方式的(2)(3)操作
引入依赖
1 | <dependency> |
配置文件
1 | tinyid.server =localhost:9999 |
test
、tinyid.token
是在数据库表中预先插入的数据,test
是具体业务类型,tinyid.token
表示可访问的业务类型
1 | // 获取单个分布式自增ID |
总结
本文只是简单介绍一下每种分布式 ID 生成器,旨在给大家一个详细学习的方向,每种生成方式都有它自己的优缺点,具体如何使用还要看具体的业务需求。
今天就说这么多,如果本文对您有一点帮助,希望能得到您一个点赞👍哦
您的认可才是我写作的动力!
☆ END ☆
技术 / 面试 / 吐槽
程序员内点事这都有
长按扫码可关注
在看点这里