业务量小于500W的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来。

但当主从同步也扛不住的是就需要分表分库了,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;

特别一点的如订单、优惠券也都需要有唯一ID做标识。

此时一个能够生成全局唯一ID的系统是非常必要的。

那么这个全局唯一ID就叫分布式ID,分布式ID需满足哪些条件?

条件解释全局唯一基本要求就是必须保证ID是全局性唯一的。高性能高可用低延时,ID生成响应要快。高可用无限接近于100%的可用性好接入遵循拿来主义原则,在系统设计和实现上要尽可能的简单趋势递增最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求

比如我们的身份证,就完美地满足了分布式ID的特性:

组成特点2位省级或直辖市编码全局唯一4位地区编码非唯一8位出生日期非唯一(时钟回退),递增顺序码(2位出生序列+1位性别)非唯一,递增1位校验码验证组合序列的正确性

一言以蔽之:

不过由于身份证是18位的10进制数最多表示 条数据,这在计算机的二进制条件下是远远不够的。

UUID (Universally Unique Identifier),通用唯一识别码。

UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。

UUID由以下几部分的组合:

  1. 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
  2. 时钟序列。
  3. 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

UUID 是由一组32位数的16进制数字所构成「」,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。

 
优点缺点简单、代码方便没有排序,无法保证趋势递增性能好,全球唯一使用字符存储,查询效率低本地生成无网络消耗占用空间大,传输效率低

如果需求是只保证唯一性,那么UUID也是可以使用的,但是按照上面的分布式id的要求, UUID其实是不能做成分布式id的,原因如下:

  1. 首先分布式id一般都会作为主键,但是安装mysql官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐。
  2. 既然分布式id是主键,然后主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键生成的b+树进行很大的修改,带来很大压力,这一点很不好。
  3. 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

对于第二条:UUID的无序产生的IO压力,可以将UUID作为逻辑主键,物理主键仍然使用自增ID进行解决。

UUID生成策略:

UUID Version 1:基于时间的UUID

基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。
由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。
但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。
如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)

UUID Version 2:DCE安全的UUID

DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。
这个版本的UUID在实际中较少用到。

UUID Version 3:基于名字的UUID(MD5)

基于名字的UUID通过计算名字和名字空间的MD5散列值得到。
这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;
不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。

UUID Version 4:随机UUID

根据随机数,或者伪随机数生成UUID。
这种UUID产生重复的概率是可以计算出来的,
但随机的东西就像是买彩票:你指望它发财是不可能的,但狗屎运通常会在不经意中到来。

UUID Version 5:基于名字的UUID(SHA1)

和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

针对表结构的主键,我们常规的操作是在创建表结构的时候给对应的ID设置 也就是自增选项。

但是这种方式我们清楚在单个数据库的场景中我们是可以这样做的,但如果是在分库分表的环境下。

直接利用单个数据库的自增肯定会出现问题。

因为ID要唯一,但是分表分库后只能保证一个表中的ID的唯一,而不能保证整体的ID唯一。

在这里插入图片描述

上面的情况我们可以通过单独创建主键维护表来处理。

在这里插入图片描述

举个例子来看看:

创建一个表结构

 

然后我们通过更新ID操作来获取ID信息

 
优点缺点实现简单,ID单调自增DB单点存在宕机风险数值类型查询速度快无法扛住高并发场景

单点数据库方式存在明显的性能问题,可以对数据库进行高可以优化,担心一个主节点挂掉没法使用,可以选择做双主模式集群,也就是两个MySQL实例都能单独生产自增的ID。

查看主键自增的属性

 
Variable_nameValueauto_increment_increment1 (步长)auto_increment_offset1(起始值)div_precision_increment4innodb_autoextend_increment64

我们可以设置主键自增的步长从2开始,这样两个MySQL实例的自增ID分别就是:

在这里插入图片描述

但是如果两个还是无法满足咋办呢?

增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些。

但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例的起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改。

所以这种在并发量比较高的情况下,如何保证扩展性其实会是一个问题。

在高并发情况下无能为力,依旧无法满足高并发场景。

号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围。

例如 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。

表结构如下:

 

字段说明:

biz_type :代表不同业务类型

max_id :当前最大的可用id

step :代表号段的长度

version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

等这批号段ID用完,再次向数据库申请新号段,对 max_id 字段做一次 update 操作,update max_id = max_id + step,update 成功则说明新号段获取成功,新的号段范围是。

 

在这里插入图片描述

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

但同样也会存在一些缺点比如:服务器重启,单点故障会造成ID不连续。

基于全局唯一ID的特性,我们可以通过RedisINCR命令来生成全局唯一ID。

在这里插入图片描述

同样使用Redis也有对应的缺点:

  1. RDB 数据备份存在丢失数据的问题,理论上ID可能重复。
  2. ID 生成的持久化问题,加上AOF 性能也会存在损耗。
  3. 如果Redis宕机了怎么进行恢复,单个节点宕机问题。

当然针对故障问题我们可以通过Redis集群来处理,比如我们有三个Redis的Master节点。

可以初始化每台Redis的值分别是1,2,3;然后分别把分布式ID的KEY用Hash Tags固定每一个master节点,步长就是master节点的个数。各个Redis生成的ID为:

  • A:1,4,7

  • B:2,5,8

  • C:3,6,9

优点缺点不依赖于数据库,灵活方便,且性能优于数据库如果没有Redis数据库,需要安装配置,增加复杂度数字ID有序,对分页处理和排序都很友好分布式环境下,数据一致性策略需要考虑防止了Redis的单机故障集群节点确定是3个后,后面调整不是很友好

Redis分布式ID的简单案例

 
 

原理:利用zookeeper中的顺序节点的特性,制作分布式的序列号生成器(ID生成器)。

  • 在ID下创建持久的顺序节点。
  • 获取返回的节点名称。
  • 删除该节点。
 
 

ETCD 生成全局唯一 ID 的原理:

  • 在 ETCD 中,每个事务(tx)都有一个唯一的事务 ID,称为 main ID。这个 main ID 是全局递增且不重复的。
  • 一个事务可以包含多个修改操作(如 和 ),每个操作称为一个 revision(修订)。这些修订共享同一个 main ID
  • 在一个事务内,连续的多个修改操作会被从 0 开始递增编号,这个编号称为 sub ID。每个 revision(修订) 都有一个唯一的 「main ID,sub ID」 组成唯一标识。

Revision的定义:

// A revision indicates modification of the key-value space.
// The set of changes that share same main revision changes the key-value space atomically. type revision struct {
// main is the main revision of a set of changes that happen atomically. main int64
// sub is the the sub revision of a change in a set of changes that happen
// atomically. Each change has different increasing sub revision in that
// set. sub int64 }

Snowflake,雪花算法是有Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64bit 位分割成了多个部分,每个部分都有具体的不同含义。

在Java中64Bit位的整数是Long类型,所以在Java中Snowflake算法生成的ID就是long来存储的,具体如下:

在这里插入图片描述

  • 第一部分:占用1bit,第一位为符号位,无意义。因为二进制里第一个 bit 位如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

  • 第二部分:41位的时间戳。41bit位可以表示 个数,每个数代表的是毫秒,那么雪花算法的时间年限是年。

  • 第三部分:5位的机房id。意思就是最多代表 个机房(32 个机房)。

  • 第四部分:5位的机器id。每个机房里可以有 个机器(32 台机器),也可以根据自己公司的实际情况确定。

  • 第五部分:12bit位是自增序列。可以表示个数,一秒内可以生成4096个ID。

案例代码:

 
优点缺点不依赖于数据库,完全在内存中生成,性能好。严重依赖与系统时间的一致性,每秒中能生成数百万的自增ID,容量大。如果系统时间被回调,或者改变,存入数据库中,索引效率高,ID自增。可能会造成id冲突或者重复。

实际生产环境中我们应该怎么来应用雪花算法来实现分布式ID——实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化成业务表或者和我们系统相关的业务。

在这里插入图片描述

时针回拨问题【20:28 --》20:26】 【20:28 --》 20:30】:

问题场景解决方案?回拨时间很短:(<=100毫秒)直接阻塞100毫秒回拨时间适中:(>100毫秒 <500毫秒)维护这500毫秒时间戳的最大ID信息回拨时间比较长:(>=500毫秒 <1000毫秒)通过分布式ID服务器的轮询处理回拨时间很长:(>=1000毫秒)直接下线

源码地址:https://github.com/baidu/uid-generator。

中文文档地址:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。

它是分布式的,并克服了雪花算法的并发限制。

UidGenerator以组件形式工作在应用项目中,支持自定义workerId位数和初始化策略,从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。

在实现上,UidGenerator通过借用未来时间来解决sequence天然存在的并发限制;

采用RingBuffer来缓存已生成的UID,并行化UID的生产和消费,同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题,最终单机QPS可达600万。

其实现原理和雪花算法并无二致,自定义号段,并且采用RingBuffer作为缓冲 从而提升性能。

需要的环境:JDK8+,MySQL(用于分配WorkerId)。

百度的Uidgenerator对结构做了部分的调整,具体如下:

在这里插入图片描述

UidGenerator的时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年()。

也可以根据你业务的需求,UidGenerator可以适当调整、和占用位数。

摘自官网 CachedUidGenerator

RingBuffer环形数组,数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值,且为。可通过配置进行扩容,以提高RingBuffer 读写吞吐量。

Tail指针、Cursor指针用于环形数组上读写slot:

  • Tail指针
    表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上curosr,此时可通过指定PutRejectPolicy

  • Cursor指针
    表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过指定TakeRejectPolicy

在这里插入图片描述

CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)

由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine 补齐方式。

在这里插入图片描述

RingBuffer填充时机

  • 初始化预填充
    RingBuffer初始化时,预先填充满整个RingBuffer.

  • 即时填充
    Take消费时,即时检查剩余可用slot量(- ),如小于设定阈值,则补全空闲slots。阈值可通过来进行配置,请参考Quick Start中CachedUidGenerator配置

  • 周期填充
    通过Schedule线程,定时补全空闲slots。可通过配置,以应用定时填充功能,并指定Schedule时间间隔

由美团开发,开源项目链接:https://github.com/Meituan-Dianping/Leaf,相关博客链接:https://tech.meituan.com/2017/04/21/mt-leaf.html。

Leaf同时支持号段模式snowflake算法模式,可以切换使用。

ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。

Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。

 

Leaf的号段模式是对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的频率操作。

相当于从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。

特性:

  1. 全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。

  2. 高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。

  3. 高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。

  4. 接入简单,直接通过公司RPC服务或者HTTP调用即可接入。

Leaf 采用双 buffer 的方式,它的服务内部有两个号段缓存区segment

当前号段已消耗10%时,还没能拿到下一个号段,则会另启一个更新线程去更新下一个号段,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

在这里插入图片描述

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS(秒处理事务数)的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

简而言之就是Leaf保证了总是会多缓存两个号段,即便哪一时刻数据库挂了,也会保证发号服务可以正常工作一段时间。

 

配置:

 
 

由滴滴开发,开源项目链接:https://github.com/didi/tinyid。

Tinyid是在美团(Leaf)的算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client客户端的接入方式,使用起来更加方便。

但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。

Tinyid提供了两种调用方式,一种基于Tinyid-server提供的http方式,另一种Tinyid-client客户端方式。

每个服务获取一个号段、、。

特性:

  1. 全局唯一的long型ID

  2. 趋势递增的id

  3. 提供 http 和 java-client 方式接入

  4. 支持批量获取ID

  5. 支持生成 1,3,5,7,9… 序列的ID

  6. 支持多个db的配置

适用场景不适用场景只关心ID是数字像类似于订单ID的业务趋势递增的系统因生成的ID大部分是连续的可以容忍ID不连续容易被扫库可以容忍ID的浪费或者推算出订单量等信息
更新日期: 2024年10月22日
文章链接: https://fzjwsw.com/post/11.html