ID生成器-第二讲:实现一个客户端批量ID生成器?你还在为不了解ID生成器而烦恼吗?本文带你实现一个自定义客户端批量生成ID生成器?

一、为什么要实现?逻辑是什么?

介绍

ID 是标识符(identifier)的前缀,它代表一个可以唯一识别一个对象或者物体的名称。在软件系统中,ID 用于对一组信息进行标识,它是信息系统里最底层、最基础的概念,从系统诞生到消亡,都与 ID 息息相关。

在分布式系统中,ID生成器是至关重要的,因为分布式系统中,一个数据表可能存储在多个物理机上,这样主键Id如何生成,怎么保证其有序性,生成可靠性,生成稳定性等都是一个问题。

作为一个Java后端架构师,我们需要知道 ID生成器的原理和常见的ID生成器都有哪些,以及如何自己实现一个简易的Id生成器,加深对分布式系统的理解。

对比:

  • ID-多ServerRpc生成:
    • 优点:严格递增
    • 缺点:每次一个请求调用,生成一个,浪费RPC资源,稳定性取决于RPC和网络
  • ID-客户端批量生成:
    • 优点:减少网络调用,ID本地缓存获取更高效
    • 缺点:趋势递增,不是严格递增

二、自定义客户端批量生成

实现链接:

Crescent-Toolkit/src/main/java/io/github/qingguox/id/sequence/IdSequenceClient.java at main · qingguox/Crescent-Toolkit

效果: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;
    }
}
相关推荐
卡皮巴拉_32 分钟前
Trae Solo 在「日志分析」场景中的神级体验:比我写脚本快五倍
后端
传感器与混合集成电路36 分钟前
提升多轴同步精度:DSP+FPGA架构在高端特种装备伺服控制中的应用
嵌入式硬件·fpga开发·架构
okseekw36 分钟前
Java内部类实战指南:4种类型+5个经典场景,开发效率直接拉满!
java·后端
紫檀香37 分钟前
InfluxDB 3 入门指南
后端
猫猫能有什么坏心眼39 分钟前
采用sharding-jdbc分库分表
后端
猫猫能有什么坏心眼43 分钟前
ElasticSearch入门手册
后端
猫猫能有什么坏心眼43 分钟前
HashMap解析
后端
猫猫能有什么坏心眼1 小时前
Kafka安装教程
后端