1.写唯一ID生成器的原由

在阅读工程源码的时候,发现有一个工具职责生成一个消息ID,方便进行全链路的查询,实现方式特别简单,核心源码不过两行,根据时间戳以及随机数生成一个ID,这种算法ID在分布式系统中重复的风险就很明显了。本来以为只是日志打印功能,根据于此在不同系统调用间关联业务日志而已,不过后来发现此ID需要入库,看到这里就觉得有些风险了,于是就想着怎么改造它。

String timeString = String.valueOf(System.currentTimeMillis());
return Long.parseLong(timeString.substring(timeString.length() - 8, timeString.length()))
* RandomUtils.nextInt(1, 9);

2. Twitter snowflake

既然是分布式唯一ID,自然而然想到了Twitter的snowflake算法,在以前的做的部分业务中也用到过它来生成数据库主键ID,不过当时仅限于使用,以及将64 bit的long数字拆分成几个部分,以保证唯一,对具体实现没有深入研究。正好借着机会深入下。

snowflake拆分long的示意图

该ID生成方式,用41位时间戳保存当前时间的毫秒数(69年后一个轮回),十位机器码,最多可以供一项业务1024台实例,每个毫秒数,12位序列自增,每秒理论上单机环境可生产409.6万个ID,分享一个官方github的scala实现,twitter-archive/snowflake

3.关于snowflake的一些思考

1.监视器锁

此锁的目的是为了保证在多线程的情况下,只有一个线程进入方法体生成ID,保证并发情况下生成ID的唯一性,如果在竞争激烈情况下,自选锁+ CAS原子变量的方式或许是更为合理的选择,可以达到优化部分性能的目的。

源码中的监视器锁

2.时间回退问题

时间校准,以及其他因素,可能导致服务器时间回退(时间向前快进不会有问题),如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。官方对于此并没有给出解决方案,而是简单的抛错处理,这样会造成在时间被追回之前的这段时间服务不可用,显然我无法接受这一点。

官方的抛错处理

而对于此的思考是,既然snowflake理论情况下单机可实现每秒409.6万个ID的生成上限,实际上能想得到的业务都不太可能产生如此高的并发,那么就会存在在过去的一段时间内,有大量的时间戳“被浪费”,达不到该上限,可能在某一毫秒内只生成几个ID,如果发生了时间回退,这些“被浪费”的资源是不是就能利用起来,而不是抛错。

被浪费的时间戳

如果在内存中建立一个数组,这个数组设定固定长度,比如说200,这些数组中存储上一次该位置对应的毫秒数的messageId,那么就能在时间回退到追回时间这段时间内,再至多提供819200((2^12) *200)个messageId,如果发生时间回退,就只用在上一次messageId进行+1操作,直到系统时间被追回(此段结合后续源码进行解释)。

4.改进版的snowflake

1.机器码生成器 MachineIdService设计及其实现:

public interface MachineIdService {
/**
* 生成MachineId的方法
*
* @return machineId 机器码
* @throws MessageIdException 获取机器码可能因为外部因素失败
*/
Long getMachineId() throws MessageIdException;
}

实现该接口确保一个集群中,每台实例生成不同的machineID,并且MachineID 不能超过(2^10) 1023,具体实现方式,可使用MySQL数据库,文件描述映射,Redis自增等方式,这里我使用了Redis自增的方式(所以在需要用到该ID生成器的地方需要依赖Redis),具体实现方式如下:

public class RedisMachineIdServiceImpl implements MachineIdService {
<span class="token keyword">private</span> <span class="token keyword">static</span> final String <span class="token constant">MAX_ID</span> <span class="token operator">=</span> <span class="token string">"MAX_ID"</span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> final String <span class="token constant">IP_MACHINE_ID_MAPPING</span> <span class="token operator">=</span> <span class="token string">"IP_MACHINE_ID_MAPPING"</span><span class="token punctuation">;</span> <span class="token keyword">private</span> RedisTemplate<span class="token operator">&lt;</span>String<span class="token punctuation">,</span> String<span class="token operator">&gt;</span> redisTemplate<span class="token punctuation">;</span> <span class="token keyword">private</span> String redisKeyPrefix<span class="token punctuation">;</span> <span class="token comment">//设置RedisTemplate实例</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setRedisTemplate</span><span class="token punctuation">(</span><span class="token parameter">RedisTemplate<span class="token operator">&lt;</span>String<span class="token punctuation">,</span> String<span class="token operator">&gt;</span> redisTemplate</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>redisTemplate <span class="token operator">=</span> redisTemplate<span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token comment">// 设置redisKey前缀,如果多个业务使用同一个Redis集群,使用不同的Redis前缀进行区分</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setRedisKeyPrefix</span><span class="token punctuation">(</span><span class="token parameter">String redisKeyPrefix</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>redisKeyPrefix <span class="token operator">=</span> redisKeyPrefix<span class="token punctuation">;</span>
<span class="token punctuation">}</span> @Override
<span class="token keyword">public</span> Long <span class="token function">getMachineId</span><span class="token punctuation">(</span><span class="token punctuation">)</span> throws MessageIdException <span class="token punctuation">{</span>
String host<span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token comment">//获取本机IP地址</span>
host <span class="token operator">=</span> InetAddress<span class="token punctuation">.</span><span class="token function">getLocalHost</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getHostAddress</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>UnknownHostException e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">MessageIdException</span><span class="token punctuation">(</span><span class="token string">"Can not get the host!"</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>redisTemplate <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">MessageIdException</span><span class="token punctuation">(</span><span class="token string">"Can not get the redisTemplate instance!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>redisKeyPrefix <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">MessageIdException</span><span class="token punctuation">(</span><span class="token string">"The redis key prefix is null,please set redis key prefix first!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
HashOperations<span class="token operator">&lt;</span>String<span class="token punctuation">,</span> String<span class="token punctuation">,</span> Long<span class="token operator">&gt;</span> hashOperations <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForHash</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//通过IP地址在Redis中的映射,找到本机的MachineId</span>
Long result <span class="token operator">=</span> hashOperations<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>redisKeyPrefix <span class="token operator">+</span> <span class="token constant">IP_MACHINE_ID_MAPPING</span><span class="token punctuation">,</span> host<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>result <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> result<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">//如果没有找到,说明需要对该实例进行新增MachineId,使用Redis的自增函数,生成一个新的MachineId</span>
Long incrementResult <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">increment</span><span class="token punctuation">(</span>redisKeyPrefix <span class="token operator">+</span> <span class="token constant">MAX_ID</span><span class="token punctuation">,</span> <span class="token number">1</span>L<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>incrementResult <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">MessageIdException</span><span class="token punctuation">(</span><span class="token string">"Get the machine id failed,please check the redis environment!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">//将生成的MachineId放入Redis中,方便下次查找映射</span>
hashOperations<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>redisKeyPrefix <span class="token operator">+</span> <span class="token constant">IP_MACHINE_ID_MAPPING</span><span class="token punctuation">,</span> host<span class="token punctuation">,</span> incrementResult<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> incrementResult<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

2.MessageIdService设计以及实现

public interface MessageIdService {
<span class="token comment"><span class="hljs-comment">/**
* 生成一个保证全局唯一的MessageId
*
* <span class="hljs-doctag">@return</span> messageId
*/</span></span>
<span class="token keyword"><span class="hljs-function"><span class="hljs-keyword">long</span></span></span><span class="hljs-function"> </span><span class="token function"><span class="hljs-function"><span class="hljs-title">genMessageId</span></span></span><span class="token punctuation"><span class="hljs-function"><span class="hljs-params">(</span></span></span><span class="token punctuation"><span class="hljs-function"><span class="hljs-params">)</span></span></span><span class="token punctuation">;</span> <span class="token comment"><span class="hljs-comment">/**
* 初始化方法
*
* <span class="hljs-doctag">@throws</span> MessageIdException
*/</span></span>
<span class="token keyword"><span class="hljs-function"><span class="hljs-keyword">void</span></span></span><span class="hljs-function"> </span><span class="token function"><span class="hljs-function"><span class="hljs-title">init</span></span></span><span class="token punctuation"><span class="hljs-function"><span class="hljs-params">(</span></span></span><span class="token punctuation"><span class="hljs-function"><span class="hljs-params">)</span></span></span><span class="hljs-function"> </span><span class="token keyword"><span class="hljs-function"><span class="hljs-keyword">throws</span></span></span><span class="hljs-function"> </span><span class="token class-name"><span class="hljs-function">MessageIdException</span></span><span class="token punctuation">;</span>

}

public class MessageIdServiceImpl implements MessageIdService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MessageIdServiceImpl.class);
//最大的MachineId,1024个
private static final long MAX_MACHINE_ID = 1023L;
//AtomicLongArray 环的大小,可保存200毫秒内,每个毫秒数上一次的MessageId,时间回退的时候依赖与此
private static final int CAPACITY = 200;
// 时间戳在messageId中左移的位数
private static final int TIMESTAMP_SHIFT_COUNT = 22;
// 机器码在messageId中左移的位数
private static final int MACHINE_ID_SHIFT_COUNT = 12;
// 序列号的掩码 2^12 4096
private static final long SEQUENCE_MASK = 4095L; //messageId ,开始的时间戳,start the world,世界初始之日
private static long START_THE_WORLD_MILLIS;
//机器码变量
private long machineId;
// messageId环,解决时间回退的关键,亦可在多线程情况下减少毫秒数切换的竞争
private AtomicLongArray messageIdCycle = new AtomicLongArray(CAPACITY);
//生成MachineIds的实例
private MachineIdService machineIdService; static {
try {
//使用一个固定的时间作为start the world的初始值
START_THE_WORLD_MILLIS = SimpleDateFormat.getDateTimeInstance().parse("2018-09-13 00:00:00").getTime();
} catch (ParseException e) {
throw new RuntimeException("init start the world millis failed", e);
}
} public void setMachineIdService(MachineIdService machineIdService) {
this.machineIdService = machineIdService;
} /**
* init方法中通过machineIdService 获取本机的machineId
* @throws MessageIdException
*/
@Override
public void init() throws MessageIdException {
if (machineId == 0L) {
machineId = machineIdService.getMachineId();
}
//获取的machineId 不能超过最大值
if (machineId <= 0L || machineId > MAX_MACHINE_ID) {
throw new MessageIdException("the machine id is out of range,it must between 1 and 1023");
}
}
/**
* 核心实现的代码
*/
@Override
public long genMessageId() {
do {
// 获取当前时间戳,此时间戳是当前时间减去start the world的毫秒数
long timestamp = System.currentTimeMillis() - START_THE_WORLD_MILLIS;
// 获取当前时间在messageIdCycle 中的下标,用于获取环中上一个MessageId
int index = (int)(timestamp % CAPACITY);
long messageIdInCycle = messageIdCycle.get(index);
//通过在messageIdCycle 获取到的messageIdInCycle,计算上一个MessageId的时间戳
long timestampInCycle = messageIdInCycle >> TIMESTAMP_SHIFT_COUNT;
// 如果timestampInCycle 并没有设置时间戳,或时间戳小于当前时间,认为需要设置新的时间戳
if (messageIdInCycle == 0 || timestampInCycle < timestamp) {
long messageId = timestamp << TIMESTAMP_SHIFT_COUNT | machineId << MACHINE_ID_SHIFT_COUNT;
// 使用CAS的方式保证在该条件下,messageId 不被重复
if (messageIdCycle.compareAndSet(index, messageIdInCycle, messageId)) {
return messageId;
}
LOGGER.debug("messageId cycle CAS1 failed");
}
// 如果当前时间戳与messageIdCycle的时间戳相等,使用环中的序列号+1的方式,生成新的序列号
// 如果发生了时间回退的情况,(即timestampInCycle > timestamp的情况)那么不能也更新messageIdCycle 的时间戳,使用Cycle中MessageId+1
if (timestampInCycle >= timestamp) {
long sequence = messageIdInCycle & SEQUENCE_MASK;
if (sequence >= SEQUENCE_MASK) {
LOGGER.debug("over sequence mask :{}", sequence);
continue;
}
long messageId = messageIdInCycle + 1L;
// 使用CAS的方式保证在该条件下,messageId 不被重复
if (messageIdCycle.compareAndSet(index, messageIdInCycle, messageId)) {
return messageId;
}
LOGGER.debug("messageId cycle CAS2 failed");
}
// 整个生成过程中,采用的spinLock
} while (true);
} }

原文地址:https://www.jianshu.com/p/b1124283fc43

关于分布式唯一ID,snowflake的一些思考及改进(完美解决时钟回拨问题)的更多相关文章

  1. 分布式唯一id:snowflake算法思考

    匠心零度 转载请注明原创出处,谢谢! 缘起 为什么会突然谈到分布式唯一id呢?原因是最近在准备使用RocketMQ,看看官网介绍: 一句话,消息可能会重复,所以消费端需要做幂等.为什么消息会重复后续R ...

  2. 分布式唯一ID生成方案选型!详细解析雪花算法Snowflake

    分布式唯一ID 使用RocketMQ时,需要使用到分布式唯一ID 消息可能会发生重复,所以要在消费端做幂等性,为了达到业务的幂等性,生产者必须要有一个唯一ID, 需要满足以下条件: 同一业务场景要全局 ...

  3. 分布式唯一ID:雪花ID Snowflake .Net版

    先抄个雪花ID介绍,雪花算法: 雪花算法的原始版本是scala版,用于生成分布式ID(纯数字,时间顺序),订单编号等. 自增ID:对于数据敏感场景不宜使用,且不适合于分布式场景.GUID:采用无意义字 ...

  4. 分布式唯一id生成器的想法

    0x01 起因 前端时间遇到一个问题,怎么快速生成唯一的id,后来采用了hashid的方法.最近在网上读到了美团关于分布式唯一id生成器的解决方案, 其中提到了三种生成法:(建议看一下这篇文章,写得很 ...

  5. 分布式唯一ID极简教程

    原创 2017-11-21 帝都羊 架构师小秘圈 一,题记 所有的业务系统,都有生成ID的需求,如订单id,商品id,文章ID等.这个ID会是数据库中的唯一主键,在它上面会建立聚集索引! ID生成的核 ...

  6. 分布式唯一ID生成算法-雪花算法

    在我们的工作中,数据库某些表的字段会用到唯一的,趋势递增的订单编号,我们将介绍两种方法,一种是传统的采用随机数生成的方式,另外一种是采用当前比较流行的“分布式唯一ID生成算法-雪花算法”来实现. 一. ...

  7. 百度开源的分布式唯一ID生成器UidGenerator,解决了时钟回拨问题

    UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器.而且,它非常适合虚拟环境,比如:Docker.另外,它通过消费未来时间克服了雪花算法的并发限制.Uid ...

  8. 分布式唯一ID实现

    ID生成的核心需求 全局唯一 趋势有序 为什么要全局唯一 避免ID冲突 著名的例子就是身份证号码,身份证号码确实是对人唯一的,然而一个人是可以办理多个身份证的,例如你身份证丢了,又重新补办了一张,号码 ...

  9. 讲分布式唯一id,这篇文章很实在

    分布式唯一ID介绍 分布式系统全局唯一的 id 是所有系统都会遇到的场景,往往会被用在搜索,存储方面,用于作为唯一的标识或者排序,比如全局唯一的订单号,优惠券的券码等,如果出现两个相同的订单号,对于用 ...

随机推荐

  1. QTextStream写文件中文乱码解决办法

    1.首先把Qt Creator的编辑器设置为使用 UTF-8:   工具-->选项-->文本编辑器-->行为,在右侧选项界面找到文件编码选项,设置为 UTF-8.2.使用 QText ...

  2. 前端每日实战:30# 视频演示如何用纯 CSS 创作一个晃动的公告板

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/wjZoGV 可交互视频教程 此视频 ...

  3. margin 和padding 的区别

    margin是用来隔开元素与元素的间距:padding是用来隔开元素与内容的间隔.margin用于布局分开元素使元素与元素互不相干: padding用于元素与内容之间的间隔,让内容(文字)与(包裹)元 ...

  4. @ContrllerAdvice全局异常

    @ControllerAdvice,是Spring3.2提供的新注解,它是一个Controller增强器,可对controller中被 @RequestMapping注解的方法加一些逻辑处理.最常用的 ...

  5. IDEA git 合并多个commit

    当前三个commit,demo1,demo2,demo3 选择demo1右键 选择action 跟着指示操作,最后合并 时间线: Log 框时间线:是从上到下,越来越早. 弹出框时间线:是从上到下,越 ...

  6. python之路----操作系统的发展史

    阅读目录 手工操作 —— 穿孔卡片 批处理 —— 磁带存储和批处理系统 多道程序系统 分时系统 实时系统 通用操作系统 操作系统的进一步发展 操作系统的作用 手工操作 —— 穿孔卡片 1946年第一台 ...

  7. 关于Reporting Services网站

    1.http://www.c-sharpcorner.com/search/sql%20server%20reporting%20services 2.https://msdn.microsoft.c ...

  8. VBA在Excel中的应用(三)

    目录  Chart Export Chart Format Chart Lengend  Chart Protect  Chart Title  Chart Chart Export 1. 将Exce ...

  9. Win10真正好用之处

    第一步.  关闭无用服务 刚装好Win10的时候,整部电脑响应很慢,有时什么都不做,硬盘灯也能狂闪半天.很明显,这是微软爸爸默认开启的服务未被及时关闭所致. 网上有很多文章指导新手如何关闭系统服务,但 ...

  10. ECSHOP2.7源码分析

    目录结构