
一、为什么要实现?逻辑是什么?
介绍
ID 是标识符(identifier)的前缀,它代表一个可以唯一识别一个对象或者物体的名称。在软件系统中,ID 用于对一组信息进行标识,它是信息系统里最底层、最基础的概念,从系统诞生到消亡,都与 ID 息息相关。
在分布式系统中,ID生成器是至关重要的,因为分布式系统中,一个数据表可能存储在多个物理机上,这样主键Id如何生成,怎么保证其有序性,生成可靠性,生成稳定性等都是一个问题。
作为一个Java后端架构师,我们需要知道 ID生成器的原理和常见的ID生成器都有哪些,以及如何自己实现一个简易的Id生成器,加深对分布式系统的理解。
对比:
- ID-多ServerRpc生成:
- 优点:严格递增
- 缺点:每次一个请求调用,生成一个,浪费RPC资源,稳定性取决于RPC和网络
- ID-客户端批量生成:
- 优点:减少网络调用,ID本地缓存获取更高效
- 缺点:趋势递增,不是严格递增

二、自定义客户端批量生成
实现链接:
效果:10000个Id

源码:
数据库定义
java
DROP DATABASE IF EXISTS `test`;
CREATE DATABASE `test`;
USE test;
DROP TABLE IF EXISTS `id_biz`;
CREATE TABLE `id_biz`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`biz_type` varchar(200) NOT NULL DEFAULT '' COMMENT '业务标识比如 表的标识',
`rule` int(10) NOT NULL DEFAULT '0' COMMENT '规则: 0:无 1:单号段 2:双号段缓存',
PRIMARY KEY(`id`),
UNIQUE KEY `uniq_biz_type`(`biz_type`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = 'id业务表';
INSERT INTO test.id_biz (id, biz_type, rule) VALUES (1, 'testGenIdSingleSection', 1);
INSERT INTO test.id_biz (id, biz_type, rule) VALUES (2, 'testGenIdTwoSection', 2);
DROP TABLE IF EXISTS `id_gen`;
CREATE TABLE `id_gen`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
`step` int(10) NOT NULL DEFAULT '0' COMMENT '号段长度',
`version` bigint(10) NOT NULL DEFAULT '0' COMMENT '版本',
`biz_id` bigint(10) NOT NULL DEFAULT '0' COMMENT '业务id',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_biz_id`(`biz_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = '业务id表';
INSERT INTO test.id_gen (id, max_id, step, version, biz_id) VALUES (1, 1, 10, 1, 1);
INSERT INTO test.id_gen (id, max_id, step, version, biz_id) VALUES (2, 1, 10, 1, 2);
生成器
规则:
java
package io.github.qingguox.id.sequence;
import io.github.qingguox.enums.EnumUtils;
import io.github.qingguox.enums.IntDescValue;
/**
* @author wangqingwei
* Created on 2022-08-18
*/
public enum IdRule implements IntDescValue {
UNKNOWN(0, "未知"),
SINGLE_SECTION(1, "单号段"),
TWO_SECTION(2, "双号段")
;
private final int value;
private final String desc;
IdRule(int value, String desc) {
this.value = value;
this.desc = desc;
}
@Override
public String getDesc() {
return desc;
}
@Override
public int getValue() {
return value;
}
public static IdRule fromValue(int value) {
return EnumUtils.fromValue(IdRule.class, value, UNKNOWN);
}
}
核心代码
java
package io.github.qingguox.id.sequence;
/**
* @author wangqingwei
* Created on 2022-08-18
*/
public interface IdSequenceClient {
long getId(long bizId);
IdRule supportRule();
}
package io.github.qingguox.id.sequence.impl;
import static io.github.qingguox.id.sequence.utils.DynamicChangeClassUtils.swapCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import io.github.qingguox.id.sequence.IdSequenceClient;
import io.github.qingguox.id.sequence.dao.IdGenDAO;
import io.github.qingguox.id.sequence.model.ClientIdCache;
import io.github.qingguox.id.sequence.model.IdGen;
import io.github.qingguox.json.JacksonUtils;
/**
* @author wangqingwei
* Created on 2022-08-18
*/
@Service
public abstract class AbstractIdSequenceClient implements IdSequenceClient {
private static final Logger logger = LoggerFactory.getLogger(AbstractIdSequenceClient.class);
@Autowired
private IdGenDAO idGenDAO;
protected long preCheckBizIdAndGenStartTimeMills(long bizId) {
long startTimeMills = System.currentTimeMillis();
IdGen idGen = idGenDAO.getByBizId(bizId);
Assert.notNull(idGen, "idGen not exists! bizId : " + bizId);
return startTimeMills;
}
protected ClientIdCache genClientIdCache(long bizId) {
IdGen idGen = idGenDAO.getByBizId(bizId);
final long version = idGen.getVersion();
final long maxId = idGen.getMaxId();
final long id = idGen.getId();
final int step = idGen.getStep();
long nextMaxId = maxId + step;
int updateCount = idGenDAO.updateByIdAndVersion(id, version, nextMaxId);
// 其他进程已经修改了
if (updateCount == 0) {
logger.info("other processor is updated! so try once! bizId : {}, curIdVersion : {}, curIdNextMaxId : {}",
bizId, version, nextMaxId);
return genClientIdCache(bizId);
}
final ClientIdCache
clientIdCache = new ClientIdCache(maxId, maxId, nextMaxId);
logger.info("genClientIdCache : {}, bizId : {}", JacksonUtils.toJSON(clientIdCache), bizId);
return clientIdCache;
}
protected long checkAndGet(long resultId, long startTimeMills) {
Assert.isTrue(resultId != 0L, "resultId is 0L");
final long endTimeMills = System.currentTimeMillis();
logger.info("checkAndGet cost : {}", endTimeMills - startTimeMills);
return resultId;
}
/**
* cache是否需要重新申请.
*/
protected boolean cacheIsNeedProcess(ClientIdCache cache) {
return cache == null || cache.getCurId() == cache.getRightId();
}
public static void main(String[] args) {
ClientIdCache a = new ClientIdCache(1, 2, 3);
ClientIdCache b = new ClientIdCache(3, 2, 1);
System.out.println(a + " " + b);
swapCache(a, b);
System.out.println(a + " " + b);
}
}
单号段生成
java
package io.github.qingguox.id.sequence.impl;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.github.qingguox.id.sequence.IdRule;
import io.github.qingguox.id.sequence.dao.IdGenDAO;
import io.github.qingguox.id.sequence.model.ClientIdCache;
/**
* id生成器-客户端批量单号段实现.
* 调用 io.github.qingguox.id.sequence.IdSequenceTest#testGenId(java.lang.String)
* testGenId("testGenIdSingleSection");
* Db: 表id_gen
* INSERT INTO test.id_gen (id, max_id, step, version, biz_id) VALUES (1, 1, 10, 1, 1);
* @author wangqingwei
* Created on 2022-08-18
*/
@Lazy
@Service
public class IdSequenceSingleSectionClient extends AbstractIdSequenceClient {
private static final Logger logger = LoggerFactory.getLogger(IdSequenceSingleSectionClient.class);
/**
* bizId, [1, 11) cur=1
* AtomicReference
*/
private final Cache<Long, ClientIdCache> idCache = CacheBuilder.newBuilder()
.maximumSize(500)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
private final Object LOCK = new Object();
@Autowired
private IdGenDAO idGenDAO;
@Override
public long getId(long bizId) {
final long startTimeMills = preCheckBizIdAndGenStartTimeMills(bizId);
synchronized (LOCK) {
long resultId;
ClientIdCache clientIdCache = idCache.getIfPresent(bizId);
if (clientIdCache == null || clientIdCache.getCurId() == clientIdCache.getRightId()) {
clientIdCache = genClientIdCache(bizId);
resultId = clientIdCache.getCurId();
clientIdCache.setCurId(resultId + 1);
idCache.put(bizId, clientIdCache);
logger.info("getIdBy : {}", "Db");
return checkAndGet(resultId, startTimeMills);
}
final long curId = clientIdCache.getCurId();
if (curId < clientIdCache.getRightId()) {
resultId = curId;
clientIdCache.setCurId(resultId + 1);
logger.info("getIdBy : {}", "Cache");
return checkAndGet(resultId, startTimeMills);
}
}
return checkAndGet(0L, startTimeMills);
}
@Override
protected ClientIdCache genClientIdCache(long bizId) {
// 极少意外情况, 比如当前线程被解锁了, 其他线程生成了一个
ClientIdCache oldCache = idCache.getIfPresent(bizId);
if (oldCache != null && oldCache.getCurId() < oldCache.getRightId()) {
logger.info("getIdBy : {}", "OtherThreadGen");
return oldCache;
}
return super.genClientIdCache(bizId);
}
@Override
public IdRule supportRule() {
return IdRule.SINGLE_SECTION;
}
}
双号段生成
java
package io.github.qingguox.id.sequence.impl;
import static io.github.qingguox.id.sequence.utils.DynamicChangeClassUtils.swapCache;
import static io.github.qingguox.money.NumberUtils.DEFAULT_SCALE;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.github.qingguox.id.sequence.IdRule;
import io.github.qingguox.id.sequence.model.ClientIdCache;
import io.github.qingguox.json.JacksonUtils;
/**
* id生成器-客户端批量双号段实现.
* 性能: 这样比单号段性能好一些, 当第一个号段快没数据的时候, 异步加载第二个号段数据.
* 调用 io.github.qingguox.id.sequence.IdSequenceTest#testGenId(java.lang.String)
* testGenId("testGenIdTwoSection");
* Db: 表id_gen
* INSERT INTO test.id_gen (id, max_id, step, version, biz_id) VALUES (2, 1, 10, 1, 2);
*
* @author wangqingwei
* Created on 2022-08-18
*/
@Lazy
@Service
public class IdSequenceTwoSectionClient extends AbstractIdSequenceClient {
private static final Logger logger = LoggerFactory.getLogger(IdSequenceTwoSectionClient.class);
private static final double PERCENTAGE = 0.8d;
/**
* bizId, [1, 11) cur=5
* AtomicReference
*/
private final Cache<Long, ClientIdCache> mainCache = CacheBuilder.newBuilder()
.maximumSize(500)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
/**
* bizId, [11, 21) cur=10
*/
private final Cache<Long, ClientIdCache> slaveCache = CacheBuilder.newBuilder()
.maximumSize(500)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
private final Object LOCK = new Object();
@Override
public long getId(long bizId) {
final long startTimeMills = preCheckBizIdAndGenStartTimeMills(bizId);
synchronized (LOCK) {
long resultId;
ClientIdCache clientIdCache = mainCache.getIfPresent(bizId);
ClientIdCache slaveIdCache = slaveCache.getIfPresent(bizId);
logger.info("clientIdCache : {}, slaveIdCache : {}", clientIdCache, slaveIdCache);
if (cacheIsNeedProcess(clientIdCache)) {
if (cacheIsNeedProcess(slaveIdCache)) {
clientIdCache = genClientIdCache(bizId);
resultId = clientIdCache.getCurId();
clientIdCache.setCurId(resultId + 1);
mainCache.put(bizId, clientIdCache);
logger.info("getIdBy : {}", "Db");
return checkAndGet(resultId, startTimeMills);
}
// 交换
swapCache(clientIdCache, slaveIdCache);
resultId = clientIdCache.getCurId();
clientIdCache.setCurId(resultId + 1);
logger.info("getIdBy : {}", "SlaveCache");
// 清理
slaveCache.invalidate(bizId);
return checkAndGet(resultId, startTimeMills);
}
final long curId = clientIdCache.getCurId();
if (curId < clientIdCache.getRightId()) {
resultId = curId;
clientIdCache.setCurId(resultId + 1);
logger.info("getIdBy : {}", "Cache");
// checkMainCacheCapacityAndGen();
checkMainCacheCapacityAndGen(clientIdCache, bizId);
return checkAndGet(resultId, startTimeMills);
}
}
return checkAndGet(0L, startTimeMills);
}
private void checkMainCacheCapacityAndGen(ClientIdCache mainCache, long bizId) {
final long curId = mainCache.getCurId();
final long rightId = mainCache.getRightId();
final long leftId = mainCache.getLeftId();
BigDecimal curPercentage = new BigDecimal(curId - leftId)
.divide(BigDecimal.valueOf(rightId - leftId), DEFAULT_SCALE, RoundingMode.DOWN);
logger.info("curPercentage : {}", curPercentage);
if (Double.compare(curPercentage.doubleValue(), PERCENTAGE) >= 0) {
// 看第二个是否有, 没有的话需要设置
ClientIdCache curSlaveCache = slaveCache.getIfPresent(bizId);
if (cacheIsNeedProcess(curSlaveCache)) {
curSlaveCache = genClientIdCache(bizId);
slaveCache.put(bizId, curSlaveCache);
logger.info("loadingSlaveCache curSlaveCache : {}, bizId : {}", JacksonUtils.toJSON(curSlaveCache), bizId);
}
}
}
@Override
public IdRule supportRule() {
return IdRule.TWO_SECTION;
}
}