1 问题引出
MySQL自增属性AUTO_INCREMENT可以保证单表id唯一,但是单表可能成为性能瓶颈,且扩展性较差。
2 几种生成方法比较
2.1 UUID
性能差、太长、占用空间大、不能反解析出信息、无序性导致B+树索引在写的时候有过多随机写操作。
2.2 基于DB表实现
分库分表,每个表从不同的数字开始自增、使用相同的步长。
假设分3张表:
初始值 | 步长 | 生成的id | |
表1 | 0 | 3 | 0、3、6、9、... |
表2 | 1 | 3 | 1、4、7、10、... |
表3 | 2 | 3 | 2、5、8、11、... |
缺点:把可提供id的服务节点数(表的个数)固定住了
3 Snowflake算法
全局唯一ID结构:
总长度64位,从低到高位依次划分为:
1)0~11位(共12bit)表示序列号,最大值2^12=4096,意味着在一个时间单位(例如1毫秒)内最多可以生成4096个ID;
2)12~21位(共10bit)表示机器id,最大值2^10=1024,意味着支持的最大集群规模为1024台机器。
3)22~62位(共41bit)表示时间戳,最大值2^41=2 199 023 255 552(单位:ms),意味着在这么多时间内我们可以肆意妄为地制造ID。是多久呢?一年按365天算,2^41 / 1000 / 3600 / 24 / 365 ≈ 69.7(年)。系统运行之前我们设置一个起始时间,例如“2019-2-21 00:00:00”,然后从此时开始算,差不多能用到2088年。
4) 63位(共1bit)最高位是符号位,不使用,设置为固定值“0”。
3.1 生成分布式全局唯一ID
/**
* 全局唯一id生成器
* global unique id generator
*/
public interface IdGenerator<T> {
T generateId();
}
/**
* 基于workId的全局唯一Id生成器
*/
public abstract class BaseWorkIdIdGenerator<T> implements IdGenerator<T> {
@Resource(name = "dbWorkIdResolver")
private WorkIdResolver workIdResolver;
protected Long getWorkId() {
return workIdResolver.resolveWorkId();
}
}
import org.springframework.stereotype.Service;
import java.util.Calendar;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*
* <p>
* 产生long类型的唯一id,基于Twitter的snow flake算法实现,单台机器每毫秒支持2^12=4096个id
*
* <p>
* 第1位为0,符号位。第2-42位表示毫秒数,共41位,当前时间毫秒-2018年02月21日的毫秒数。第43-52位表示workId,即机器id,共10位,能支持1024台机器。第53-64位表示序列号,共12位
*/
@Service("commonIdGenerator")
public class CommonIdGenerator extends BaseWorkIdIdGenerator<Long> {
public static final long START_TIME_MILLIS;
private static final long SEQUENCE_BITS = 12L; // 12位序列号
private static final long WORKER_ID_BITS = 10L; // 10位workId号
private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
private long sequence;
private long lastTime;
private Lock lock = new ReentrantLock();
static {
Calendar calendar = Calendar.getInstance();
calendar.set(2018, Calendar.FEBRUARY, 21);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
START_TIME_MILLIS = calendar.getTimeInMillis(); // 从2019.02.21开始
}
@Override
public Long generateId() {
// time
long currentTime;
// seq
long seq;
// 此处加锁也可以使用synchronized关键字,用来在多线程并发执行时保护lastTime、sequence这两个变量
lock.lock();
try {
currentTime = System.currentTimeMillis();
//时钟被回拨,直接拒绝服务
if (currentTime < lastTime) {
throw new IllegalStateException("Clock go back, refused generator guid service.");
}
if (currentTime == lastTime) {
//如果1ms内单台机器的4096个序号用完了,等待下一毫秒
if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
lastTime = waitUntilNextMillis(currentTime);
}
} else {
lastTime = currentTime;
sequence = 0;
}
currentTime = lastTime;
seq = sequence;
}finally {
lock.unlock();
}
return ((currentTime - START_TIME_MILLIS) << TIMESTAMP_LEFT_SHIFT_BITS)
| (getWorkId() << WORKER_ID_LEFT_SHIFT_BITS) | seq;
}
private long waitUntilNextMillis(final long fromMills) {
long nextMills = System.currentTimeMillis();
while (nextMills <= fromMills) {
nextMills = System.currentTimeMillis();
}
return nextMills;
}
}
代码说明:
1.到了java自带的锁,而不是分布式锁,因为只要保证在单机上线程安全就行了,不同机器由于使用了机器id来区分,所以不同机器不会生成相同的全局id。
2.时钟回拨问题:当前时间小于上一次发号结束时间,可能是时钟回拨了,这里直接抛异常拒绝服务,不然可能会产生重复id,因为回到了历史时间点。也可以设置一个等待,等到时钟时间大于上次发号时间再继续执行,不过这中间会等待多久不可确定,取决于回拨了多久。(注意下,生成ID的算法强依赖于机器时间,要是先回拨时间、再重启下应用,就没法判断是否回拨了,这样就有可能生成重复id,所以说禁止任何情况下回拨时钟,是基本要求。)
3.如何判断在此1ms内4096个号已经发完了:
0L == (sequence = ++sequence & SEQUENCE_MASK)
SEENCE_MASK值是2^12 - 1 = 4095,转换成二进制:111 111 111 111
最初,currentTime取当前时间,long类型的lastTime值为0,if (currentTime == lastTime)这个条件判断结果是false,所以会走lastTime = currentTime;sequence = 0;将lastTime赋值为当前时间、sequence设为0并返回。
从下次开始,lastTime有值了,就会走if分支,然后里面会校验4096个号码有没有发完(1~4095,0已经发过了)。
(++sequence) 从1开始,1 & SEQUENCE_MASK=1
000 000 000 001
& 111 111 111 111
---------------------------
000 000 000 001 (1)
一直到4095:
111 111 111 111
& 111 111 111 111
---------------------------
011 111 111 111 (4095)
到4096的时候又会回到0:
1 000 000 000 000
& 111 111 111 111
---------------------------
0 000 000 000 000 (0)
如此一轮下来正好是4096个号(0-4095)。
4.最后,把二进制ID转成十进制数字返回:((currentTime - START_TIME_MILLIS) << TIMESTAMP_LEFT_SHIFT_BITS)| (getWorkId() << WORKER_ID_LEFT_SHIFT_BITS) | seq
假设指定初始时间START_TIME_MILLIS为2019-02-21 00:00:00,转换成毫秒就是:1550678400000,假设现在currentTime是2019-02-22 13:34:23,则对应的毫秒是:1550813663000,减去初始值:1550813663000-1550678400000=135263000,转换成二进制:1000000011111111001100011000,然后左移12+10=22位,得到时间戳:
1000000011111111001100011000 0 000 000 000 000 000 000 000
getWorkId() 假设当前机器编号是1,转成二进制:1,再左移12位:
0 000 000 001 000 000 000 000
seq 假设当前生成的序号是1024,转成二进制:
010 000 000 000
最后对时间戳、机器编号、序号三者进行逻辑“或”运算:
1000000011111111001100011000 0 000 000 000 000 000 000 000
| 0 000 000 001 000 000 000 000
| 010 000 000 000
----------------------------------------------------------
1000000011111111001100011000 0 000 000 001 010 000 000 000
(转成十进制:567334141957120)
如此,便得到了全局唯一ID:567334141957120。
3.2 获取机器ID
(数据库可参考另一篇博客:Mybatis逆向工程_一次编写 到处调试的博客-优快云博客)
表设计:
CREATE TABLE `key_value_data` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_key` varchar(64) NOT NULL COMMENT 'key',
`data_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '类型',
`data_value` mediumtext,
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0失效 1有效',
`db_create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`db_update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_data_key_data_type` (`data_key`,`data_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='K-V表'
package service.guid;
/**
*
* 本机workId持有者
*/
public interface WorkIdResolver {
long resolveWorkId();
}
@Service("dbWorkIdResolver")
public class DbWorkIdResolver implements WorkIdResolver {
private volatile Long workId;
private static final String KEY = "GUID_WORK_ID";
@Autowired
private KeyValuePOMapperExt keyValuePOMapper;
@Resource
private WorkIdService workIdService;
/**
* 返回0 ~ 1023之间的workId,最大支持1024台机器
*
* @return
*/
@Override
public long resolveWorkId() {
return workId;
}
@PostConstruct
private void init() {
workId = workIdService.generateWorkId();
}
}
@Service
public class WorkIdService {
private static final String KEY = "GUID_WORK_ID";
private static final long WORKER_ID_MAX_VALUE = 1L << 10;
/**
* 默认最近使用的workId是0
*/
private static final Long DEFAULT_LAST_WORK_ID = 0L;
@Autowired
private KeyValuePOMapperExt keyValuePOMapper;
@Transactional
public Long generateWorkId() {
String ipAddr = NetwokUtils.getLocalhost();
KeyValuePOExample keyValuePOExample = new KeyValuePOExample(); keyValuePOExample.createCriteria().andKeyEqualTo(KEY).andBizTypeEqualTo(KeyValueBizTypeEnum.DEFAULT);
List<KeyValuePO> keyValuePOList = keyValuePOMapper.selectByExample(keyValuePOExample);
if (CollectionUtils.isEmpty(keyValuePOList)) {
// 数据库表里没数据,直接插入
WorkIdData workIdData = new WorkIdData();
Map<String, Long> workIdsMap = new HashMap<>(1);
workIdsMap.put(ipAddr, DEFAULT_LAST_WORK_ID);
workIdData.setWorkIdsMap(workIdsMap);
workIdData.setLastWorkId(DEFAULT_LAST_WORK_ID);
KeyValuePO insertPO = new KeyValuePO();
insertPO.setKey(KEY);
insertPO.setBizType(KeyValueBizTypeEnum.DEFAULT);
insertPO.setValue(JSON.toJSONString(workIdData));
keyValuePOMapper.insertSelective(insertPO);
return DEFAULT_LAST_WORK_ID;
}
// 数据库表里有数据,先取出来,然后看看有没有保存当前IP地址
KeyValuePO keyValuePO = keyValuePOList.get(0);
WorkIdData workIdData = JSON.parseObject(keyValuePO.getValue(), WorkIdData.class);
Map<String, Long> workIdsMap = workIdData.getWorkIdsMap();
if (workIdsMap != null && workIdsMap.containsKey(ipAddr)) {
// 已经保存了当前ip,直接返回对应的workId
return workIdsMap.get(ipAddr);
}
// 没有保存当前ip,把ip插进去
if (workIdsMap == null) {
workIdsMap = new HashMap<>();
}
long newLastId = workIdData.getLastWorkId() + 1;
checkWorkerId(workIdData.getLastWorkId());
workIdData.setLastWorkId(newLastId);
workIdsMap.put(ipAddr, newLastId);
KeyValuePO updatePO = new KeyValuePO();
updatePO.setValue(JSON.toJSONString(workIdData));
keyValuePOExample.getOredCriteria().get(0).andDbUpdateTimeEqualTo(keyValuePO.getDbUpdateTime());
keyValuePOMapper.updateByExampleSelective(updatePO, keyValuePOExample);
return newLastId;
}
/**
* 校验机器编码是否超过1024
*/
private void checkWorkerId(long workId) {
if (workId >= 0L && workId < WORKER_ID_MAX_VALUE) {
return;
}
throw new RuntimeException("workerId is overflow, attempt " + workId + " but max is " + WORKER_ID_MAX_VALUE);
}
}
4 反解析全局唯一ID
把生成的GUID解析出来:
先定义下GUID结构:
/**
* @Description 全局唯一id数据结构
* @Author lilong
* @Date 2019-02-21 14:44
*/
public class GuidBO {
/**
* 生成id的时间戳
*/
private Timestamp lockTime;
/**
* 机器id
*/
private Long workId;
/**
* 机器ip地址
*/
private String workIpAddr;
/**
* 生成的序列号
*/
private Long sequence;
// getter/setter
}
反解析GUID:
@Service("commonIdGenerator")
public class CommonIdGenerator extends BaseWorkIdIdGenerator<Long> {
public static final long START_TIME_MILLIS;
private static final long SEQUENCE_BITS = 12L; // 12位序列号
private static final long WORKER_ID_BITS = 10L; // 10位workId号
private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
private static final long WORK_ID_MASK = (1 << WORKER_ID_BITS) - 1; // 10位workId掩码
private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
static {
Calendar calendar = Calendar.getInstance();
calendar.set(2017, Calendar.APRIL, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
START_TIME_MILLIS = calendar.getTimeInMillis(); // 从2017.04.01开始
}
@Override
public Long generateId() {
...
}
@Override
public GuidBO parseGUID(Long id) {
GuidBO guidBO = new GuidBO();
//1.时间戳
long generateTimeLong = (id >> TIMESTAMP_LEFT_SHIFT_BITS) + START_TIME_MILLIS;
guidBO.setLockTime(new Timestamp(generateTimeLong));
//2.机器号
Long workId = (id >> SEQUENCE_BITS) & WORK_ID_MASK;
guidBO.setWorkId(workId);
//3.机器ip
guidBO.setWorkIpAddr(parseWorkerIp(workId));
//4.序列号
guidBO.setSequence(id & SEQUENCE_MASK);
return guidBO;
}
}
5 测试
public class CommonIdGeneratorTest extends BaseTest {
@Resource
private CommonIdGenerator commonIdGenerator;
@Test
public void testGenerateId() {
long guid = commonIdGenerator.generateId();
System.out.println("############## guid:" + guid);
GuidBO guidBO = commonIdGenerator.parseGUID(guid);
System.out.println("############## guidBO:" + JSON.toJSONString(guidBO));
}
}
6 机器编码耗尽问题
“3.2 获取机器ID”小节将机器ip与编号绑定起来,希望每台机器有固定的编号,但是这样引出了新的问题:当集群机器的ip地址频繁变动时,会导致编号持续增长,由于算法最大支持1024台机器,因此必须设置最大编号1023(0~1023共1024个唯一的机器编码),随着编码的递增,迟早会超过该值。
其实并不需要把机器ip与编码一一映射起来,只要保证在同一时刻集群中所有机器的编码都不同即可。具体做法是,在数据库中保存当前集群的最大编码,每次机器启动时,读取该编码并做“+1”操作,“+1”后的值作为该机器的编码,并更新到数据库中,同时设置编码上限阈值,当超过该阈值时,编码重置为0,如此循环往复。例如本次算法支持最多1024台机器,那么可以设置上限阈值为1023,即可支持编号0~1023共1024个唯一的机器编码。
优点:循环使用,编码不会无限膨胀,不会超过限制;
缺点:不能从ID反解析出生成ID的机器ip(不过这也无关紧要,一般不需要反解析ID)
示例代码:
package com.alibaba.fc.max.front.integration.backend;
@Service("dbWorkIdResolver")
public class DbWorkIdResolver implements WorkIdResolver {
private static final String KEY = "GUID_WORK_ID_";
private static final long WORKER_ID_MAX_VALUE = 1024L;
private volatile Long workId;
private CountDownLatch countDownLatch = new CountDownLatch(1);
@Resource(name = "keyValueDataDao")
private KeyValueDataDao keyValueDataDao;
/**
* 返回0 ~ 1023之间的workId,最大支持1024台机器
*
* @return
*/
@Override
public long resolveWorkId() {
if (null == workId) {
try {
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
return workId;
}
@PostConstruct
private void init() {
try {
/*
* 应用每次启动,都获取递增的workId,达到最大的1024后,重置为从0开始获取
*/
KeyValueDataPO keyValueData = keyValueDataDao.getKeyValueData(KEY,
KeyValueDataTypeEnum.DEFAULT.intValue());
if (null != keyValueData) {
long maxId = Long.parseLong(keyValueData.getDataValue());
maxId += 1;
if (maxId >= WORKER_ID_MAX_VALUE) {
maxId = 0L;
}
if (keyValueDataDao.updateKeyValueDataWithOldValue(KEY, String.valueOf(maxId),
KeyValueDataTypeEnum.DEFAULT.intValue(), keyValueData.getDbUpdateTime(),
keyValueData.getDataValue())) {
workId = maxId;
} else {
log.error("update database error, data={} ",
FastJsonUtil.toJSONString(keyValueData.getDbUpdateTime()));
}
} else {
if (keyValueDataDao.insertDataValue(KEY, KeyValueDataTypeEnum.DEFAULT.intValue(),
"0")) {
workId = 0L;
}
}
if (null == workId) {
// 抛异常,spring 初始化失败
throw new IllegalStateException("init workId fail");
}
} finally {
countDownLatch.countDown();
}
}
}
7 其他
1.最大峰值型 vs. 最小粒度型
通过调短时间戳长度和调长序列号长度,可以解决峰值压力大的情况。
最大峰值型能够承受更大的峰值压力,但是粗略有序的粒度有点大;最小粒度型有较细致的粒度,但是没毫秒能承受的峰值较小。
2.美团Leaf开源:https://github.com/Meituan-Dianping/Leaf