在咱们的业务需求中一般有须要一些惟一的ID,来记录咱们某个数据的标识:html
一般咱们会调研各类各样的生成策略,根据不一样的业务,采起最合适的策略,下面我会讨论一下各类策略/算法,以及他们的一些优劣点。java
UUID是通用惟一识别码(Universally Unique Identifier)的缩写,开放软件基金会(OSF)规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素。利用这些元素来生成UUID。node
UUID是由128位二进制组成,通常转换成十六进制,而后用String表示。在java中有个UUID类,在他的注释中咱们看见这里有4种不一样的UUID的生成策略:面试
UUID的优势:redis
UUID的缺点:算法
适用场景:UUID的适用场景能够为不须要担忧过多的空间占用,以及不须要生成有递增趋势的数字。在Log4j里面他在UuidPatternConverter中加入了UUID来标识每一条日志。数据库
你们对于惟一标识最容易想到的就是主键自增,这个也是咱们最经常使用的方法。例如咱们有个订单服务,那么把订单id设置为主键自增便可。缓存
优势:安全
缺点:bash
适用场景: 根据上面能够总结出来,当数据量很少,并发性能不高的时候这个很适合,好比一些to B的业务,商家注册这些,商家注册和用户注册不是一个数量级的,因此能够数据库主键递增。若是对顺序递加强依赖,那么也可使用数据库主键自增。
熟悉Redis的同窗,应该知道在Redis中有两个命令Incr,IncrBy,由于Redis是单线程的因此能保证原子性。
优势:
缺点:
适用:因为其性能比数据库好,可是有可能会出现ID重复和不稳定,这一块若是能够接受那么就可使用。也适用于到了某个时间,好比天天都刷新ID,那么这个ID就须要重置,经过(Incr Today),天天都会从0开始加。
利用ZK的Znode数据版本以下面的代码,每次都不获取指望版本号也就是每次都会成功,那么每次都会返回最新的版本号:
Zookeeper这个方案用得较少,严重依赖Zookeeper集群,而且性能不是很高,因此不予推荐。
这个方法在美团的Leaf中有介绍,详情能够参考美团技术团队的发布的技术文章:Leaf——美团点评分布式ID生成系统,这个方案是将数据库主键自增进行优化。
biz_tag表明每一个不一样的业务,max_id表明每一个业务设置的大小,step表明每一个proxyServer缓存的步长。 以前咱们的每一个服务都访问的是数据库,如今不须要,每一个服务直接和咱们的ProxyServer作交互,减小了对数据库的依赖。咱们的每一个ProxyServer回去数据库中拿出步长的长度,好比server1拿到了1-1000,server2拿到来 1001-2000。若是用完会再次去数据库中拿。
优势:
缺点:
适用场景:须要趋势递增,而且ID大小可控制的,可使用这套方案。
固然这个方案也能够经过一些手段避免被人猜想,把ID变成是无序的,好比把咱们生成的数据是一个递增的long型,把这个Long分红几个部分,好比能够分红几组三位数,几组四位数,而后在创建一个映射表,将咱们的数据变成无序。
Snowflake是Twitter提出来的一个算法,其目的是生成一个64bit的整数:
上面只是一个将64bit划分的标准,固然也不必定这么作,能够根据不一样业务的具体场景来划分,好比下面给出一个业务场景:
这个时候咱们根据上面的场景能够再次合理的划分62bit,QPS几年以内会发展到百万,那么每毫秒就是千级的请求,目前10台机器那么每台机器承担百级的请求,为了保证扩展,后面的循环位能够限制到1024,也就是2^10,那么循环位10位就足够了。
机器三地部署咱们能够用3bit总共8来表示机房位置,当前的机器10台,为了保证扩展到百台那么能够用7bit 128来表示,时间位依然是41bit,那么还剩下64-10-3-7-41-1 = 2bit,还剩下2bit能够用来进行扩展。
适用场景:当咱们须要无序不能被猜想的ID,而且须要必定高性能,且须要long型,那么就可使用咱们雪花算法。好比常见的订单ID,用雪花算法别人就没法猜想你天天的订单量是多少。
public class IdWorker{
private long workerId;
private long datacenterId;
private long sequence = 0;
/**
* 2018/9/29日,今后时开始计算,能够用到2089年
*/
private long twepoch = 1538211907857L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
private long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 获得0000000000000000000000000000000000000000000000000000111111111111
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
public IdWorker(long workerId, long datacenterId){
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
//时间回拨,抛出异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
/**
* 当前ms已经满了
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
IdWorker worker = new IdWorker(1,1);
for (int i = 0; i < 30; i++) {
System.out.println(worker.nextId());
}
}
}
复制代码
上面定义了雪花算法的实现,在nextId中是咱们生成雪花算法的关键。
由于机器的缘由会发生时间回拨,咱们的雪花算法是强依赖咱们的时间的,若是时间发生回拨,有可能会生成重复的ID,在咱们上面的nextId中咱们用当前时间和上一次的时间进行判断,若是当前时间小于上一次的时间那么确定是发生了回拨,普通的算法会直接抛出异常,这里咱们能够对其进行优化,通常分为两个状况:
经过上面的几种策略能够比较的防御咱们的时钟回拨,防止出现回拨以后大量的异常出现。下面是修改以后的代码,这里修改了时钟回拨的逻辑:
本文分析了各类生产分布式ID的算法的原理,以及他们的适用场景,相信你已经能为本身的项目选择好一个合适的分布式ID生成策略了。没有一个策略是完美的,只有适合本身的才是最好的。
最后打个广告,若是你以为这篇文章对你有文章,能够关注个人技术公众号,也能够加入个人技术交流群进行更多的技术交流。最近做者收集了不少最新的学习资料视频以及面试资料,关注以后便可领取,你的关注和转发是对我最大的支持,O(∩_∩)O。