前言
关于什么是分布式ID
数据量不是很多的时候,单一个数据库表可以支撑其业务,即使数据在大也可以主从复制
到一定量的数据时,实现分库分表的时候,就需要一个全局唯一的ID,订单的编号就是分布式ID
关于上面牵扯到的主从复制
可看我之前的文章进行查缺补漏
关于主从复制的超详细解析(全)
关于数据库的分布式ID可看我之前在Mycat种提及到
具体都有如下:
在实现分库分表的情况下,数据库自增主键已无法保证自增主键的全局唯一。为此,Mycat 提供了全局 sequence
- 本地文件
优点:本地加载,读取速度较快
缺点:抗风险能力差,Mycat 所在主机宕机后,无法读取本地文件
- 数据库方式
利用数据库一个表 来进行计数累加。但是并不是每次生成序列都读写数据库,这样效率太低。Mycat 会预加载一部分号段到 Mycat 的内存中,这样大部分读写序列都是在内存中完成的。如果内存中的号段用完了 Mycat 会再向数据库要一次
- 时间戳方式
全局序列ID= 64 位二进制 (42(毫秒)+5(机器 ID)+5(业务编码)+12(重复累加) 换算成十进制为 18 位数的
long 类型,每毫秒可以并发 12 位二进制的累加。
优点:配置简单
缺点:18 位 ID 过长
而涉及到程序上实现的分布式ID可看下面的文章
可通过UUID、雪花算法等
1. UUID
具体UUID是什么以及可看我之前的文章
java之UUID.randomUUID().toString()详细解析(全)
这里直接push过来
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,主要是让让分布式系统中的所有元素,都能有唯一的辨识信息。
- 为了提高效率,常用的UUID可缩短至16位比特数值
- 使用UUID的一个好处是可以为新的服务创建新的标识符
==源码==
生成的UUID标识符16位具体信息如下:
- (1)当前日期和时间,值第一部分与时间有关(如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同)
- (2)时钟序列
- (3)全局唯一的IEEE机器识别号(如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得)
public static UUID randomUUID() {
SecureRandom ng = Holder.numberGenerator;
byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}
==通过randomUUID().toString()生成==
随机生成UUID的标识符是UUID类中的方法
而UUID.randomUUID().toString()是javaJDK提供的一个自动生成主键的方法
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
List<String> list = new ArrayList<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString());
//从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
代码截图
2. 数据库自增
前言中也有提及到数据库的方式,但是mycat的方式是放一定的序列到内存中,如果宕机之后,前面的序列不会更改,但是刚开始的序列号和之前的序列号会断开,也可以保证高可用
这种数据库的自增是通过数据库种的auto_increment,
用它来实现分布式服务风险比较大
3. 数据库集群
改进上面单个数据库自增的模式,因为怕宕机
那就多加入一个从机,实现主从复制或者是双主模式等
具体的规则就是设置一个起始值和步长值,实现两个mysql的不一样值
具体设置的方法为:
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 2; -- 步长
但是如果突然加进来第三个服务器,前面两个序列号就都要重改,也比较麻烦
4. 数据库号段
从数据库批量的获取自增ID,每次从数据库取出一个号段范围,生成的自增ID加载到内存中
这与一开始前言所提及到的mycat比较相像
但是mycat是已经把一部分号段放置到内存中
而这里是在数据库取一部分,再放置到内存中,逻辑不一样
加入号段之后,在每个号段还要加上乐观锁(在数据库表中增加一个version字段),防止高并发的数据量
关于这部分乐观锁可看我之前的文章补充一些知识点,这里就不再赘述
java中各类锁的机制详细解析(全)
不会频繁的访问数据库,对数据库的压力小很多,因为有了缓存在内存中了
5. redis模式
利用redis的 incr命令实现ID的原子性自增
考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF:
(1)RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
(2)AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。
6. 雪花算法
指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)
组成部分(64bit)
1.第一位 占用1bit,其值始终是0,没有实际作用。
2.时间戳 占用41bit,精确到毫秒,总共可以容纳约69年的时间。
3.工作机器id 占用10bit,其中高位5bit是数据中心ID,低位5bit是工作节点ID,做多可以容纳1024个节点。
4.序列号 占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID。
SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢: 同一毫秒的ID数量 = 1024 X 4096 = 4194304
==最核心的是中间的10位工作机器ID的分配,做到自动生成workID,避免运维人员的去分配==
关于雪花算法详细分析可看这篇文章
理解分布式id生成算法SnowFlake
7. 其他
关于其他的自增,分布式ID,各大公司都有其一套的的算法
比如:
- 滴滴出品(TinyID)
数据库中保存了可用的id号段,tinyid会将可用号段加载到内存中,之后生成id会直接内存中产生
- 百度 (Uidgenerator)
百度的雪花算法是通过数据库来生成workID
- 美团(Leaf)
美团是通过Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID
总结
方法 | 优点 | 缺点 |
---|---|---|
UUID | 1.容易实现,产生快。2.ID唯一。3.无需要中心化服务器。4.不会泄露商业机密 | 1.可读性过差。2.占用空间过多。3.影响数据库性能 |
数据库自增 | 实现简单,ID单调自增,数值类型查询速度快。 | 单点存在宕机风险,无法扛住高并发场景。 |
数据库集群 | 解决DB单点问题 | 不利于后续扩容,单点压力也会更大不适合高并发 |
数据库号段 | 解决单点压力 | 抗风险能力差,没有集群 |
redis中的icncr | 原子性自增保证不重复 | RDB挂掉后出现重复,AOF挂掉重启时间长 |
雪花算法 | 所有生成的id按时间趋势递增。不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。可以根据自身业务特性分配bit位,非常灵活。 | 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。 |