前言
最近公司在扩招后端高级开发,有幸成为面试官之一,其中问的最多一个问题就是分布式ID的几种解决方案,不客气的说前身小公司的开发答得完整的很少。
于是就抽出了周末的时间整理了几种主流的分布式ID生成方案,希望能够帮助到你们。
开篇几个问题
如在美团点评的金融、支付、餐饮、酒店等业务场景
猫眼电影等产品的系统中数据日渐增长,对数据分库分表后需要有一个唯一ID来表示一条数据或者消息。
特别一点的如订单、骑手、优惠劵也都需要一个唯一ID作为标识。
此时一个能生成唯一ID的系统是非常必要的。
ID生成规则部分硬性要求
全局唯一:既然是唯一标识,那么全局唯一是最基本的要求。
趋势递增:在MySQL的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键来保证写入性能。
单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
信息安全:如果ID是连续的,那么恶意用户的爬取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号那么更加危险,竞争对手可以知道我们一天的单量;所以在一些应用场景下,需要ID无规则不规则,让竞争对手不好猜。
含时间戳:这样就能在开发中快速了解这个分布式ID的生成时间。
ID生成系统的可用性要求
高可用:发一个获取分布式ID的请求,服务器就要保证99.999%的情况下给我创建一个唯一分布式ID
低延迟:发一个获取分布式ID的请求,服务器就要快,极速
高QPS:假如并发一口气10万个创建分布式ID请求同时过来,服务器需要顶得住且成功创建10万个分布式ID
通用的几种方案
随着系统架构以及业务的演变,分布式ID生成也是有N种解决方案,以下就简单的列举几种。
UUID
这种方案估计大家都了解,最简单的一种方案。
public static void main(String[] args) {
String uuid = UUID.randomUUID().toString();
System.out.println(uuid);
}
如果只是考虑唯一性,那么UUID基本可以满足需求。
缺点
无序:无法预测它的生成顺序,不能生成递增有序的数字
主键:ID作为主键时在特定的环境下会存在一些问题,比如做DB主键的场景下,UUID非常不适用,MySQL官方有明确的建议主键要尽量越短越好,36位的UUID不合要求。
索引:会导致B+树索引得分裂。
数据库自增主键
此种方案有一定的局限性,在高并发集群上此策略不可用。
基于Redis生成全局ID策略
因为Redis是单线程,天生保证原子性,所以可以使用INCR和INCRBY来实现。
集群分布式
“
在Redis集群下,同样和MySQL一样需要设置不同的增长步数,同时key需要设置有效期;可以使用Redis集群来获取更高的吞吐量;假如一个集群中有五个Redis,那么初始化每台Redis步长分别是1,2,3,4,5,然后步长都是5。


twitter的snowflake算法 -- java实现
@author beyond
@date 2016/11/26
*/
public class SnowFlake {
/**
/**
/**
/**
private long datacenterId; //数据中心
private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
产生下一个ID
@return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);
for (int i = 0; i < (1 << 12); i++) {
System.out.println(snowFlake.nextId());
}}
}
测试
//测试使用雪花算法生成ID
//构造函数中传入datacenterId和workerId
SnowFlake snowFlake = new SnowFlake(1,1);
for (int i = 0; i < 10; i++) {
long id = snowFlake.nextId();
System.out.println("id:" + id + "\t" + String.valueOf(id).length() + "位");
System.out.println("------------------------------------------");
}

Spring Boot整合雪花算法
引入hutool-all,maven依赖引入如下:
@Configuration
public class SnowFlakeConfig {
@Value("{application.workerId}")
private Long workerId;
/***
* 注入一个生成雪花ID的对象
* @return
*/
@Bean
public Snowflake snowflake() {
return new Snowflake(workerId,datacenterId);
}}
yml配置文件:
application:
datacenterId: 2
workerId: 1
server:
port: 7777
service 层:
@Service
public class OrderService {
@Autowired
private Snowflake snowflake;
public String getIdBySnowFlake() {
return String.valueOf(snowflake.nextId());
}}
其他开源的解决方案
很多大厂都对雪花算法做出了改进,开源了一些改进方案,如下:
百度开源的分布式唯一ID生成器UidGenerator
Leaf–美团点评分布式ID生成系统