一开始我是拒绝学习这个知识的,因为MySQL明明就有自增属性AUTO_INCREMENT,何必多此一举、大费周章,最后就出来一个id,是否舍近求远、舍本逐末?
答案是:对,也不对。
说对,是因为一般来讲,用数据库自增字段应付得来了;
说不对,是因为要考虑到扩展性,一旦数据量太大需要扩容、分库分表的时候就炸了,当然你也可以在分表时从上一个表的末尾值开始自增,但终归是要强依赖数据库,扩展性和性能都不行。
性能差、太长、占用空间大、不能反解析出信息、无序性导致B+树索引在写的时候有过多随机写操作。
方法就是上面问题引出里介绍的。
从分布式的角度来说,如果请求量过大,还会导致单表性能问题。解决这个问题的一个好办法是分库分表,每个表从不同的数字开始自增、使用相同的步长。
假设我们分出来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个表对外提供,再加一个都不行。而且仍然是数据库的实现。
语言:Java
算法:Twitter的Snowflake算法
机器ID:数据库实现
算法介绍:
即将生成的分布式全局唯一ID的结构:
总长度64位,从低位到高位依次划分为:
1)0~11位(共12bit)表示序列号,最大值2^12=4096,意味着在一个时间单位(我们用毫秒,当然你也可以用秒)内最多可以生成4096个ID;
2)12~21位(共10bit)表示机器id,最大值2^10=1024,意味着可以在1024台机器上部署我们的算法,当然了,像我所在的团队,一个应用能有4台机器就是“富农”了,6台都能成“地主”了,所以丝毫不用担心。
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,不用,说是二进制里面最高位是1的是负数,我们用0表示正数就好了。
/**
* 全局唯一id生成器
* global unique id generator
*/
public interface IdGenerator {
T generateId();
}
/**
* 基于workId的全局唯一Id生成器
*/
public abstract class BaseWorkIdIdGenerator implements IdGenerator {
@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;
/**
*
*
* 产生long类型的唯一id,基于Twitter的snow flake算法实现,单台机器每毫秒支持2^12=4096个id
*
*
* 第1位为0,符号位。第2-42位表示毫秒数,共41位,当前时间毫秒-2017年04月01日的毫秒数。第43-52位表示workId,即机器id,共10位,能支持1024台机器。第53-64位表示序列号,共12位
*/
@Service("commonIdGenerator")
public class CommonIdGenerator extends BaseWorkIdIdGenerator {
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;
}
}
我们先看看以上3大段代码,暂不去看机器id(workId)的生成方法,等下会单独讲解。
刚开始看到锁这个东西,我愣了一分钟,我记得这东西是在同一个JVM里面才管用,分布式不是要用分布式锁吗?我甚至想了下分布式锁的实现方式有:数据库、Redis、Zookeeper几种。
然后才恍然大悟、拍手叫绝,此处只要保证在单机上线程安全就行了,因为不同机器使用了机器id来区分,所以不同机器不会生成相同的id,分布式一致性得到了保证。
1)59-61行,当前时间小于上一次发号结束时间,那肯定是时钟被回拨了,这里直接抛异常拒绝服务,不然可能会产生重复id,因为回到了历史时间点。
2)65行是判断这个时间段内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)。
3)78行,把结果转换成十进制的数字
((currentTime - START_TIME_MILLIS) << TIMESTAMP_LEFT_SHIFT_BITS)
| (getWorkId() << WORKER_ID_LEFT_SHIFT_BITS) | seq
我们知道,起始时间是2019-02-21 00:00:00,START_TIME_MILLIS转换成毫秒就是:1550678400000,假设现在是2019-02-22 13:34:23,则currentTime对应的毫秒是: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。
注意下,因为生成ID的算法强依赖于机器时间,所以禁止回拨时钟,我们代码里对于回拨时钟、未回归到拨回时间前的时间段是拒绝服务的。要是先回拨时间、再重启下应用,就没法判断是否回拨了,这样就有可能生成重复id,所以说禁止任何情况下回拨时钟,是基本要求。
对应的数据库表请参考另一篇博客:https://blog.csdn.net/u010266988/article/details/87870647
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";
/**
* 默认最近使用的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 keyValuePOList = keyValuePOMapper.selectByExample(keyValuePOExample);
if (CollectionUtils.isEmpty(keyValuePOList)) {
// 数据库表里没数据,直接插入
WorkIdData workIdData = new WorkIdData();
Map 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 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;
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;
}
}
假设事先塞了两个ip,此时会把本机ip作为新的worker加进去:
把生成的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 {
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;
}
}
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));
}
}
测试结果:
lockTime转换成时间:2019-02-24 03:45:37(时间戳工具:https://tool.lu/timestamp/)
美团Leaf开源:https://github.com/Meituan-Dianping/Leaf
介绍:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html