高并发系统-分布式唯一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 设计

相关推荐
喜欢便码2 小时前
JS小练习0.1——弹出姓名
java·前端·javascript
Asthenia04123 小时前
为什么说MVCC无法彻底解决幻读的问题?
后端
Asthenia04123 小时前
面试官问我:三级缓存可以解决循环依赖的问题,那两级缓存可以解决Spring的循环依赖问题么?是不是无法解决代理对象的问题?
后端
Asthenia04123 小时前
面试复盘:使用 perf top 和火焰图分析程序 CPU 占用率过高
后端
Asthenia04123 小时前
面试复盘:varchar vs char 以及 InnoDB 表大小的性能分析
后端
Asthenia04123 小时前
面试问题解析:InnoDB中NULL值是如何记录和存储的?
后端
王磊鑫3 小时前
重返JAVA之路-初识JAVA
java·开发语言
半兽先生3 小时前
WebRtc 视频流卡顿黑屏解决方案
java·前端·webrtc
Asthenia04123 小时前
面试官问我:TCP发送到IP存在但端口不存在的报文会发生什么?
后端
Asthenia04123 小时前
HTTP 相比 TCP 的好处是什么?
后端