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

一、前言

在当前系统开发过程中,单体架构的应用正在走向消亡,越来越多的应用开始分布式架构,在分布式架构设计过程中,有一个问题往往避免不了,就是分布式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) {// 校验sequenceKeyif (!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()) { // 将锁的粒度细化到sequenceKeydbSequenceContent = SEQUENCE_CONTENT_MAP.get(sequenceKey);if (dbSequenceContent == null) { // 双重检查,防止其他线程已经初始化了dbSequenceContentdbSequenceContent = 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) { // 同样防止其他线程更新了dbSequenceContentdbSequenceContent = 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写死在代码中,数据库操作使用原生的JDBCString 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-demodatasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: xxxpassword: xxxurl: jdbc:mysql://xx.8.xx.xx:3306/xxxdb:sequence:enable: truedefaultKey: myDBSeqdailyCutting: truestepLength: 9 #号段为9,一次缓存最多生成9个,超过以后要从数据库中重新获取sequenceLength: 12date-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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate DataSource dataSource;@Autowiredprivate 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,不过针对该方案来说也有很多可以完善的地方,比如号段回收等优化机制,建议用于生产之前还是需要进行严格功能测试和性能测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/4000.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

K8S探针分享

一&#xff0c;探针介绍 1 探针类型 livenessProbe&#xff1a;存活探针&#xff0c;用于判断容器是不是健康&#xff1b;如果探测失败&#xff0c;Kubernetes就会重启容器。 readinessProbe&#xff1a;就绪探针&#xff0c;用于判断是否可以将容器加入到Service负载均衡池…

Access2019直接将数据导入SQL Server数据库中,再直接链接回来

Access2019 的数据表等&#xff0c;除了通过 SSMA 导入数据库外&#xff0c;还可以利用access2019 自身的外部数据导出功能来达到目的。本文将详细介绍这一操作过程。 一、命令行操作阶段 1.以SA这一超级用户登录SQL Server&#xff0c;创建一个数据库&#xff0c;例如“个人…

网络安全培训对软件开发人员的重要性

微信搜索关注&#xff1a;网络研究观 阅读获取更多信息。 组织所经历的持续不断的网络威胁没有任何放缓的迹象&#xff0c;使得实现有效安全的任务变得越来越具有挑战性。 根据最新的 Verizon 数据泄露调查报告&#xff0c;2023 年高级攻击增加了 200% 以上。 IBM 数据泄露成…

C# WinForm —— 09 标签、文本框、按钮控件

标签 Label 一般显示不能编辑的文本或图像 常用属性、事件&#xff1a; 属性用途(Name)标签对象的ID&#xff0c;在代码里引用标签的时候会用到,一般以 lbl 开头Text设置或获取 界面上显示的 文本信息Image显示图像ImageList图像集&#xff0c;通常和 ListView ToolStrip Tre…

【Linux网络编程】数据链路层

数据链路层 1.以太网帧格式2.重谈局域网转发的原理(基于协议)3.认识MTU3.1MTU对IP协议的影响3.2MTU对UDP协议的影响3.3MTU对于TCP协议的影响 4.ARP协议 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励…

【QT学习】9.绘图,三种贴图,贴图的转换,不规则贴图(透明泡泡),简单绘图工具制作

一。绘图的解释 Qt 中提供了强大的 2D 绘图系统&#xff0c;可以使用相同的 API 在屏幕和绘图设备上进行绘制&#xff0c;它主要基于QPainter、QPaintDevice 和 QPaintEngine 这三个类。 QPainter 用于执行绘图操作&#xff0c;其提供的 API 在 GUI 或 QImage、QOpenGLPaintDev…

【深度学习(1)】研0和研1如何上手深度学习及定方向

深度学习&#xff08;1&#xff09; 基础部分书籍鱼书 (理论部分) 视频课程我是土堆&#xff08;代码部分&#xff09; 提升部分李沐的动手学深度学习李沐老师的书 定方向网站&#xff1a; paperwithcode谷歌学术找论文 基础部分 书籍 鱼书 (理论部分) 适合入门&#xff0c;…

11.JAVAEE之网络原理1

1.应用层(和程序员接触最密切) 应用程序 在应用层这里,很多时候, 都是程序员"自定义"应用层协议的,(当然,也是有一些现成的应用层协议)&#xff08;这里的自定义协议,其实是非常简单的~~协议 >约定,程序员在代码中规定好,数据如何进行传输) 1.根据需求, 明确要传…

XTuner微调LLM:1.8B、多模态和Agent-笔记四

本次课程由XTuner 贡献者李剑锋、汪周谦、王群老师讲解【XTuner 微调 LLM&#xff1a;1.8B、多模态和 Agent】课程 课程视频&#xff1a;http:// https://b23.tv/QUhT6ni 课程文档&#xff1a;https://github.com/InternLM/Tutorial/blob/camp2/xtuner/readme.md 两种Finetun…

目标检测的mAP、PR指标含义

基本概念 什么是一个任务的度量标准。对于目标检测任务来说&#xff0c;它的首要目标是确定目标的位置并判别出目标类别。这里已医学图像为例&#xff0c;我们需要计算出血液红细胞&#xff08;RBC&#xff09;、白细胞&#xff08;WBC&#xff09;和血小板的数量。为了实现这一…

【网络安全】HTTP协议 — 基础

专栏文章索引&#xff1a;网络安全 有问题可私聊&#xff1a;QQ&#xff1a;3375119339 目录 学习目标​ 一、万维网的诞生与发展​编辑 1.万维网的诞生与发展 2.HTTP协议诞生与发展 二、网络基础 1.TCP/IP分层传输 1&#xff09;TCP/IP协议 2&#xff09;封装与拆封 …

stm32 hid自定义接收发送程序开发过程记录

cubleMX配置如下 修改端点描述符一次传输的数据大小 根据cubelMX标准在这里修改 编译错误 直接修改&#xff08;因为没有使用nodef &#xff09;编译通过 修改报告描述符&#xff08;默认的描述符无法传输数据&#xff09; 参考&#xff1a;USB协议详解第10讲&#xff08;USB描…

Xinlinx FPGA内的存储器BRAM全解

目录 一、总体概述1.7系列FPGA的BRAM特点2.资源情况 二、BRAM分类1.单端口RAM2.简单双端口RAM3.真双端口RAM 三、BRAM的读写1、Primitives Output Registers读操作注意事项2.三种写数据模式&#xff08;1&#xff09;Write_First&#xff08;2&#xff09;Read_First&#xff0…

JetBrains CLion v2023.3.4 激活版 (C/C++ 集成开发IDE)

前言 JetBrains CLion是一款跨平台的C/C集成开发环境&#xff0c;由JetBrains公司推出。其最新版本支持C14几乎完全&#xff0c;并初步支持C17&#xff0c;使得编写代码更加便捷。CLion还提供了Disassembly view&#xff08;反汇编视图&#xff09;&#xff0c;即使没有源代码…

一、Django 初识

简介 Django 是一个用于构建 Web 应用程序的高级 Python Web 框架。 版本对应 不同版本的django框架是基于特定的不同的python版本开发的&#xff0c;所以不同版本的django框架要正常执行功能只能安装特定的python版本 Django安装 安装 Django # 全局安装 pip install dj…

web自动化系列-selenium的下拉框定位(十三)

在功能操作过程中 &#xff0c;遇到下拉列表是很正常的事 &#xff0c;比如像一些查询条件就都是使用的是下来列表 。所以 &#xff0c;selenium也需要支持对下拉框的操作 。 1.下拉列表 在selenium中&#xff0c;也提供了一个下拉列表操作的类 &#xff1a;Select . 以下为该…

jdk版本冲突,java.lang.UnsupportedClassVersionError: JVMCFRE003

主要是编辑器所用的jdk版本和项目用的不一致导致的&#xff0c;虽然编译通过了&#xff0c;但是运行是会报错 选好后点击Apply点击ok&#xff0c;然后重新编译一遍项目就可以了

万辰集团如何破局“增收不增利”的困境?

在波澜不惊的食用菌加工行业&#xff0c;万辰集团&#xff08;300972.SZ&#xff09;曾是一名平凡的参与者。2021年战略转型的号角吹响&#xff0c;万辰集团挥别了传统业务&#xff0c;转而投身于快速增长的量贩零食市场&#xff0c;并迅速扩张到成为这一领域的重要玩家。万辰的…

Docker常见问题排查思路与实战

Docker作为一种流行的容器化技术&#xff0c;已经在众多场景中得到广泛应用。然而&#xff0c;在使用过程中&#xff0c;我们难免会遇到各种问题。本文将介绍一些常见的Docker问题及其排查思路&#xff0c;并通过实战案例帮助大家更好地理解和应对这些挑战。 1. Docker容器启动…

又重新搭了个个人博客

哈喽大家好&#xff0c;我是咸鱼。 前段时间看到一个学弟写了篇用 Hexo 搭建博客的教程&#xff0c;心中沉寂已久的激情重新被点燃起来。&#xff08;以前搞过一个个人网站&#xff0c;但是因为种种原因最后不了了之&#xff09; 于是花了一天时间参考教程搭了个博客网站&…