分布式id发号器实现
项目源码:hllypi/code-hub: technical solution code
上面有提到发号器的是基于本地+数据库实现,再开始之前我们需要建张表来维护这个id值
- 数据库配置
创建数据库:
sql
CREATE DATABASE common CHARACTER set utf8mb3 COLLATE=utf8_bin;
设计发号器表结构:
sql
CREATE TABLE `t_id_generate_config` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`next_threshold` bigint DEFAULT NULL COMMENT '当前id所在阶段的阈值',
`init_num` bigint DEFAULT NULL COMMENT '初始化值',
`current_start` bigint DEFAULT NULL COMMENT '当前id所在阶段的开始值',
`step` int DEFAULT NULL COMMENT 'id递增区间',
`is_seq` tinyint DEFAULT NULL COMMENT '是否有序(0无序,1有序)',
`id_prefix` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'id前缀',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
持久化对象:
arduino
@TableName(value ="t_id_generate_config")
@Data
public class IdGeneratePO implements Serializable {
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 备注
*/
private String remark;
/**
* 当前id所在阶段的阈值
*/
private Long next_threshold;
/**
* 初始化值
*/
private Long init_num;
/**
* 当前id所在阶段的开始值
*/
private Long current_start;
/**
* id递增区间
*/
private Integer step;
/**
* 是否有序(0无序,1有序)
*/
private Integer is_seq;
/**
* id前缀
*/
private String id_prefix;
/**
* 乐观锁版本号
*/
private Integer version;
/**
* 创建时间
*/
private Date create_time;
/**
* 更新时间
*/
private Date update_time;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
项目的初始化以及依赖可以自行实现,都是一些比较简单的操作,下面我将按照平时的开发习惯,一步一步的去完成发号器的底层业务代码;
首先,我们需要定义一个分布式id的服务,服务里面主要提供两个功能,一个是提供有序的id,另外一个提供无序的id
LocalSeqIdBO业务对象:
kotlin
**
* 功能:
* 作者:lc
* 日期:2025/12/2 12:37
*/
@Data
public class LocalSeqIdBO {
private Integer id;
private AtomicLong currentNum;
}
BO中包含内存中当前这个id值所所记录的该id段的当前值currentNum以及对应的业务id;
TIdGenerateConfigService服务接口:
java
/**
* @author 罗超
* @description 针对表【t_id_generate_config】的数据库操作Service
* @createDate 2025-12-02 12:30:47
*/
public interface TIdGenerateConfigService extends IService<IdGeneratePO> {
Long getSeqId(Integer seqId);
Long getUnSeqId(Integer seqId);
}
TIdGenerateConfigService服务实现类:
java
/**
* @author 罗超
* @description 针对表【t_id_generate_config】的数据库操作Service实现
* @createDate 2025-12-02 12:30:47
*/
@Service
@Slf4j
public class TIdGenerateConfigServiceImpl extends ServiceImpl<TIdGenerateConfigMapper, IdGeneratePO>
implements TIdGenerateConfigService, InitializingBean {
private static Map<Integer, LocalSeqIdBO> localSeqIdBOMap=new ConcurrentHashMap<>();
@Override
public Long getSeqId(Integer id) {
if(id==null){
log.error("id:{} is null.", id);
return null;
}
LocalSeqIdBO localSeqIdBO = localSeqIdBOMap.get(id);
if(localSeqIdBO== null){
log.error("localSeqIdBO:{} is null.", id);
return null;
}
long resultId = localSeqIdBO.getCurrentNum().getAndIncrement();
return resultId;
}
public Long getUnSeqId(Integer seqId) {
return null;
}
@Override
public void afterPropertiesSet() throws Exception {
for(IdGeneratePO idGeneratePO: baseMapper.selectList( null)){
LocalSeqIdBO localSeqIdBO = new LocalSeqIdBO();
AtomicLong atomicLong = new AtomicLong(idGeneratePO.getCurrent_start());
localSeqIdBO.setCurrentNum(atomicLong);
localSeqIdBO.setSeqId(idGeneratePO.getId());
localSeqIdBOMap.put(idGeneratePO.getId(), localSeqIdBO);
}
}
}
id所对应的是一种分布式id生成策略,比如用户服务生成id,订单生成服务id,消息生成服务id。而这个策略在前面的结构有提到是缓存在本地内存的,这里我们采用Map<Integer, LocalSeqIdBO> localSeqIdBOMap来进行缓存,我们就可以从map中通过id取出对应的对象,来获取有序id;
到了这里,大家可能会有一个小小的疑惑,你Map只是实例化了,但是没有数据呀。别慌,我们这里是在TIdGenerateConfigServiceImpl类初始化时来给我们的map进行初始化。这里我们可以通过InitializingBean这个回调接口来做一下初始化。然后我们只需要去数据库中做一次表的查询,将数据塞入到map中即可;
bean初始化会对调到这里
scss
@Override
public void afterPropertiesSet() throws Exception {
for(IdGeneratePO idGeneratePO: baseMapper.selectList( null)){
LocalSeqIdBO localSeqIdBO = new LocalSeqIdBO();
AtomicLong atomicLong = new AtomicLong(idGeneratePO.getCurrent_start());
localSeqIdBO.setCurrentNum(atomicLong);
localSeqIdBO.setSeqId(idGeneratePO.getId());
localSeqIdBOMap.put(idGeneratePO.getId(), localSeqIdBO);
}
在上面的有一个小细节,currentNum的数据类型我没有使用long这里样的类型,而是使用的是原子类AtomicLong,这是因为在当前的这个场景中是存在并发并且需要对currentNum+1的这个操作,使用long是不合理的。
上面就是我们的分布式id发号器的一个非常简单的实现了,那么上面的设计有没有什么问题呢?很明显,我们每次在缓存的只是一个seq步长的区间段,如果我们将区间中的id都发完了怎么办?其次我们在分布式下可能会部署多台机器,那么就会出现将相同的id重复发放,出现id碰撞。
这里我们为了保证id段不会出现重复的情况,我们在进行数据库与本地内存同步的,采用乐观锁的方式来实现来
这里的乐观锁的性能相对于使用redis的分布式锁,可能性能会低一点,但是考虑到出现竞争的情况是比较小的,所以采用这种方式实现。
乐观锁的实现:
ini
UPDATE t_id_generate_config
SET
next_threshold = next_threshold + step,
current_start = current_start + step,
version = version + 1
WHERE id = ? AND version = ?
持久层:
less
@Update("""
UPDATE t_id_generate_config
SET next_threshold = next_threshold + step,current_start = current_start + step,version = version + 1
WHERE id = ? AND version = ?
""")
int updateIdCountAndVersion(@Param("id") int id, @Param("version") int version);
基于mybatis-plus的实现,自行实现都行。
现在我们需要修改TIdGenerateConfigService中的回调方法,由于这块的逻辑比较长我们简单封装一下,需要注意的是在重试时,需要重新去查一次数据库的idGeneratePO
ini
/**
* 同步本地数据前,告诉数据库抢占成功,防止重复抢占
*
* @param idGeneratePO
*/
public void tryUpdateMysqlRecord(IdGeneratePO idGeneratePO) {
int result = idGenerateMapper.updateIdCountAndVersion(idGeneratePO.getId(), idGeneratePO.getVersion());
// 未占用,同步本地内存
if (result > 0) {
LocalSeqIdBO localSeqIdBO = new LocalSeqIdBO();
AtomicLong atomicLong = new AtomicLong(idGeneratePO.getCurrentStart());
localSeqIdBO.setCurrentNum(atomicLong);
localSeqIdBO.setId(idGeneratePO.getId());
localSeqIdBO.setCurrentStart(idGeneratePO.getCurrentStart());
localSeqIdBO.setNextThreshold(idGeneratePO.getNextThreshold());
localSeqIdBOMap.put(idGeneratePO.getId(), localSeqIdBO);
return;
}
// 重试3次,失败抛出异常
for (int i = 0; i < 3; i++) {
idGeneratePO = baseMapper.selectById(idGeneratePO.getId());
result = idGenerateMapper.updateIdCountAndVersion(idGeneratePO.getId(), idGeneratePO.getVersion());
// 抢占成功,更新返回
if (result > 0) {
LocalSeqIdBO localSeqIdBO = new LocalSeqIdBO();
AtomicLong atomicLong = new AtomicLong(idGeneratePO.getCurrentStart());
localSeqIdBO.setCurrentNum(atomicLong);
localSeqIdBO.setId(idGeneratePO.getId());
localSeqIdBO.setCurrentStart(idGeneratePO.getCurrentStart());
localSeqIdBO.setNextThreshold(idGeneratePO.getNextThreshold());
localSeqIdBOMap.put(idGeneratePO.getId(), localSeqIdBO);
return;
}
}
throw new RuntimeException("抢占id段 fail,id is " + idGeneratePO.getId());
}
现在呢我们即使部署多个实例,在多个应用启动的时候会告诉mysql要占用这个id段,也是不会出现id段重复的情况了,接着上游在调用我们的getSeqId()时就可以在我们的本地map中通过id来匹配到对应的LocalSeqIdBO对象,拿到currentNum,从而拿到id,来保证我们的上游拿到的值是唯一和有序的
上面的实现还有没有问题呢?
同样很明显,我们的id段是有限,用完以后怎么呢?这时我们需要重新去数据库中抢占id段。
也就是会重复前面实现的本地map的同步,这个过程包括重新获取当前PO,然后进行更新,然后更新失败会有一个3次的重试,而这个过程实际上是有多次的网络io,针对于高并发的情况下,这是会出现阻塞的情况。但凡下游的出现点阻塞就很容易出现上游的奔溃,甚至导致雪崩
我能想到的解决方案呢,是提前去获取新的id段,而不是在id段分发完以后再去获取新的i段,为了不阻塞正常的的id分发,通过异步的方式方式去实现;
异步更新本地id段:
java
// 更新阈值比例
private static final float UPDATE_RATE = 0.75f;
// 创建线程池
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8, 16,
3, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), r -> {
Thread thread = new Thread(r);
thread.setName("id-generate-thread-" + ThreadLocalRandom.current().nextInt(1000));
return thread;
});
@Override
public Long getSeqId(Integer id) {
if (id == null) {
log.error("id:{} is null.", id);
return null;
}
LocalSeqIdBO localSeqIdBO = localSeqIdBOMap.get(id);
if (localSeqIdBO == null) {
log.error("localSeqIdBO:{} is null.", id);
return null;
}
// 异步获取id段
this.refreshLocalSeqId(localSeqIdBO);
long resultId = localSeqIdBO.getCurrentNum().getAndIncrement();
return resultId;
}
/**
* 当id分发超过75%,异步更新id段
*
* @param localSeqIdBO
*/
private void refreshLocalSeqId(LocalSeqIdBO localSeqIdBO) {
long seq = (localSeqIdBO.getNextThreshold() - localSeqIdBO.getCurrentStart());
// 判断是否需要重新抢占
if (localSeqIdBO.getCurrentNum().getAndIncrement() - localSeqIdBO.getCurrentStart() < seq * UPDATE_RATE) {
threadPoolExecutor.execute(() -> {
IdGeneratePO idGeneratePO = idGenerateMapper.selectById(localSeqIdBO.getId());
tryUpdateMysqlRecord(idGeneratePO);
});
}
}
上面的代码就完成了异步任务,但是我们这里会有一个bug,当我们的发号器的数量超过75%,就会出发异步请求,现在的代码会导致,系统在发剩下的25%时,如果更新没有那么及时,就会多次触发异步任务,导致重复执行异步任务,而且我们只希望执行一次。这里我们就可以使用分布式锁或者信号量来实现。但是建议选择信号量更简单,不用多引入依赖多维护一个组件,并且限流场景也更适合使用信号量
定义semaphoreMap集合和修改数据初始化:
java
// 更新限流
private static Map<Integer, Semaphore> semaphoreMap = new HashMap<>();
@Override
public void afterPropertiesSet() throws Exception {
for (IdGeneratePO idGeneratePO : baseMapper.selectList(null)) {
this.tryUpdateMysqlRecord(idGeneratePO);
// 针对不同策略,初始化不同限流工具
semaphoreMap.put(idGeneratePO.getId(), new Semaphore(1));
}
}
修改refreshLocalSeqId方法:
ini
/**
* 当id分发超过75%,异步更新id段
*
* @param localSeqIdBO
*/
private void refreshLocalSeqId(LocalSeqIdBO localSeqIdBO) {
long seq = (localSeqIdBO.getNextThreshold() - localSeqIdBO.getCurrentStart());
// 判断是否需要重新抢占
if (localSeqIdBO.getCurrentNum().getAndIncrement() - localSeqIdBO.getCurrentStart() < seq * UPDATE_RATE) {
Semaphore semaphore = semaphoreMap.get(localSeqIdBO.getId());
if (semaphore == null) {
log.error("semaphore:{} is null.", localSeqIdBO.getId());
return;
}
boolean tryAcquireStatus = semaphore.tryAcquire();
if (!tryAcquireStatus) {
threadPoolExecutor.execute(() -> {
try {
IdGeneratePO idGeneratePO = idGenerateMapper.selectById(localSeqIdBO.getId());
tryUpdateMysqlRecord(idGeneratePO);
} finally {
semaphore.release();
}
});
}
}
}
为了避免线程阻塞导致重复请求数据库和性能问题,我们选择使用tryAcquire(),并且我们需要在抢占完成以后将semaphore关闭,不然会会在下次更新时出现无法获得许可情况。
上面虽已经提前刷新,但不排除极端情况下,出现id发放超出进行校验一下,对于快速失败还是同步刷新,请自己思考一下。
kotlin
// 提供保护机制(思考这里是快速失败还是同步刷新)
if (localSeqIdBO.getCurrentNum().get() > localSeqIdBO.getNextThreshold()) {
log.error("localSeqIdBO:{} is full.id generation failed", id);
return null;
}
测试大家可以自己打上日志,然后在本地跑跑,上面的代码有几处小问题。
虽然前面我们通过异步方式去防止阻塞线程,通过阈值提前去获取刷新id段,去尽可能的提高性能,去解决高并发的场景,但仅仅做好这些是完全不够的,因为决定我们发号的能力的主要是step的大小,他决定了我们机器能有多大的qps,步长越大这块性能越大,但不是越大越好的。所以要分析好系统的实际情况,合理设置步长
这里简单说说,正常来说一台机器能抗500qps并发我觉得是已经是极限了
500并发--->一台机器
正常系统0.几ms就能完成一次发号,假设1ms这里------》500次请求------》500*1ms=500ms
假设极端情况假如------》更新和查询2s,所以需要满足2s内没有发完id段,所以需要留1000id在内存中
25%-》1000
100%-》4000
上面已经完成了分布式id的有序id的代码实现
无序id的生成与有序id的逻辑基本相同,只需要注意一下细节即可。
创建业务类LocalUnSeqIdBO:
java
@Data
public class LocalUnSeqIdBO {
// id
private int id;
// 缓存当前值
private ConcurrentLinkedQueue<Long> idQueue;
// 下一次阈值
private long nextThreshold;
// 当前开始值
private long currentStart;
}
我们这里用ConcurrentLinkedQueue来存储无序id;
修改本地缓存初始化代码:
ini
/**
* 处理缓存本地id段
*
* @param idGeneratePO
*/
private void localIdBOHandler(IdGeneratePO idGeneratePO) {
if (idGeneratePO.getIsSeq() == ORDER_SEQ) {
LocalSeqIdBO localSeqIdBO = new LocalSeqIdBO();
AtomicLong atomicLong = new AtomicLong(idGeneratePO.getCurrentStart());
localSeqIdBO.setCurrentNum(atomicLong);
localSeqIdBO.setId(idGeneratePO.getId());
localSeqIdBO.setCurrentStart(idGeneratePO.getCurrentStart());
localSeqIdBO.setNextThreshold(idGeneratePO.getNextThreshold());
localSeqIdBOMap.put(idGeneratePO.getId(), localSeqIdBO);
log.info("抢占成功:{}__{}", idGeneratePO.getCurrentStart(), idGeneratePO.getNextThreshold());
} else {
LocalUnSeqIdBO localUnSeqIdBO = new LocalUnSeqIdBO();
localUnSeqIdBO.setId(idGeneratePO.getId());
localUnSeqIdBO.setCurrentStart(idGeneratePO.getCurrentStart());
localUnSeqIdBO.setNextThreshold(idGeneratePO.getNextThreshold());
// 缓存无序id到队列
long start = idGeneratePO.getCurrentStart();
long end = idGeneratePO.getNextThreshold();
ArrayList<Long> idsList = new ArrayList<>();
for (; start < end; start++) {
idsList.add(start);
}
// 打乱数组元素数据
Collections.shuffle(idsList);
ConcurrentLinkedQueue<Long> newQueue = new ConcurrentLinkedQueue<>(idsList);
localUnSeqIdBO.setIdQueue(newQueue);
localUnSeqIdBOMap.put(idGeneratePO.getId(), localUnSeqIdBO);
}
}
我们将代码封装一下,添加了一个判断来区分是否是有序的,对于无序的我们先将其存入一个缓存数组中,然后打乱后加入队列
刷新id段与有序id生成方式的刷新几乎一样
ini
/**
* 当id分发超过75%,异步更新id段
*
* @param localUnSeqIdBO
*/
private void refreshLocalUnSeqId(LocalUnSeqIdBO localUnSeqIdBO) {
long start = localUnSeqIdBO.getCurrentStart();
long end = localUnSeqIdBO.getNextThreshold();
int size = localUnSeqIdBO.getIdQueue().size();
if(size >= (end - start) * UPDATE_RATE){
Semaphore semaphore = semaphoreMap.get(localUnSeqIdBO.getId());
if (semaphore == null) {
log.error("semaphore:{} is null.", localUnSeqIdBO.getId());
return;
}
boolean tryAcquireStatus = semaphore.tryAcquire();// tryAcquire不论是否成功都只请求一次,避免重复请求和阻塞
if (tryAcquireStatus) {
threadPoolExecutor.execute(() -> {
try {
IdGeneratePO idGeneratePO = idGenerateMapper.selectById(localUnSeqIdBO.getId());
tryUpdateMysqlRecord(idGeneratePO);
} catch (Exception e){
log.error("更新id段失败",e);
} finally {
semaphore.release();
}
});
}
}
}
对外暴露方法:
kotlin
@Override
public Long getUnSeqId(Integer id) {
if (id == null) {
log.error("[getSeqId] id is error,id is {}", id);
return null;
}
LocalUnSeqIdBO localUnSeqIdBO = localUnSeqIdBOMap.get(id);
if (localUnSeqIdBO == null) {
log.error("[getUnSeqId] localUnSeqIdBO is null,id is {}", id);
return null;
}
Long returnId = localUnSeqIdBO.getIdQueue().poll();
if (returnId == null) {
log.error("[getUnSeqId] returnId is null,id is {}", id);
return null;
}
this.refreshLocalUnSeqId(localUnSeqIdBO);
return returnId;
}
上面的代码实现也有一处小小问题
如果没有发现问题,可以fork项目进行对比,项目源码 hllypi/code-hub: technical solution code,制作不易,欢迎star关注~