如何基于Spring Boot项目从零开始打造一个基于数据库号段模式去中心化的分布式ID生成方案

发布于:2024-04-27 ⋅ 阅读:(33) ⋅ 点赞:(0)

一、前言

在当前系统开发过程中,单体架构的应用正在走向消亡,越来越多的应用开始分布式架构,在分布式架构设计过程中,有一个问题往往避免不了,就是分布式ID生成问题

在常见解决方案中,有使用雪花算法或者自建统一ID生成服务雪花算法是一个很好的分布式ID生成方案,不过雪花算法的递增规律可能看起来不太明显,自建统一ID生成服务面对中小型系统来说又太过于复杂了,那除了这些方法之外还有什么好的解决方法没有呢?

这次我们介绍一个解决方案,基于数据库号段的解决方案

二、技术实现

1. 原理解析

我们本次介绍的基于数据库号段的解决方案方案的原理大体如下:

  1. 数据库中新建一张表,用于记录号段的使用情况,每个序列号的号段信息都有唯一标识用于区分;

  2. 应用第一次获取ID的时候,先根据序号标识从数据库中获取并更新号段信息,将获取的号段信息缓存到应用中,在应用中根据号段信息和指定的ID生成属性生成ID;

  3. 应用后续生成ID时,直接通过缓存在应用内的号段信息生成,如果生成的ID超过号段限制了,再去更新数据库并重新获取号段信息,进行ID生成;

  4. 为了防止号段一直更新导致溢出,增加号段日切方案,即:每次生成的ID可以携带当前日期信息,应用日期发生日切时,数据库号段信息重新置0,简单来说就是新的一天,序列号又从1开始,由于携带了当前日期信息系,所以也不会重复。

示意架构如下:

在这里插入图片描述

生成序列号ID的逻辑嵌入到每个应用中,是去中心化的模式,号段信息维护依赖数据库,更新时依靠数据库的锁机制保障号段的递增性,防止由于号段覆盖产生的序号ID重复,应用内真正生成ID时,会使用Java的锁机制进行应用内的序号生成唯一性保证

2. 编码实现

好了,上面介绍了我们数据库号段模式序列号组件大概原来,下面进行实战阶段吧。

首先,我们需要在数据库中创建一张表,由于记录数据库中的号段信息,表信息不用很复杂,建表语句如下:

CREATE TABLE `db_sequence`
(
    `sequence_key`   varchar(64) NOT NULL COMMENT '序列号key,应用通过不同的key可以获取不同序号信息',
    `start_index`    bigint(20)  COMMENT '号段的起始值',
    `generator_date` datetime  COMMENT '当前序号的生成日期',
    PRIMARY KEY (`sequence_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建好表以后就正式进入编码阶段了。

  1. 新建一个spring boot项目,导入如下依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    
  2. 创建序列号功能的配置文件属性接收类

    为了使我们使用序列号功能更加灵活,我们创建一个属性配置接收实体类:

    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    import java.io.Serializable;
    
    @ConfigurationProperties(prefix = DBSequenceProperties.KEY_PREFIX)
    @Getter
    @Setter
    @ToString
    public class DBSequenceProperties implements Serializable {
    
        public static final String KEY_PREFIX = "db.sequence";
    
        /**
         * 是否启用组件
         */
        private boolean enable;
    
        /**
         * 是否日切,默认日切,即:每日生成的序列号会重置从1开始,同时生成的序列号会默认添加当前应用日志,
         * 如果关闭则一直使用序列号生成,有溢出的风险
         */
        private boolean dailyCutting = true;
    
        /**
         * 从数据库获取序号信息时,默认的key名称
         */
        private String defaultKey = "sys_default";
    
        /**
         * 数据库号段的步长
         */
        private Integer stepLength = 10;
    
        /**
         * 生成的序号长度,长度不够时,默认前面进行补0操作
         */
        private Integer sequenceLength = 16;
    
        /**
         * 序号是否拼接日期字符串
         */
        private boolean dateStr = true;
    }
    

    配置信息比较简单,核心就是号段的大小和生成的序号长度,号段的大小直接关乎序列号生成的性能,毕竟是依赖数据库保存号段信息,如果号段设置过小会导致数据库锁竞争频繁,影响性能,如果设置过大,应用宕机又有序号浪费的问题;同时,一般针对序号的生成为了使用方便都有长度要求,所以我们也要设置合理的序号长度。

  3. 创建序列号功能的缓存信息保存类

    前面已经介绍了,应用获取了号段之后需要缓存到应用中,这样下次获取的时候就不用频繁访问数据库了,我们需要构建一个可以用于缓存序号信息的类。

    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.Date;
    
    /**
     * 数据库序列号信息
     */
    @Getter
    @Setter
    @ToString
    public class DBSequenceContent {
    
        /**
         * 序列号key
         */
        private String sequenceKey;
    
        /**
         * 当前序列号
         */
        private Long currentIndex;
    
        /**
         * 最大序列号
         */
        private Long maxId;
    
        /**
         * 序列号生成时间
         */
        private Date sequenceGeneratorDate;
    
        /**
         * 序列号生成时间字符串
         */
        private String sequenceGeneratorDateStr;
    }
    
  4. 创建序列号功能的生成器

    前面做好准备工作以后,就可以真正准备序列号的生成逻辑了,整个生成逻辑比较简单,注释在代码中已经写了。

    import com.j.sequence.support.DBSequenceProperties;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.util.StringUtils;
    
    import javax.sql.DataSource;
    import java.util.Date;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    
    /**
     * 数据库序号组件生成器
     */
    @Slf4j
    public class DBSequenceGenerator {
    
        /**
         * 缓存序列号信息
         */
        private static final ConcurrentMap<String, DBSequenceContent> SEQUENCE_CONTENT_MAP = new ConcurrentHashMap();
    
        /**
         * 数据源对象
         */
        private DataSource dataSource;
    
        /**
         * 组件配置信息
         */
        private DBSequenceProperties dbSequenceProperties;
    
        public DBSequenceGenerator(DataSource dataSource, DBSequenceProperties dbSequenceProperties) {
            this.dataSource = dataSource;
            this.dbSequenceProperties = dbSequenceProperties;
        }
    
        /**
         * 获取默认key的序号信息
         *
         * @return
         */
        public String getId() {
            return getId(dbSequenceProperties.getDefaultKey());
        }
    
        /**
         * 获取指定sequenceKey的序列号
         *
         * @param sequenceKey
         * @return
         */
        public String getId(String sequenceKey) {
            // 校验sequenceKey
            if (!StringUtils.hasLength(sequenceKey)) {
                throw new IllegalArgumentException("sequenceKey must not be null!");
            }
    
            Date appDate = new Date();
            // 判断当前应用内是否已经缓存了
            DBSequenceContent dbSequenceContent = SEQUENCE_CONTENT_MAP.get(sequenceKey);
            if (dbSequenceContent == null) { // 内存中没有,需要从数据库中加载信息
                synchronized (sequenceKey.intern()) { // 将锁的粒度细化到sequenceKey
                    dbSequenceContent = SEQUENCE_CONTENT_MAP.get(sequenceKey);
                    if (dbSequenceContent == null) { // 双重检查,防止其他线程已经初始化了dbSequenceContent
                        dbSequenceContent = DBSequenceDBHandler.loadSequenceContent(dataSource, sequenceKey, dbSequenceProperties, appDate);
                        updateSequenceContentMap(dbSequenceContent, sequenceKey);
                    }
                }
            }
    
            if (dbSequenceProperties.isDailyCutting()) { // 开启了日切模式
                if (DBSequenceDBHandler.compareDate(appDate, dbSequenceContent.getSequenceGeneratorDate()) > 0) { // 当前应用时间大于了序列号变动时间了
                    synchronized (sequenceKey.intern()) {
                        dbSequenceContent = SEQUENCE_CONTENT_MAP.get(sequenceKey);
                        if (DBSequenceDBHandler.compareDate(appDate, dbSequenceContent.getSequenceGeneratorDate()) > 0) { // 同样防止其他线程更新了dbSequenceContent
                            dbSequenceContent = DBSequenceDBHandler.reloadSequenceContent(dataSource, sequenceKey, dbSequenceProperties, appDate);
                            updateSequenceContentMap(dbSequenceContent, sequenceKey);
                        }
                    }
                }
            }
    
            return doGeneratorSequence(dataSource, sequenceKey, dbSequenceProperties, appDate);
    
        }
    
    
        /**
         * 生成序列号
         *
         * @param dataSource
         * @param sequenceKey
         * @param appDate
         * @return
         */
        private String doGeneratorSequence(DataSource dataSource, String sequenceKey, DBSequenceProperties dbSequenceProperties, Date appDate) {
            long tempId;
            String dateStr;
            synchronized (sequenceKey.intern()) {
                DBSequenceContent dbSequenceContent = SEQUENCE_CONTENT_MAP.get(sequenceKey);
                long sequence = dbSequenceContent.getCurrentIndex() + 1;
                if (sequence > dbSequenceContent.getMaxId()) { // 超过了最大值,重新从数据库中获取号段信息
                    dbSequenceContent = DBSequenceDBHandler.reloadSequenceContent(dataSource, sequenceKey, dbSequenceProperties, appDate);
                    updateSequenceContentMap(dbSequenceContent, sequenceKey);
                    sequence = dbSequenceContent.getCurrentIndex() + 1;
                }
                dbSequenceContent.setCurrentIndex(sequence);
    
                tempId = sequence;
                dateStr = dbSequenceContent.getSequenceGeneratorDateStr();
            }
            String idStr = String.valueOf(tempId);
            int sequenceLength = dbSequenceProperties.getSequenceLength();
            int idLength = idStr.length();
            StringBuilder idSb = new StringBuilder();
            if (dbSequenceProperties.isDateStr()) {
                idSb.append(dateStr);
                idLength += idSb.length();
            }
    
            if (sequenceLength >= idLength) { // 位数不够需要进行补0操作
                int length = sequenceLength - idLength;
                for (int i = 0; i < length; i++) {
                    idSb.append("0");
                }
            } else {
                throw new IllegalArgumentException("idLength more than sequenceLength limit!");
            }
            idSb.append(tempId);
    
            return idSb.toString();
        }
    
    
        /**
         * 更新dbSequenceContent属性
         *
         * @param dbSequenceContent
         * @param sequenceKey
         */
        private void updateSequenceContentMap(DBSequenceContent dbSequenceContent, String sequenceKey) {
            if (dbSequenceContent == null || dbSequenceContent.getCurrentIndex() == null) {
                SEQUENCE_CONTENT_MAP.remove(sequenceKey); // 移除缓存中的信息,方便下次从数据库中获取
                throw new RuntimeException(String.format("get %s info error, please check db!", sequenceKey));
            }
            SEQUENCE_CONTENT_MAP.put(sequenceKey, dbSequenceContent);
        }
    
        /**
         * 清理缓存中的sequenceKey信息,清理以后,下次获取会重新从数据库中查询
         *
         * @param sequenceKeys
         * @return
         */
        public boolean clearCacheSequence(String... sequenceKeys) {
            if (sequenceKeys == null || sequenceKeys.length == 0) {
                synchronized (this) {
                    SEQUENCE_CONTENT_MAP.clear();
                }
            } else {
                for (int i = 0; i < sequenceKeys.length; i++) {
                    String key = sequenceKeys[i];
                    synchronized (key.intern()) {
                        SEQUENCE_CONTENT_MAP.remove(key);
                    }
                }
            }
            return true;
        }
    
    }
    
  5. 实现序列号功能的数据库操作逻辑

    DBSequenceGenerator类中的逻辑主要专注于ID生成的整个逻辑流转,涉及真正的数据库操作,我们可以放到另一个类中,这样核心代码看起来会简洁一些:

    import com.j.sequence.support.DBSequenceProperties;
    import lombok.extern.slf4j.Slf4j;
    
    import javax.sql.DataSource;
    import java.sql.*;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * 数据库序号组件数据库操作处理器
     */
    @Slf4j
    public class DBSequenceDBHandler {
    
        /**
         * 加载数据库中序列号信息,没有的话则保存
         *
         * @param dataSource
         * @param sequenceKey
         * @param dbSequenceProperties
         * @param appDate
         * @return
         */
        public static DBSequenceContent loadSequenceContent(DataSource dataSource, String sequenceKey, DBSequenceProperties dbSequenceProperties, Date appDate) {
            DBSequenceContent dbSequenceContent;
            Connection connection = null;
            Boolean autoCommit = null;
            try {
                connection = dataSource.getConnection();
                // 都是简单操作SQL,为了适配不同ORM框架,只需要注入DataSource对象就行,所以SQL写死在代码中,数据库操作使用原生的JDBC
                String sql = "SELECT start_index, generator_date FROM db_sequence where sequence_key = ? ";
                PreparedStatement ps = connection.prepareStatement(sql);
                ps.setString(1, sequenceKey);
                ResultSet rs = ps.executeQuery();
    
                autoCommit = connection.getAutoCommit();
                connection.setAutoCommit(false);
    
                if (rs != null && rs.next()) { // 数据库中已经存在该条记录
                    dbSequenceContent = updateDBSequenceContent(connection, sequenceKey, dbSequenceProperties, appDate);
                } else { // 数据库中不存在数据需要新增
                    sql = "INSERT INTO db_sequence (sequence_key, start_index, generator_date) VALUES(?, ?, ?)";
                    PreparedStatement psSave = connection.prepareStatement(sql);
                    psSave.setString(1, sequenceKey);
                    psSave.setInt(2, dbSequenceProperties.getStepLength());
                    psSave.setTimestamp(3, new Timestamp(appDate.getTime()));
                    psSave.executeUpdate();
                    psSave.close();
    
                    dbSequenceContent = new DBSequenceContent();
                    dbSequenceContent.setSequenceKey(sequenceKey);
                    dbSequenceContent.setSequenceGeneratorDate(appDate);
                    dbSequenceContent.setSequenceGeneratorDateStr(new SimpleDateFormat("yyyyMMdd").format(appDate));
                    dbSequenceContent.setCurrentIndex(0L);
                    dbSequenceContent.setMaxId(dbSequenceProperties.getStepLength() * 1L);
                }
                rs.close();
                ps.close();
                connection.commit();
            } catch (SQLException sqlException) {
                if (connection != null) {
                    try {
                        connection.rollback();
                    } catch (SQLException se) {
                        log.error("connection rollback error!", se);
                    }
                }
                log.error("add sequenceKey: {} error!", sequenceKey, sqlException);
                // 可能是其他应用已经save过了,此时插入报主键冲突了,所以重试一下
                log.info("retry get dbSequenceContent by reloadSequenceContentByDailyCutting start!");
                dbSequenceContent = reloadSequenceContent(dataSource, sequenceKey, dbSequenceProperties, appDate);
                if (dbSequenceContent != null && dbSequenceContent.getCurrentIndex() != null) {
                    log.info("retry get dbSequenceContent by reloadSequenceContentByDailyCutting successes!");
                } else {
                    log.error("retry get dbSequenceContent by reloadSequenceContentByDailyCutting error!");
                }
            } finally {
                closeConnection(connection, autoCommit);
            }
    
            return dbSequenceContent;
        }
    
        private static DBSequenceContent updateDBSequenceContent(Connection connection, String sequenceKey, DBSequenceProperties dbSequenceProperties, Date appDate) throws SQLException {
            String sql = "SELECT start_index, generator_date FROM db_sequence where sequence_key = ? for update "; // 存在该条记录再进行上锁
            PreparedStatement psLock = connection.prepareStatement(sql);
            psLock.setString(1, sequenceKey);
            ResultSet rsLock = psLock.executeQuery();
    
            DBSequenceContent dbSequenceContent = new DBSequenceContent();
    
            if (rsLock.next()) {
                long startIndex = rsLock.getLong("start_index");
                Date generatorDate = rsLock.getDate("generator_date");
    
                dbSequenceContent.setSequenceKey(sequenceKey);
                if (dbSequenceProperties.isDailyCutting() && compareDate(generatorDate, appDate) < 0) { //如果序列号需要日切
                    // 数据库中日期晚于应用日期,需要进行日切操作
                    sql = "update db_sequence set start_index=?, generator_date=? where sequence_key = ? ";
                    final PreparedStatement psUpdateSIDate = connection.prepareStatement(sql);
                    psUpdateSIDate.setInt(1, dbSequenceProperties.getStepLength());
                    psUpdateSIDate.setTimestamp(2, new Timestamp(appDate.getTime()));
                    psUpdateSIDate.setString(3, sequenceKey);
                    psUpdateSIDate.executeUpdate();
                    psUpdateSIDate.close();
    
                    dbSequenceContent.setSequenceGeneratorDate(appDate);
                    dbSequenceContent.setSequenceGeneratorDateStr(new SimpleDateFormat("yyyyMMdd").format(appDate));
                    dbSequenceContent.setCurrentIndex(0L);
                    dbSequenceContent.setMaxId(dbSequenceProperties.getStepLength() * 1L);
    
                } else {
                    sql = "update db_sequence set start_index=start_index+? where sequence_key = ? ";
                    final PreparedStatement psUpdateSI = connection.prepareStatement(sql);
                    psUpdateSI.setInt(1, dbSequenceProperties.getStepLength());
                    psUpdateSI.setString(2, sequenceKey);
                    psUpdateSI.executeUpdate();
                    psUpdateSI.close();
    
                    dbSequenceContent.setSequenceGeneratorDate(generatorDate);
                    dbSequenceContent.setSequenceGeneratorDateStr(new SimpleDateFormat("yyyyMMdd").format(generatorDate));
                    dbSequenceContent.setCurrentIndex(startIndex);
                    dbSequenceContent.setMaxId(startIndex + dbSequenceProperties.getStepLength());
                }
            } else {
                log.error("sequenceKey: {} record maybe delete, please check db!", sequenceKey);
            }
            rsLock.close();
            psLock.close();
    
            return dbSequenceContent;
        }
    
        /**
         * 更新数据库号段信息
         *
         * @param dataSource
         * @param sequenceKey
         * @param dbSequenceProperties
         * @param appDate
         * @return
         */
        public static DBSequenceContent reloadSequenceContent(DataSource dataSource, String sequenceKey, DBSequenceProperties dbSequenceProperties, Date appDate) {
            DBSequenceContent dbSequenceContent = null;
            Connection connection = null;
            Boolean autoCommit = null;
            try {
                connection = dataSource.getConnection();
                autoCommit = connection.getAutoCommit();
                connection.setAutoCommit(false);
                dbSequenceContent = updateDBSequenceContent(connection, sequenceKey, dbSequenceProperties, appDate);
                connection.commit();
            } catch (SQLException sqlException) {
                dbSequenceContent = null;
                log.error("reloadSequenceContentByDailyCutting error!", sqlException);
            } finally {
                closeConnection(connection, autoCommit);
            }
            return dbSequenceContent;
        }
    
        /**
         * 比较日期,只比较年月日
         *
         * @param date0
         * @param date1
         * @return
         */
        public static int compareDate(Date date0, Date date1) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
            int date0Int = Integer.parseInt(simpleDateFormat.format(date0));
            int date1Int = Integer.parseInt(simpleDateFormat.format(date1));
            return date0Int > date1Int ? 1 : (date0Int < date1Int ? -1 : 0);
        }
    
        /**
         * 关闭connection资源
         *
         * @param connection
         * @param autoCommit
         */
        private static void closeConnection(Connection connection, Boolean autoCommit) {
            if (connection != null) {
                if (autoCommit != null) {
                    try {
                        connection.setAutoCommit(autoCommit);
                    } catch (SQLException sqlException) {
                        log.error("connection set autoCommit error!", sqlException);
                    }
                }
                try {
                    connection.close();
                } catch (SQLException sqlException) {
                    log.error("connection close error!", sqlException);
                }
            }
        }
    }
    
  6. 创建配置类进行功能加载

    在上面核心功能编码实现以后,为了适配spring boot项目,我们可以准备一个Configuration进行配置加载操作,简化功能使用。

    import com.j.sequence.core.DBSequenceGenerator;
    import com.j.sequence.support.DBSequenceProperties;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * 数据库序列号组件配置类
     */
    @Configuration
    @EnableConfigurationProperties(DBSequenceProperties.class)
    /**
     * 条件加载,需要显示启用设置 db.sequence.enable=true
     */
    @ConditionalOnProperty(prefix = DBSequenceProperties.KEY_PREFIX, name = "enable", matchIfMissing = true)
    public class DBSequenceConfiguration {
    
        @Bean("com.j.sequence.boot.DBSequenceConfiguration.dbSequenceGenerator")
        public DBSequenceGenerator dbSequenceGenerator(DataSource dataSource, DBSequenceProperties dbSequenceProperties) {
            return new DBSequenceGenerator(dataSource, dbSequenceProperties);
        }
    
    }
    

3. 编码总结

编码实现阶段到此就结束了,代码的核心的逻辑都在注释中描述了,这里我们简单总结一个核心编码逻辑。

  1. 获取ID时,优先从缓存中获取缓存的号段信息;
  2. 如果号段信息不存在则需要在数据库中新增sequenceKey对应信息号段信息,为了防止其他应用进行了新增,防止主键冲突,程序会先进行是否存在的判断,如果存在则会使用for update关键字进行行锁,然后进行数据更新,缓存更新操作;否则才会添加,同样为了防止其他应用抢先进行了新增,在新增失败以后,会进行一次直接获取的重试操作,如果这次操作也失败,才会返回空的缓存信息,结束ID获取;
  3. 经历步骤2以后,程序再往下运行,号段信息就一定存在了,此时判断是否发生了日切,如果需要日切则将数据库中的序列号信息重置;
  4. 经历步骤3以后,应用中的号段缓存信息此时已经可以用于最后的ID生成了,如果ID位数不够就进行补0操作,最后ID生成格式为:年年年年月月日日[n个0]递增的序号 (n可以为0)。

三、功能测试

application.yaml配置文件中添加配置:

spring:
  application:
    name: db-sequence-demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: xxx
    password: xxx
    url: jdbc:mysql://xx.8.xx.xx:3306/xxx

db:
  sequence:
    enable: true
    defaultKey: myDBSeq
    dailyCutting: true
    stepLength: 9 #号段为9,一次缓存最多生成9个,超过以后要从数据库中重新获取
    sequenceLength: 12
    date-str: true

在编码完成以后我们需要进行功能,为了方便我们直接在应用中编写测试代码,启动工程进行测试。

1. 简单测试

简单测试,我们主要测试生成的序列号是否正确并且连续。

  • 测试代码
import com.j.sequence.core.DBSequenceGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SeqController {

    @Autowired
    private DBSequenceGenerator dbSequenceGenerator;

    @RequestMapping("/getId")
    public List<String> getId() {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(dbSequenceGenerator.getId());
        }
        return list;
    }
}
  • 测试请求

    在这里插入图片描述

可以看到,序列号正常生成了,同时设置的序号号段为9,自动更新获取为10也没有发生任何问题,测试通过

2. 多线程测试

多线程测试,主要是模拟多个线程并发请求获取ID的时候,ID是否可以正常生成并获取。

  • 测试代码

    import com.j.sequence.core.DBSequenceGenerator;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
    
    @RestController
    public class SeqController {
    
        @Autowired
        private DBSequenceGenerator dbSequenceGenerator;
    
        private Set<String> ids = Collections.synchronizedSet(new HashSet<>());
    
        @RequestMapping("getIdByMultiThread")
        public String getIdByMultiThread() {
            int threadNum = 5; // 线程数量
            int idNum = 10;//每个线程获取ID数量
            CountDownLatch countDownLatch = new CountDownLatch(threadNum);
    
            for (int i = 0; i < threadNum; i++) {
                new Thread(() -> {
                    try {
                        Thread.sleep(200L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                    for (int i1 = 0; i1 < idNum; i1++) {
                        ids.add(dbSequenceGenerator.getId());
                    }
                }).start();
            }
            try {
                TimeUnit.SECONDS.sleep(2L); // 暂停等待线程执行完成,本地测试2s够了,如果不够可自行调整
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return ids.size() == threadNum * idNum ? "生成ID数量符合预期:" + ids.size() : "生成ID重复导致集合数量错误:" + ids.size();
        }
    
    }
    
  • 测试请求

    在这里插入图片描述

本次测试案例中,我们主要使用Set集合测试多线程情况下ID生成的正确性,我们使用5个线程,每个线程生成10个序号的方式进行测试,预期会生成50个序号,最后测试结果符合预期,测试通过

3. 多实例测试

多实例测试时,我们打算使用5个实例进行测试,为了测试简单,并不会真正部署5个实例节点,为了方便,修改一下DBSequenceGenerator类,去掉static修饰符,使成员变量都是类级别的,如下:

/**
 * 数据库序号组件生成器
 */
@Slf4j
public class DBSequenceGenerator {

    /**
     * 缓存序列号信息
     */
    private /*static*/ final ConcurrentMap<String, DBSequenceContent> SEQUENCE_CONTENT_MAP = new ConcurrentHashMap();

   // ........................其他代码不变
  • 测试代码

    import com.j.sequence.core.DBSequenceGenerator;
    import com.j.sequence.support.DBSequenceProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.sql.DataSource;
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
    
    @RestController
    public class SeqController {
    
        @Autowired
        private DataSource dataSource;
    
        @Autowired
        private DBSequenceProperties dbSequenceProperties;
    
        private Set<String> ids = Collections.synchronizedSet(new HashSet<>());
    
        @RequestMapping("getIdByMultiInstance")
        public String getIdByMultiInstance() {
            int threadNum = 5; // 线程数量
            int idNum = 10;//每个线程获取ID数量
            CountDownLatch countDownLatch = new CountDownLatch(threadNum);
    
            for (int i = 0; i < threadNum; i++) {
                new Thread(() -> {
                    // 构建多个DBSequenceGenerator,模拟多个实例
                    DBSequenceGenerator dbSequenceGenerator = new DBSequenceGenerator(dataSource, dbSequenceProperties);
                    try {
                        Thread.sleep(200L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                    for (int i1 = 0; i1 < idNum; i1++) {
                        ids.add(dbSequenceGenerator.getId());
                    }
                }).start();
            }
            try {
                TimeUnit.SECONDS.sleep(2L); // 暂停等待线程执行完成,本地测试2s够了,如果不够可自行调整
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return ids.size() == threadNum * idNum ? "生成ID数量符合预期:" + ids.size() : "生成ID重复导致集合数量错误:" + ids.size();
        }
    
    }
    
  • 测试请求

    在这里插入图片描述

本次测试过程中,模拟多实例请求,因为DBSequenceGenerator对象是通过注入spring容器方式提供的,用户在一个实例中使用的时候,只需要通过spring提供的依赖注入就行,所以多实例测试,我们模拟使用5个线程,每个线程都单独创建DBSequenceGenerator对象去获取10个序号,预期可以获取到50个序号,最后测试结果也符合我们预期,测试通过

4. 数据库信息核对

按照我们的测试流程,每次测试都会重新重启,我们可以计算一下数据库最终的号段偏移量:

  • 简单测试:10/9=110%9=1<9,偏移:1*9 + 1*9=18

  • 多线程测试:50/9=550%9=5<9,偏移:5*9+1*9=54

  • 多实例测试:参考简单测试计算方法:18*5=90

最终:18+54+90=162

查看数据库记录:

在这里插入图片描述

通过数据库记录可以确定,号段变化符合我们预期,测试通过

四、写在最后

通过上面的编码我们实行了一个基于数据库号段去中心化的分布式ID生成方案,该组件生成的序列号可以保证有序递增,且递增规律比较明显,不过由于号段信息存储在数据库中,多个实例去获取时,只能保证每次获取号段以后,单个实例里面生成的序号是递增的,但是不能保证单个实例里面的序号是连续的,这个需要注意。

一般情况下应用数据库还是很稳定的,合理的设置号段也可以避免数据库的压力,可以把改功能封装成一个可以复用的SDK,不过针对该方案来说也有很多可以完善的地方,比如号段回收等优化机制,建议用于生产之前还是需要进行严格功能测试和性能测试。


网站公告

今日签到

点亮在社区的每一天
去签到