分布式系统之全局唯一ID生成策略 (UUID,基于mysql的replace into基于redis生成全局唯一id,Twitter的分布式自增id雪花算法 )
通过在 生产生活中,分布式的系统以及大数据量的存储和读取都离不开 ID的 唯一性,例如订单号,快递单号,商品编号等等。通常我们都会采用 uuid ,mysql replace into ,
一、 UUID
直接使用 java 代码 本地生成,没有网络消。 但是会存在以下的缺点:
1. 无序,无法预测他的生成顺序,不能生成递增有序的数字。
首先分布式id 一般都会作为主键,但是mysql官方推荐主键要尽量越短越好,uuid 每一个都很长,所以不是很推荐。
2. uuid 作为 主键,在特定的环境下会存在一些问题
比如做 DB主键的场景下,uuid 就非常不适用 mysql,官方有明确的建议主键要尽量越短越好,36个字符长度的uuid不符合要求。
3.索引,b+树索引的分裂
既然分布式id 是 主键,然后主键是包含索引的,然后 mysql 的 索引是通过b+树 来实现的,每一次新的uuid 的数据插入,为了查询的优化,都会对索引底层的b+树进行修改,因为 uuid 是无序的,所以每一次的uuid 数据的插入都会对主键底层的b+树进行很大的修改,这一点很不好,插入完全无序,不但会导致一些中间节点产生分裂,也会自创造出很多不饱和的节点,这样大大降低了数据库插入的性能。
二、mysql 的 replace into 生成唯一id
replcae into 首先尝试 插入数据列表中,如果发现表中已经有此行数据,(根据主键或者唯一索引进行判断)则先删除,后插入。
实现方式:
create table `t_test` (
id bigint(20) unsigned not null auto_increment primary key,
stub char(1) not null default '',
unique key stub(stub)
);
select * from t_test;
replcae into t_test (stub) values ('b') ;
select LAST_INSERT_ID();
这种方式生成的 id 很显然在高并发的场景 下仍然是不太合适作为分布式id 的,一秒内mysql 的性能达不到 10万。
会存在以下缺点:
1.系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做? 假设现在只有一台机器,发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台,可以这样做: 把第二台机器的初始值设置的比第一台机器超过很多,貌似还好,现在想象一下,如果我们线上有100台机器,这个时候要 扩容该怎么做?简直是噩梦,所以系统水平扩展复杂难以实现。
2. 数据库压力还是很大,每次获取id 都得读写一次数据库,非常影响性能,不符合分布式id里面的低延迟和高qps的规则
三 、基于 redis生成 全局唯一id 的策略
因为redis是单线程的天生 保证原子性,可以使用原子操作incr 和 incrby 来实现。
在 redis 集群的情况下, 和 mysql 一样要设置 不同的 增长步长,同时key一定要设置有效期。可以使用redis 集群来获得更高的吞吐量,假如一个集群中有5台redis,可以初始化每台redis 的值分别为1,2,3,4,5,然后步长都是5.
以上 三个生成 全局唯一id 的方式均有优缺点。
四、 Twitter 的分布式自增id 算法snowflake 雪花算法
Snowflake 可以保证:
所有生成的id 按照时间趋势递增,整个分布式系统内不会产生重复id ,因为有 datacenterid 和 workerid来做区分。
项目地址 :https://github.com/twitter-archive/snowflake
hutool 工具包: https://www.hutool.cn/ , https://github.com/looly/hutool
spring boot 整合 雪花算法
(1)pom 文件 添加依赖
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-captcha -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.3.3</version>
</dependency>
(2)
@Component
@Slf4j
public class IdGeneratorSnowflake{
private long workerId = 0 ;
private long datacenterId = 1;
private Snowflake snowflake = IdUtil.createSnowflake(workerId,datacenterId);
@PostConstruct
public void init(){
try{
workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("当前机器的workerId:{}",workerId);
}catch(Exception e){
e.printStackTrace();
log.warn("当前机器的workerId获取失败",e);
workerId = NetUtil.getLocalhostStr().hashCode();
}
}
public synchronized long snowflake(){
return snowflake.nextId();
}
public synchronized long snowflake(long workerId,long datacenterId){
Snowflake snowflake = IdUtil.createSnowflake(workerId,datacenterId);
return snowflake.nextId();
}
public static void main(String[] args){
System.out.println(new IdGeneratorSnowflake().snowflake());
}
}
如果想 彻底解决 机器时钟回拨导致重复id 生成的问题,则可以参考 百度开源的分布式id 生成器 UidGenerator 和 美团点评的分布式id 生成系统 Leaf 。