高并发系统-分布式唯一ID生成(二)-号段模式及应用

本文紧接着上文高并发系统-分布式唯一ID生成(一)

2. 生成方案

2.4 号段模式

号段模式,则是指发号服务每次会从数据库获取一批 ID,然后将这批 ID 缓存到发号服务本地

业务系统每次向发号服务请求 ID 时,后者会先判断其本地是否还有可用的 ID 分配给业务系统。如果有则直接分配,反之则再次访问数据库来批量获取 ID。

1. 优点

显然在号段模式下,由于发号服务不用每次都请求数据库

提高了系统的可用性、可靠性

后续由于业务拓展、业务系统激增时,对基于号段模式的设计方案进行分库分表也非常便于实现

2. 缺点

依赖数据库,数据库本身有一些缺陷

ID无业务含义

3. 应用场景

应用在一些并发量中等,不想依赖额外中间件(如REDIS)和发号服务场景。 比如生成订单号场景,可通过应用服务横向扩展

4. 案例

下面是一个基于号段模式生成CODE的案例,可集成在微服务本身,只需要在数据库增加一张表即可。

数据库设计

数据库表如下:

sql 复制代码
drop TABLE IF EXISTS `code_seq`;
create TABLE `code_seq` (
                            `env` VARCHAR(10) not null COMMENT '环境编号',
                            `prefix` int(10) not null COMMENT 'code前缀',
                            `seq` int(10) not null COMMENT '当前序列',
                            `update_time` datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
                            PRIMARY KEY(`prefix`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment 'CODE编号表';

可在code_seq预制多条数据,每条相当于是一个自增主键,有很好的扩展性。

每次获取一批号段,需要更新sequpdate_time

预制数据如下

sql 复制代码
INSERT INTO code_seq (env, prefix, seq)
VALUES
    ('T1', 1001, 0),
    ('T1', 1002, 0),
    ('T1', 1003, 0),
    ('T1', 1004, 0),
    ('T1', 1005, 0),
    ('T1', 1006, 0),
    ('T1', 1007, 0),
    ('T1', 1008, 0),
    ('T1', 1009, 0),
    ('T1', 1010, 0);

代码设计

整体UML类图如下:

CodeHandler:主类,提供初始化和生成Code方法

主要采用并发原子类减少并发依赖,从数据库可以捞取一批codePrefixSet,在code缓存不到UPDATE_THRESHOLD比例时,会域加载下一批code到缓存中

java 复制代码
public class CodeHandler {
    private static final double UPDATE_THRESHOLD = 0.9f;

    private final String name;

    private final Set<Integer> codePrefixSet = new ConcurrentSkipListSet<>();

    private final BlockingQueue<CodeSegment> codeSegmentBlockingQueue = new LinkedBlockingQueue<>(1);

    private final AtomicBoolean needLoading = new AtomicBoolean(false);

    private final AtomicBoolean statusHealthy = new AtomicBoolean(false);

    private final String idc = "T1";

    private CodeSeqService codeSeqService;

    private CodeTpsMonitor codeTpsMonitor;

    private Executor loadingExecutor;

    public CodeHandler(String name) {
        this.name = name;
    }

    public void setCodeSeqService(CodeSeqService codeSeqService) {
        this.codeSeqService = codeSeqService;
    }

    public void init() {
        this.loadingExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("order-handle-" + name).daemon(true).priority(Thread.MAX_PRIORITY).build());
        this.codeTpsMonitor = new CodeTpsMonitor(name);
        this.codeTpsMonitor.start();
        initQueue();
    }

    private void initQueue() {
        CodeSeqRet codeSeqRet = loadCodeAlloc();
        try {
            codeSegmentBlockingQueue.put(new CodeSegment(codeSeqRet.getPrefix(), codeSeqRet.getEnd(), codeSeqRet.getStart()));
        } catch (InterruptedException e) {
            log.error("initQueue put error", e);
        }
    }


    private CodeSeqRet loadCodeAlloc() {
        CodeSeqRet result = null;
        try {
            loadCodePrefix();
            int retryCount = 0;
            while (Objects.isNull(result) && !codePrefixSet.isEmpty() && retryCount <= 2) {
                int index = SecureRandomUtil.getInstance().nextInt(codePrefixSet.size());
                Integer prefix = (codePrefixSet.toArray(new Integer[codePrefixSet.size()]))[index];
                CodeSeqRet codeSeqRet = codeSeqService.generateCodeByPrefix(idc, prefix, codeTpsMonitor.getStep().get());
                int retCode = Objects.isNull(codeSeqRet) ? 1 : codeSeqRet.getResult();
                if (retCode == 0) {
                    result = codeSeqRet;
                } else if (retCode == 1) {
                    this.codePrefixSet.remove(prefix);
                    if (this.codePrefixSet.isEmpty()) {
                        loadCodePrefix();
                    }
                } else {
                    retryCount++;
                }
            }
        } finally {
            this.statusHealthy.set(result != null);
        }
        if (Objects.isNull(result)) {
            throw new IllegalStateException("load code from db error!");
        }
        return result;
    }

    private void loadCodePrefix() {
        if (!codePrefixSet.isEmpty()) {
            return;
        }
        codePrefixSet.addAll(codeSeqService.selectPrefixByEnv(idc));
        if (codePrefixSet.isEmpty()) {
            throw new IllegalStateException("no code prefix,plz check db config.");
        }
    }

    public String getCode() {
        this.codeTpsMonitor.increase();
        String code = null;
        int retryNum = 0;
        while (Objects.isNull(code)) {
            CodeSegment curSegment = this.codeSegmentBlockingQueue.peek();
            if (Objects.nonNull(curSegment)) {
                if (curSegment.getIdle() <= UPDATE_THRESHOLD * codeTpsMonitor.getStep().get() && this.needLoading.compareAndSet(false, true)) {
                    this.loadingExecutor.execute(new CodeLoader());
                }
                code = curSegment.getCode();
                if (Objects.isNull(code)) {
                    this.codeSegmentBlockingQueue.poll();
                }
            } else {
                if (!this.statusHealthy.get() || retryNum > 2) {
                    throw new IllegalStateException("create code failed,no available codes.");
                }
            }
            retryNum++;
            if (Objects.isNull(code)) {
                LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
            }
        }
        return code;
    }

    private class CodeLoader implements Runnable {

        @Override
        public void run() {
            CodeSeqRet codeSeqRet = null;
            while (Objects.isNull(codeSeqRet)) {
                try {
                    codeSeqRet = loadCodeAlloc();
                } catch (Exception e) {
                    log.error("load code error.", e);

                    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                }
            }
            try {
                CodeHandler.this.codeSegmentBlockingQueue.put(new CodeSegment(codeSeqRet.getPrefix(), codeSeqRet.getEnd(), codeSeqRet.getStart()));
            } catch (InterruptedException e) {
                log.error("CodeLoader put queue error", e);
            }

            CodeHandler.this.needLoading.set(false);
        }
    }
}

CodeSegment记录prefix以及当前队列值和剩余余量方法

java 复制代码
public class CodeSegment {
    private final int prefix;

    private final int maxSeq;

    private AtomicLong curSequence;

    public CodeSegment(int prefix,int maxSeq,long start) {
        this.prefix = prefix;
        this.maxSeq = maxSeq;
        curSequence = new AtomicLong(start);
    }

    public String getCode() {
        long value = curSequence.getAndIncrement();
        if (value <= maxSeq) {
            return prefix + String.format(Locale.ENGLISH,"%07d",value);
        } else {
            return null;
        }
    }

    public long getIdle() {
        return maxSeq - curSequence.get() + 1;
    }
}

CodeTpsMonitor是TPS监控器,可以根据CODE生成值动态调整预获取CODE的批量值

java 复制代码
public class CodeTpsMonitor implements Runnable {
    public static final int INITIAL_BATCH_COUNT = 100;

    private static final int MAX_BATCH_COUNT = 1000;

    private final AtomicInteger step = new AtomicInteger(INITIAL_BATCH_COUNT);

    private final String name;

    private AtomicInteger count = new AtomicInteger(0);

    private long startTime;

    private ScheduledExecutorService scheduledExecutorService;

    public CodeTpsMonitor(String name) {
        this.name = name;
    }

    public void start() {
        scheduledExecutorService = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder()
                .namingPattern("check-" + name + "-order-thread").daemon(true).build());
        scheduledExecutorService.scheduleWithFixedDelay(this, 1, 5, TimeUnit.SECONDS);
    }

    public void increase() {
        count.incrementAndGet();
    }

    public AtomicInteger getStep() {
        return step;
    }

    @Override
    public void run() {
        //重置count和时间
        long start = startTime;
        int reqNum = count.getAndSet(0);
        startTime = System.currentTimeMillis();

        long timeCost = startTime - start;
        final long tps = reqNum * 1000 / timeCost;
        int newBatchCount;
        if (tps < INITIAL_BATCH_COUNT) {
            newBatchCount = INITIAL_BATCH_COUNT;
        } else if (tps > MAX_BATCH_COUNT) {
            newBatchCount = MAX_BATCH_COUNT;
        } else {
            newBatchCount = (int) tps;
        }
        step.set(newBatchCount);
    }
}

CodeSeqService是与数据库交互服务

java 复制代码
public class CodeSeqService {
    private static final int MAX_SEQ = 999999999;

    @Resource
    private CodeSeqMapper codeSeqMapper;

    public List<Integer> selectPrefixByEnv(String env) {
        return codeSeqMapper.selectPrefixByEnv(env, MAX_SEQ);
    }

    @Transactional(timeout = 300, isolation = Isolation.REPEATABLE_READ, rollbackFor = Throwable.class)
    public CodeSeqRet generateCodeByPrefix(String env, Integer prefix, Integer step) {
        log.info("generateCodeByPrefix begin env={},prefix={},step={}", env, prefix, step);
        //加上行锁
        CodeSeq codeSeq = codeSeqMapper.selectCodeSeqByPrefix(env, prefix);
        CodeSeqRet codeSeqRet = new CodeSeqRet();
        codeSeqRet.setResult(0);
        codeSeqRet.setPrefix(prefix);

        if (Objects.isNull(codeSeq) || Objects.isNull(codeSeq.getSequence())) {
            codeSeqRet.setResult(1);
            return codeSeqRet;
        }
        if (codeSeq.getSequence() > MAX_SEQ) {
            codeSeqRet.setResult(1);
            return codeSeqRet;
        }

        if (MAX_SEQ - codeSeq.getSequence() + 1 < step) {
            codeSeqRet.setStart(codeSeq.getSequence());
            codeSeqRet.setEnd(MAX_SEQ);
        } else {
            codeSeqRet.setStart(codeSeq.getSequence());
            codeSeqRet.setEnd(codeSeq.getSequence() + step - 1);
        }
        codeSeq.setSequence(codeSeqRet.getEnd() + 1);
        int ret = codeSeqMapper.updateCodeSeqByPrefix(codeSeq);
        if (ret <= 0) {
            log.info("update error,ret={},codeSeq={}", ret, codeSeq);
            codeSeqRet.setResult(2);
            return codeSeqRet;
        }
        return codeSeqRet;
    }
java 复制代码
public class CodeSeqRet {
    private int prefix;

    /**
     * 0:正常 1:无可用 2:失败
     */
    private Integer result;

    private Integer start;

    private Integer end;
}
java 复制代码
public interface CodeSeqMapper {

    List<Integer> selectPrefixByEnv(@Param("env") String env,@Param("maxSeq") Integer maxSeq);

    CodeSeq selectCodeSeqByPrefix(@Param("env") String env, @Param("prefix") Integer prefix);

    int updateCodeSeqByPrefix(CodeSeq codeSeq);
}
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.toby.dynamic.data.source.db.dao.config.CodeSeqMapper">
    <resultMap id="paramConfig" type="com.toby.dynamic.data.source.db.model.CodeSeq">
        <result column="env" property="env" jdbcType="VARCHAR"/>
        <result column="prefix" property="prefix" jdbcType="INTEGER"/>
        <result column="seq" property="sequence" jdbcType="INTEGER"/>
    </resultMap>
    <select id="selectPrefixByEnv" resultType="java.lang.Integer">
        select
        `prefix`
        from code_seq where `env`=#{env} and `seq` &lt;= #{maxSeq};
    </select>

    <select id="selectCodeSeqByPrefix" resultMap="paramConfig">
        select `env`,`prefix`,`seq` from code_seq where `env` = #{env} and `prefix` = #{prefix} for update;
    </select>
    <update id="updateCodeSeqByPrefix" parameterType="com.toby.dynamic.data.source.db.model.CodeSeq" >
        update code_seq set `seq` = #{sequence} where `env` = #{env} and `prefix` = #{prefix};
    </update>
</mapper>

最终TEST用例调用如下:

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class CodeTest {
    @Autowired
    private CodeSeqService codeSeqService;

    @Test
    public void createCodeTestCase01() {
        log.info("createCodeTestCase01 begin.");
        CodeHandler codeHandler = new CodeHandler("code-create");
        codeHandler.setCodeSeqService(codeSeqService);
        codeHandler.init();
        ExecutorService executorService = Executors.newFixedThreadPool(5, new ThreadFactory() {
            private volatile int index = 0;

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "t" + (index++));
            }
        });
        CountDownLatch countDownLatch = new CountDownLatch(5);
        executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
        executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
        executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
        executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
        executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
//        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            log.error("countDownLatch.await() error", e);
        }
        executorService.shutdown();
    }

    private static class CreateCodeJob implements Runnable {
        private CodeHandler codeHandler;
        private int times;
        private CountDownLatch countDownLatch;

        public CreateCodeJob(CodeHandler codeHandler, int times, CountDownLatch countDownLatch) {
            this.codeHandler = codeHandler;
            this.times = times;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            while (times > 0) {
                String code = codeHandler.getCode();
                log.info("threadName:{},code={}", Thread.currentThread().getName(), code);
                times--;
            }
            countDownLatch.countDown();
        }
    }
}

参考
基于号段模式的分布式 ID 设计

相关推荐
路在脚下@几秒前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一2 分钟前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien4 分钟前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader1 小时前
深入解析 Apache APISIX
java·apache
盛派网络小助手2 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl