1. 异常概述
2018年1月26日下午,业务方信贷小组的同学反馈服务执行数据库插入操作出现异常,异常信息显示数据库主键出现重复:
在仔细分析了用户的重复主键ID、机器列表、雪花算法之后,下掉55这台机器,至此,异常得以解除。
本次异常看似平常,然而仔细分析起来可能造成的后果比较严重。 (1)波及面广、影响时间长。目前大量业务都采用了雪花算法的主键生成策略,如果业务、运维同学不了解雪花算法,会造成大量的时间分析排查此问题,造成一定的业务损失。 (2)存在潜在的隐患。雪花算法除了会产生此类Workid问题,也强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
2. 原因分析
为什么会造成数据库主键重复呢? 要回答这个问题要先介绍一下作为主键生成策略主要算法之一的雪花算法的工作原理。
2.1 Snowflake工作原理
对于分布式的ID生成,以Twitter Snowflake为代表的, Flake 系列算法,属于划分命名空间并行生成的一种算法,生成的数据为64bit的long型数据,在数据库中应该用大于等于64bit的数字类型的字段来保存该值,比如在MySQL中应该使用BIGINT。
Twitter在2010年6月1日(在Flickr那篇文章发布不到4个月之后),Ryan King 在Twitter的Blog 撰文 写道:
Ticket Servers方案缺乏顺序的保证
考虑过采用UUID,不过128-bit太长了
E也考虑过采用ZooKeeper所提供的 Unique Naming Seuence Nodes 所提供的 Unique Naming 特性,但是性能不能满足。(Sequence Nodes的设计目标是解决分布式锁的问题,但不解决性能要求极高的ID生成问题,直接应用是一种Hack行为)
在这种情况下,Twitter给出了 64-bit 长的 Snowflake ,它的结构是:
E1-bit reserved
E41-bit timestamp
E10-bit machine id
E12-bit sequence
在过了不到4年,2014年的5月31日,Twitter 更新了 Snowflake 的 README,其中陈述了两个容易被忽视的事实:
"We have retired the initial release of Snowflake …" "… heavily relies on existing infrastructure at Twitter to run. "
可以看出,这个方案所支持的最小划分粒度是「毫秒 * 线程」,单线程(Snowflake 里对应的概念是 Worker)的每秒容量是12-bit,也就是接近4096。
Snowflake的意义,不仅仅在于提供了解决方式,更多的是一种基于Long长度实现具有时间相关性的id自增序列。因此,很多公司基于它进行二次改造适应自己的场景。Snowflake家族的算法还有Instagram SnowFlake、Simpleflake、Boundary flake等等。
目前业界使用当当亮哥的sharding-jdbc,一般都会采取其内置的Snowflake算法,关于二次改造我这里列举一个58沈剑在《架构师之路》系列中提出的例子。
2.2 问题定位
收到业务方反馈以后,条件反射得第一时间连问了业务方同学三个问题:
你们的服务有没有什么特殊型的地方?
是重启的时候发生的么?
你能不能查一下对应重复的记录所在的机器,重复是不是只发生在这个ip段?
业务方也是第一时间给了反馈
就是普通的微服务,普通的机器
不是重启时发生的
果然就是两台机器上出现了问题!
好了,那我就定位到了是workid出现了问题,马上建议业务方下掉其中一台。为什么是workid,而不是时钟回拨等其他原因,且听我慢慢道来。
2.3 排除时钟回拨
大家应该都知道雪花算法存在的缺点是:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成
在单机上是递增的,但是由于设计到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以认为无所谓,一般分布式ID只要求趋势递增,并不会严格要求递增~90%的需求都只要求趋势递增)
我们采用的是当当的sharding-jdbc 1.4.2这个版本(1.5之前的sj并不成熟,强烈推荐使用2.x),可以说直接使用了其com.dangdang.ddframe.rdb.sharding.id.generator.self.IPIdGenerator进行主键生成,其序列号生成采用的是com.dangdang.ddframe.rdb.sharding.id.generator.self.CommonSelfIdGenerator,这里特别推荐读者读一下这两个类前面的注释,写得非常清楚。
/**
* 根据机器IP获取工作进程Id,如果线上机器的IP二进制表示的最后10位不重复,建议使用此种方式
* ,列如机器的IP为192.168.1.108,二进制表示:11000000 10101000 00000001 01101100
* ,截取最后10位 01 01101100,转为十进制364,设置workerId为364.
*
* @author DonneyYoung
*/
public class IPIdGenerator implements IdGenerator {
上面这段注释非常有用,先贴在这里,后面会详细谈到。 序列号生成采用的IPIdGenerator,当当的实现算法如下所示:
/**
* 自生成Id生成器.
*
*
* 长度为64bit,从高位到低位依次为
*
*
*
* 1bit 符号位
* 41bits 时间偏移量从2016年11月1日零点到现在的毫秒数
* 10bits 工作进程Id
* 12bits 同一个毫秒内的自增量
*
*
*
* 工作进程Id获取优先级: 系统变量{@code sjdbc.self.id.generator.worker.id} 大于 环境变量{@code SJDBC_SELF_ID_GENERATOR_WORKER_ID}
* ,另外可以调用@{@code CommonSelfIdGenerator.setWorkerId}进行设置
*
*
* @author gaohongtao
*/
@Getter
@Slf4j
public class CommonSelfIdGenerator implements IdGenerator {
public static final long SJDBC_EPOCH;//时间偏移量,从2016年11月1日零点开始
private static final long SEQUENCE_BITS = 12L;//自增量占用比特
private static final long WORKER_ID_BITS = 10L;//工作进程ID比特
private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;//自增量掩码(最大值)
private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;//工作进程ID左移比特数(位数)
private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;//时间戳左移比特数(位数)
private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;//工作进程ID最大值
@Setter
private static AbstractClock clock = AbstractClock.systemClock();
@Getter
private static long workerId;//工作进程ID
static {
Calendar calendar = Calendar.getInstance();
calendar.set(2016, Calendar.NOVEMBER, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
SJDBC_EPOCH = calendar.getTimeInMillis();
initWorkerId();
}
private long sequence;//最后自增量
private long lastTime;//最后生成编号时间戳,单位:毫秒
static void initWorkerId() {
String workerId = System.getProperty("sjdbc.self.id.generator.worker.id");
if (!Strings.isNullOrEmpty(workerId)) {
setWorkerId(Long.valueOf(workerId));
return;
}
workerId = System.getenv("SJDBC_SELF_ID_GENERATOR_WORKER_ID");
if (Strings.isNullOrEmpty(workerId)) {
return;
}
setWorkerId(Long.valueOf(workerId));
}
/**
* 设置工作进程Id.
*
* @param workerId 工作进程Id
*/
public static void setWorkerId(final Long workerId) {
Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
CommonSelfIdGenerator.workerId = workerId;
}
/**
* 生成Id.
*
* @return 返回@{@link Long}类型的Id
*/
@Override
public synchronized Number generateId() {
//保证当前时间大于最后时间。时间回退会导致产生重复id
long time = clock.millis();
Preconditions.checkState(lastTime <= time, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, time);
// 获取序列号
if (lastTime == time) {
if (0L == (sequence = ++sequence & SEQUENCE_MASK)