# 分布式ID
分布式ID的两大核心需求:
- **全局唯一**
- **趋势有序**
- **高性能**
## UUID
基于 `UUID` 实现全球唯一的ID。用作订单号`UUID`这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务`主键ID`,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作`分布式ID`。
**优点**
- 生成足够简单,本地生成无网络消耗,具有唯一性
**缺点**
- 无序的字符串,不具备趋势自增特性
- 没有具体的业务含义,看不出和订单相关的有用信息
- 长度过长16 字节128位,36位长度的字符串,存储以及查询对MySQL的性能消耗较大,MySQL官方明确建议主键要尽量越短越好,作为数据库主键 `UUID` 的无序性会导致数据位置频繁变动,严重影响性能
**适用场景**
- 可以用来生成如token令牌一类的场景,足够没辨识度,而且无序可读,长度足够
- 可以用于无纯数字要求、无序自增、无可读性要求的场景
## 数据库自增ID
基于数据库的 `auto_increment` 自增ID完全可以充当 `分布式ID` 。当我们需要一个ID的时候,向表中插入一条记录返回`主键ID`,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐。相关SQL如下:
```mysql
CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
id bigint(20) unsigned NOT NULL auto_increment,
value char(10) NOT NULL default '',
PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');
```
**优点**
- 实现简单,ID单调自增,数值类型查询速度快
**缺点**
- DB单点存在宕机风险,无法扛住高并发场景
**适用场景**
- 小规模的,数据访问量小的业务场景
- 无高并发场景,插入记录可控的场景
## 数据库多主模式
单点数据库方式不可取,那对上述的方式做一些高可用优化,换成主从模式集群。一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。
**问题**:如果两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?
**解决方案**:设置`起始值`和`自增步长`
MySQL_1 配置:
```mysql
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 2; -- 步长
-- 自增ID分别为:1、3、5、7、9 ......
```
MySQL_2 配置:
```mysql
set @@auto_increment_offset = 2; -- 起始值
set @@auto_increment_increment = 2; -- 步长
-- 自增ID分别为:2、4、6、8、10 ......
```
那如果集群后的性能还是扛不住高并发咋办?则进行MySQL扩容增加节点:
从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点压力的问题,同时为了ID生成特性,将自增步长按照机器数量来设置。增加第三台`MySQL`实例需要人工修改一、二两台`MySQL实例`的起始值和步长,把`第三台机器的ID`起始生成位置设定在比现有`最大自增ID`的位置远一些,但必须在一、二两台`MySQL实例`ID还没有增长到`第三台MySQL实例`的`起始ID`值的时候,否则`自增ID`就要出现重复了,**必要时可能还需要停机修改**。
**优点**
- 解决DB单点问题
**缺点**
- 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景
**适用场景**
- 数据量不大,数据库不需要扩容的场景
这种方案,除了难以适应大规模分布式和高并发的场景,普通的业务规模还是能够胜任的,所以这种方案还是值得积累。
## 数据库号段模式
号段模式是当下分布式ID生成器的主流实现方式之一,可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:
```mysql
CREATE TABLE id_generator (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前最大id',
step int(20) NOT NULL COMMENT '号段的步长',
biz_type int(20) NOT NULL COMMENT '业务类型',
version int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
)
```
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]`。
```mysql
update id_generator set max_id=max_id+${step}, version = version+1 where version=${version} and biz_type=${XXX}
```
由于多业务端可能同时操作,所以采用版本号`version`乐观锁方式更新,这种`分布式ID`生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
## Redis模式
`Redis`也同样可以实现,原理就是利用`redis`的 `incr`命令实现ID的原子性自增。
```shell
# 初始化自增ID为1
127.0.0.1:6379> set seq_id 1
OK
# 增加1,并返回递增后的数值
127.0.0.1:6379> incr seq_id
(integer) 2
```
用`redis`实现需要注意一点,要考虑到`redis`持久化的问题。`redis`有两种持久化方式`RDB`和`AOF`:
- `RDB`:会定时打一个快照进行持久化,假如连续自增但`redis`没及时持久化,而这会`redis`挂掉了,重启`redis`后会出现ID重复的情况
- `AOF`:会对每条写命令进行持久化,即使`Redis`挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致`Redis`重启恢复的数据时间过长
**优点**
- 有序递增,可读性强
- 能够满足一定性能
**缺点**
- 强依赖于Redis,可能存在单点问题
- 占用宽带,而且需要考虑网络延时等问题带来地性能冲击
**适用场景**
- 对性能要求不是太高,而且规模较小业务较轻的场景,而且Redis的运行情况有一定要求,注意网络问题和单点压力问题,如果是分布式情况,那考虑的问题就更多了,所以一帮情况下这种方式用的比较少
Redis的方案其实可靠性有待考究,毕竟依赖于网络,延时故障或者宕机都可能导致服务不可用,这种风险是不得不考虑在系统设计内的。