业务量小于500W的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来。
但当主从同步也扛不住的是就需要分表分库了,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;
特别一点的如订单、优惠券也都需要有唯一ID做标识。
此时一个能够生成全局唯一ID的系统是非常必要的。
那么这个全局唯一ID就叫分布式ID,分布式ID需满足哪些条件?
比如我们的身份证,就完美地满足了分布式ID的特性:
一言以蔽之:。
不过由于身份证是18位的10进制数最多表示 条数据,这在计算机的二进制条件下是远远不够的。
UUID (Universally Unique Identifier),通用唯一识别码。
UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。
UUID由以下几部分的组合:
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列。
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
UUID 是由一组32位数的16进制数字所构成「」,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。
如果需求是只保证唯一性,那么UUID也是可以使用的,但是按照上面的分布式id的要求, UUID其实是不能做成分布式id的,原因如下:
- 首先分布式id一般都会作为主键,但是安装mysql官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐。
- 既然分布式id是主键,然后主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键生成的b+树进行很大的修改,带来很大压力,这一点很不好。
- 信息不安全:基于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信息
单点数据库方式存在明显的性能问题,可以对数据库进行高可以优化,担心一个主节点挂掉没法使用,可以选择做双主模式集群,也就是两个MySQL实例都能单独生产自增的ID。
查看主键自增的属性
我们可以设置主键自增的步长从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的特性,我们可以通过Redis的INCR命令来生成全局唯一ID。
同样使用Redis也有对应的缺点:
- RDB 数据备份存在丢失数据的问题,理论上ID可能重复。
- ID 生成的持久化问题,加上AOF 性能也会存在损耗。
- 如果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的简单案例
原理:利用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——实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化成业务表或者和我们系统相关的业务。
时针回拨问题【20:28 --》20:26】 【20:28 --》 20:30】:
源码地址: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并加载到内存。
特性:
-
全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。
-
高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。
-
高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。
-
接入简单,直接通过公司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客户端方式。
每个服务获取一个号段、、。
特性:
-
全局唯一的long型ID
-
趋势递增的id
-
提供 http 和 java-client 方式接入
-
支持批量获取ID
-
支持生成 1,3,5,7,9… 序列的ID
-
支持多个db的配置