分布式ID的业务需求
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。
比如美团外卖:由于系统中数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据,如订单、骑手、优惠券也都需要有唯一ID做标识。因此一个能够生成全局唯一ID的系统是非常必要的。
生成ID的硬性要求
ID生成系统的可用性要求
一般通用方案
UUID(Universally Unique Dentifer)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符,示例:550e8400-e29b-41d4-a716-446655440000
UUID性能非常高:本地生成,没有网络消耗,如果只考虑唯一性UUID是ok的。但是入数据库的性能较差。
为什么无序的UUID会导致数据库性能变差呢?
数据库自增主键
在分布式里面,数据库的自增ID机制的主要原理是:
基于数据库自增ID和mysql数据库的replace into实现的。这里的replace into 跟insert功能类似,不同点在于replace into首先尝试把数据插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入,否则直接插入新数据。
数据库自增ID机制为什么不适合作分布式ID?
基于redis的id生成策略
因为Redis是单线程的天生保证原子性,可以使用原子操作INCR和INCRBY来实现。
注意:在Redis集群情况下,同样和MySQL一样需要设置不同的增长步长,同时key一定要设置有效期,可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis,可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5
各个Redis生成的ID为:
A: 1,6,11,16,21
B: 2,7,12,17,22
C:3.8.13.18.23
D: 4,9,14,19,24
E: 5,10,15,20,25
雪花算法
Twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra(由Facebook开发一套开源分布式NoSQL数据库系统)因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。
Twitter的分布式雪花算法 ,经测试snowlake每秒能够产生26万个自增可排序的ID。
优缺点
优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。可以根据自身业务特性分配bit位,非常灵活。
缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成。
可能在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以忽略, 一般分布式ID只要求趋势递增,并不会严格要求递增, 90%的需求都只要求趋势递增)
测试雪花算法
导入依赖
<!--hutool 测试雪花算法-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Slf4j
@Component
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) {
log.info("当前机器的workerId获取失败", e);
workerId = NetUtil.getLocalhostStr().hashCode();
log.info("当前机器 workId:{}", workerId);
}
}
public synchronized long snowflakeId() {
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long datacenterId) {
snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
public static void main(String[] args) {
// 1303931069132832768
System.out.println(new IdGeneratorSnowflake().snowflakeId());
}
}