最近在研究分布式ID的生成方法,发现Twitter的snowflake算法挺有意思,因此亲自动手用Java进行了实现。
snowflake算法用64位整数来表示主键,其结构如下图:
1 bit符号位:设计者不喜欢负数主键?方便使用负数标识不正确的ID?
41 bit毫秒时间:2^41 / (365 * 24 * 3600 * 1000) ≈ 69年
10 bit机房ID + 机器ID:最大值为1023
12 bit递增序列:最大值为4095
因为使用机房ID + 机器ID来标识机器,因此可以分散到每台业务机器运行而不会产生重复,不需要集中产生主键,这是这个算法最大的优点。
每秒最多可以生成主键数:4096 * 1000毫秒 = 4096000。以当前机器的配置情况和业务情况,单机每秒400万不重复ID无论如何都已经足够。
虽然算法本身很简单,但分布式集群面临的情况很复杂,编码过程中要考虑的因素有很多。废话不多说,“翠花!上代码!”
1.0 分布式时间发生器
1.1 设计考虑
(1) System.currentTimeMillis()方法每次执行都要进行一次系统内核调用,系统开销较大。对于当前的这个序列号生成器来说,只要保证递增序列从4095归0时获取的时间 比 上次归0时获取的时间大就不会产生重复值,因此使用一个long变量缓存了最近一次时间。
(2) 机房ID 和 机器ID正常情况下不会发生改变,因此每次从系统更新时间后立即进行或运算并保存,避免频繁的更新操作。
(3) 配置类AbstractRMConfig 设计成抽象类,用户可自由实现并注册到时间发生器即可。
(4) 为避免业务平静期递增序列长时间无法到达4096,导致缓存时间过旧引发其它问题,因此使用定时线程TimeUpdater每1000毫秒更新一次时间,间隔时间可以自由设置。
1.2 代码
/**
* 分布式时间发生器
* @author Tony.Lau
*/
public enum TimeGenerator {
INSTANCE;
private Logger logger = LoggerFactory.getLogger(TimeGenerator.class);
private AbstractRMConfig config;
private long lastTimeMills;
private volatile boolean isFail = true;
private int rmid = -1;
private final Lock rmidLock = new ReentrantLock();
private ScheduledExecutorService es = Executors.newScheduledThreadPool(1);
private boolean isRun = false;
/** 获取缓存时间 */
long getTime() {
try {
rmidLock.lock();
if (isFail) {
return -1l;
}
return lastTimeMills;
} finally {
rmidLock.unlock();
}
}
/** 获取最新时间 */
long updateTime() {
try {
rmidLock.lock();
if (isFail) {
return -1l;
}
long temp = (System.currentTimeMillis() << 23 >>> 1) ^ rmid;
while (temp <= lastTimeMills) {
temp = (System.currentTimeMillis() << 23 >>> 1) ^ rmid;
}
return lastTimeMills = temp;
} finally {
rmidLock.unlock();
}
}
/** 注册配置信息 */
public RegisterState registerRoomMachine(AbstractRMConfig config) {
isFail = true;
if (config == null) {
return RegisterState.ERROR;
}
if (config instanceof FailRMConfig) {
return RegisterState.FAIL;
}
try {
rmidLock.lock();
this.config = config;
if (!updateRmid().equals(RegisterState.OK)) {
logger.error("registerRoomMachine error");
return RegisterState.ERROR;
}
if (!isRun) {
int timePeriod = config.getTimeUpdatePeriod();
if(timePeriod < 1){
logger.error("getTimeUpdatePeriod error:" + timePeriod + "<1");
return RegisterState.ERROR;
}
es.scheduleAtFixedRate(new TimeUpdater(), 0, timePeriod, TimeUnit.MILLISECONDS);
isRun = true;
}
isFail = false;
} finally {
rmidLock.unlock();
}
logger.info("registerRoomMachine success");
return RegisterState.OK;
}
/** 更新机房ID 和 机器ID */
private RegisterState updateRmid() {
logger.debug("updateRmid()");
int roomId = config.getRoomId();
int roomBitNum = config.getRoomBitNum();
int machineId = config.getMachineId();
int machineBitNum = config.getMachineBitNum();
if (roomId < 0 || machineId < 0) {
isFail = true;
logger.error("房间ID 或 机器ID不能小于0:roomId=" + roomId + "--machineId=" + machineId);
return RegisterState.ERROR;
}
if (roomBitNum < 1 || machineBitNum < 1) {
isFail = true;
logger.error("房间ID位数 或 机器ID位数不能小于1:roomBitNum=" + roomBitNum + "--machineBitNum=" + machineBitNum);
return RegisterState.ERROR;
}
if (roomBitNum + machineBitNum > 10) {
isFail = true;
logger.error("房间ID+机器ID组合后位数不能超过10位:roomBitNum=" + roomBitNum + "--machineBitNum=" + machineBitNum);
return RegisterState.ERROR;
}
if (roomId >= (1 << roomBitNum)) {
isFail = true;
logger.error("机房ID超过设定数值:" + roomId + ">=" + (1 << roomBitNum));
return RegisterState.ERROR;
}
if (machineId >= (1 << machineBitNum)) {
isFail = true;
logger.error("机器ID超过设定数值" + machineId + ">=" + (1 << machineBitNum));
return RegisterState.ERROR;
}
rmid = ((roomId << machineBitNum) ^ machineId) << 12;
lastTimeMills = (System.currentTimeMillis() << 23 >>> 1) ^ rmid;
return RegisterState.OK;
}
/**
* <b>注册状态</b><br>
* OK:注册机房ID和机器ID成功,可以开始获取主键。<br>
* FAIL:注册Fail对象成功,系统停止产生正确主键,全部返回-1。<br>
* ERROR:注册机房ID和机器ID失败,空对象或者参数错误,系统无法产生正确主键,全部返回-1。<br>
*
* @create 2016-12-22 21:06:35
*/
public enum RegisterState {
OK, FAIL, ERROR;
}
/**
* <b>时间定时更新器</b><br>
* @create 2016-12-22 22:09:45
*/
private class TimeUpdater implements Runnable {
@Override
public void run() {
try {
updateTime();
} catch (Exception e) {
logger.error("定时更新时间发生错误", e);
}
}
}
}
2.0 分布式自增长主键发生器
2.1 设计考虑
(1) 多表共用一个实例,避免连锁更新时间和代码复杂化。
(2) 每次增长到4096就归0并更新到最新时间,其它取缓存时间。
(3) 有文章说每次归0会导致0过多,Hash取模分表后0表的数据会偏多。但似乎并不会,因此没有采用随机数发生器。
2.2 代码
/**
* <b>分布式自增长主键发生器</b><br>
* 枚举单例,只允许公用一个实例。
* @author Tony.Lau
* @create 2016-12-23 09:50:41
*/
public enum PrimaryKeyGen {
INSTANCE;
private final Lock INCR_LOCK = new ReentrantLock();
private int increment = 0;
/**
* <b>1bit符号位 + 41bit时间 + 机房ID + 机器ID + 12bit自增长ID</b><br>
* @return 如果返回值小于等于0,则表示系统环境错误;大于0为正常值。
*/
public long getIncrKey() {
try {
INCR_LOCK.lock();
long time = 0l;
if (increment >= 4096) {
increment = 0;
if((time = TimeGenerator.INSTANCE.updateTime()) < 0){
return -1l;
}else{
return time ^ (increment++);
}
}else{
if((time = TimeGenerator.INSTANCE.getTime()) < 0){
return -1l;
}else{
return time ^ (increment++);
}
}
} finally {
INCR_LOCK.unlock();
}
}
}
3.0 使用示例
3.1 使用步骤
(1) 实现具体的配置类,譬如从配置文件获取配置信息,从zookeeper在线获取配置信息。
(2) 匿名静态代码块注册配置信息到时间发生器,然后就可以正常获取主键。
(3) 如果使用Spring容器,可以使用@Postconstruct初始化注册信息。
(4) 配置类的fail()方法:如发生异常情况,譬如与zookeeper失去连接,意味着节点可能被清理,其它机器上线后可能使用了相同的机器ID导致主键重复。因此可以在配置实现类中跟踪异常信息,并在异常出现时立刻调用fail()方法停止产生正确主键。
(5) 配置类的init()方法:如需要使用动态注册方式,可以将获取配置的代码在这里实现。
(6) 配置类的refresh()方法:如想动态扩容方便,运行期动态更新机器ID和机房ID,那么可以将实现放在这里。
注意事项:如果机房内的机器时间有快有慢,那么当一台机器意外下线,另外一台机器上线抢占了相同ID,那么很大可能会产生重复主键。编程实现时一定要注意:
① 机器时间一定要尽可能一致。
② 新上线机器一段时间内不会抢占其它机器ID,哪怕其已经下线。
3.2 示例代码
/**
* 使用示例
* @author Tony.Lau
*/
public class Example{
static {
RoomMachineConfig config = new RoomMachineConfig(0, 1, 0, 1, 1000);
RegisterState state = TimeGenerator.INSTANCE.registerRoomMachine(config);
}
private static PrimaryKeyGen keyGen = PrimaryKeyGen.INSTANCE;
public long getKey(){
return keyGen.getIncrKey();
}
private static class RoomMachineConfig extends AbstractRMConfig{
public RoomMachineConfig(){
this.init();
/*
if(config.change()){
refresh();
}
*/
}
public RoomMachineConfig(int roomId, int roomBitNum, int machineId, int machineBitNum, int timeUpdatePeriod) {
super(roomId, roomBitNum, machineId, machineBitNum, timeUpdatePeriod);
/*
if(config.change()){
refresh();
}
*/
}
@Override
protected RegisterState init() {
// 获取配置并设置参数
//this.roomId =
//this.roomBitNum =
//this.machineId =
//this.machineBitNum =
return TimeGenerator.INSTANCE.registerRoomMachine(this);
}
@Override
protected RegisterState refresh() {
// 获取配置并更新参数
//this.roomId =
//this.roomBitNum =
//this.machineId =
//this.machineBitNum =
return TimeGenerator.INSTANCE.registerRoomMachine(this);
}
@Override
public int getRoomId() {
return roomId;
}
@Override
public int getRoomBitNum() {
return roomBitNum;
}
@Override
public int getMachineId() {
return machineId;
}
@Override
public int getMachineBitNum() {
return machineBitNum;
}
@Override
public int getTimeUpdatePeriod(){
return timeUpdatePeriod;
}
}
}
4.0 其它事项
4.1 测试结果
(1) 单线程循环取4096000个主键,刚好1004毫秒,说明没有性能问题。
(2) 多线程分别循环取4096000个主键,用时2248毫秒,未发现重复值。
4.2 源码地址
https://github.com/tonylau08/dcafe
如测试使用过程中发现任何错误,请告知。如觉得不错,给我颗小星星。谢谢!
来源:oschina
链接:https://my.oschina.net/u/2960536/blog/814132