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

相关推荐
2401_854391081 分钟前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
Cikiss10 分钟前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
wxin_VXbishe10 分钟前
springboot合肥师范学院实习实训管理系统-计算机毕业设计源码31290
java·spring boot·python·spring·servlet·django·php
Cikiss11 分钟前
微服务实战——平台属性
java·数据库·后端·微服务
无敌の星仔20 分钟前
一个月学会Java 第2天 认识类与对象
java·开发语言
OEC小胖胖25 分钟前
Spring Boot + MyBatis 项目中常用注解详解(万字长篇解读)
java·spring boot·后端·spring·mybatis·web
2401_857617621 小时前
SpringBoot校园资料平台:开发与部署指南
java·spring boot·后端
quokka561 小时前
Springboot 整合 logback 日志框架
java·spring boot·logback
计算机学姐1 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
救救孩子把1 小时前
深入理解 Java 对象的内存布局
java