0

0

生成多应用实例无间隙序列号指南

聖光之護

聖光之護

发布时间:2025-08-08 13:36:01

|

941人浏览过

|

来源于php中文网

原创

生成多应用实例无间隙序列号指南

本文详细介绍了在多应用实例环境下,如何利用数据库悲观锁和事务机制,实现序列号的无间隙生成。通过引入一个专用的计数器表,并结合JPA的PESSIMISTIC_WRITE锁模式,确保在并发场景下,每个序列号都能唯一且连续地递增,有效避免了因事务回滚或其他并发问题导致的序列号跳跃或重复,适用于需要严格顺序和完整性的业务场景。

1. 问题背景与挑战

在分布式系统或多应用实例环境中,生成具备特定系列(series)且连续递增(number)的设备号是一项常见的需求。例如,设备号可能呈现aa|1、aa|2、aa|3、bb|1等格式,其中每个系列都有其最大允许数量。核心挑战在于:

  1. 无间隙生成: 序列号必须连续,即使在事务回滚或系统崩溃的情况下,也不能出现跳号(如从1直接跳到3)。传统的数据库自增序列或findMax()然后递增的方式,在并发和回滚场景下,往往难以保证无间隙。
  2. 并发安全: 多个应用实例或线程同时请求生成设备号时,必须确保序列号的唯一性和顺序性,避免竞态条件。
  3. 系列管理: 当一个系列的序列号达到上限时,需要能够自动切换到下一个系列并从1开始重新计数。

传统的SELECT MAX(NUMBER)方法在并发环境下存在严重问题。当一个事务查询到最大值并准备插入新记录时,另一个事务可能也同时查询到相同最大值,导致两者都尝试插入下一个相同的序列号,从而引发唯一性冲突或需要复杂的重试机制。即使通过行锁锁定查询到的最大值记录,也可能无法完全避免问题,因为锁定的只是现有记录,而不是“下一个”序列号的生成权。

2. 解决方案:专用计数器表与悲观锁

为了解决上述挑战,一种健壮且可靠的方案是引入一个专门的计数器表,并结合数据库的悲观锁(PESSIMISTIC_WRITE)机制。

2.1 核心思路

  1. 独立计数器表: 创建一个独立的数据库表,例如series_counter,用于存储每个SERIES的当前下一个可用序列号。

    series_counter
    -----------------------
    series_id | current_counter
    -----------------------
    AA        | 1
    BB        | 1
    CC        | 1
    ...

    current_counter字段表示对应series_id下一次将要分配的序列号。

  2. 悲观锁锁定: 当需要为某个SERIES生成序列号时,首先通过悲观写锁(PESSIMISTIC_WRITE)锁定series_counter表中对应series_id的那一行记录。这确保了在当前事务完成之前,其他任何尝试读取或修改该行记录的事务都将被阻塞,直到锁被释放。

  3. 事务原子性: 在同一个数据库事务中,完成以下操作:

    快剪辑
    快剪辑

    国内⼀体化视频⽣产平台

    下载
    • 读取被锁定的current_counter值。
    • 使用该值生成新的设备号记录。
    • 将series_counter表中对应series_id的current_counter值递增1。
    • 保存新的设备号记录。
    • 提交事务。

2.2 实现示例(基于Spring Data JPA和PostgreSQL)

假设我们有以下实体:

  • SeriesCounter:用于存储每个系列的计数器。
  • Device:实际的设备记录,包含series和number。

2.2.1 实体定义

import jakarta.persistence.*;

@Entity
@Table(name = "series_counter")
public class SeriesCounter {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "series_id", unique = true, nullable = false)
    private String seriesId; // 例如 "AA", "BB"

    @Column(name = "current_counter", nullable = false)
    private Long currentCounter; // 当前下一个可用的序列号

    // 构造函数
    public SeriesCounter() {}

    public SeriesCounter(String seriesId, Long currentCounter) {
        this.seriesId = seriesId;
        this.currentCounter = currentCounter;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getSeriesId() { return seriesId; }
    public void setSeriesId(String seriesId) { this.seriesId = seriesId; }
    public Long getCurrentCounter() { return currentCounter; }
    public void setCurrentCounter(Long currentCounter) { this.currentCounter = currentCounter; }

    // 递增计数器的方法
    public void incrementValue() {
        this.currentCounter++;
    }
}

@Entity
@Table(name = "device")
public class Device {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "series", nullable = false)
    private String series;

    @Column(name = "number", nullable = false)
    private Long number;

    // 构造函数
    public Device() {}

    public Device(String series, Long number) {
        this.series = series;
        this.number = number;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getSeries() { return series; }
    public void setSeries(String series) { this.series = series; }
    public Long getNumber() { return number; }
    public void setNumber(Long number) { this.number = number; }
}

2.2.2 Repository 定义

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import jakarta.persistence.LockModeType;
import java.util.Optional;

public interface SeriesCounterRepository extends JpaRepository {

    /**
     * 根据seriesId获取并锁定对应的SeriesCounter记录。
     * 使用PESSIMISTIC_WRITE悲观锁,确保在当前事务中对该行的独占访问。
     *
     * @param seriesId 要锁定的系列ID
     * @return 包含SeriesCounter的Optional对象
     */
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT sc FROM SeriesCounter sc WHERE sc.seriesId = :seriesId")
    Optional findBySeriesIdWithLock(@Param("seriesId") String seriesId);
}

public interface DeviceRepository extends JpaRepository {
    // 基础的CRUD操作
}

2.2.3 服务层实现

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class DeviceNumberGeneratorService {

    private final SeriesCounterRepository seriesCounterRepository;
    private final DeviceRepository deviceRepository;

    public DeviceNumberGeneratorService(SeriesCounterRepository seriesCounterRepository, DeviceRepository deviceRepository) {
        this.seriesCounterRepository = seriesCounterRepository;
        this.deviceRepository = deviceRepository;
    }

    /**
     * 生成一个无间隙的设备序列号。
     * 整个操作在一个事务中完成,并对计数器进行悲观锁定。
     *
     * @param seriesId        要生成序列号的系列ID
     * @param maxNumForSeries 该系列允许的最大序列号(业务逻辑限制)
     * @return 生成的设备对象
     * @throws IllegalStateException 如果当前系列已达到最大数量
     */
    @Transactional // 确保整个方法在一个事务中执行
    public Device generateDeviceNumber(String seriesId, int maxNumForSeries) {
        // 1. 获取并锁定对应系列的计数器
        // 如果series_counter表中没有该seriesId的记录,则需要初始化。
        // 生产环境中,通常会在系统启动或首次使用时预先初始化所有series的计数器。
        // 这里简化处理,如果不存在则抛出异常,或根据实际需求添加初始化逻辑。
        SeriesCounter seriesCounter = seriesCounterRepository.findBySeriesIdWithLock(seriesId)
                .orElseThrow(() -> new IllegalArgumentException("SeriesCounter for seriesId " + seriesId + " not found. Please initialize it."));

        Long currentNumber = seriesCounter.getCurrentCounter();

        // 2. 检查是否达到当前系列的最大允许数量
        if (currentNumber > maxNumForSeries) {
            // 如果当前系列已满,根据业务需求可以抛出异常,
            // 或者实现切换到下一个系列的逻辑(例如,通过查找下一个可用的seriesId并递归调用)。
            throw new IllegalStateException("Series " + seriesId + " has reached its maximum number " + maxNumForSeries + ". Cannot generate more numbers for this series.");
        }

        // 3. 使用当前计数生成设备号
        Device newDevice = new Device();
        newDevice.setSeries(seriesId);
        newDevice.setNumber(currentNumber);
        // ... 设置其他设备属性,例如设备名称、型号等

        // 4. 保存新的设备记录
        deviceRepository.save(newDevice);

        // 5. 递增计数器,为下一个请求准备
        seriesCounter.incrementValue(); // currentCounter++
        seriesCounterRepository.save(seriesCounter); // 更新计数器,将递增后的值持久化

        return newDevice;
    }
}

2.3 机制详解

  • 悲观写锁 (@Lock(LockModeType.PESSIMISTIC_WRITE)): 当findBySeriesIdWithLock方法被调用时,它会在数据库层面为series_counter表中seriesId对应的行加上一个排他锁。这意味着:
    • 其他事务如果尝试读取(SELECT ... FOR UPDATE 或 SELECT ... FOR SHARE)或修改(UPDATE, DELETE)同一行,将会被阻塞,直到持有锁的事务提交或回滚。
    • 这种锁在事务开始时获取,在事务结束(提交或回滚)时释放。
  • 事务 (@Transactional): generateDeviceNumber方法被标记为@Transactional,确保整个操作(获取计数器、生成设备、保存设备、更新计数器)是一个原子单元。
    • 如果其中任何一步失败(例如,deviceRepository.save(newDevice)失败),整个事务都会回滚。
    • 回滚时,series_counter表中current_counter的值将恢复到事务开始前的状态,从而保证不会出现间隙。即使事务失败,序列号也不会被“浪费”掉。
  • 并发处理:
    • 相同系列: 当多个并发请求尝试为同一个seriesId生成设备号时,只有一个请求能成功获取到series_counter表的行锁。其他请求会被阻塞,排队等待。一旦前一个事务完成并释放锁,下一个等待的事务才能获取锁并继续执行。这保证了同一系列序列号的严格顺序和无间隙。
    • 不同系列: 如果并发请求是为不同的seriesId生成设备号,它们会锁定series_counter表中不同的行,因此它们可以并行执行,互不影响,提高了系统的并发能力。

3. 优点与注意事项

3.1 优点

  • 严格无间隙: 即使在并发高、事务回滚频繁的场景下,也能保证序列号的严格无间隙生成。
  • 并发安全: 通过数据库层面的悲观锁,有效解决了多实例并发生成序列号的竞态条件问题。
  • 数据一致性: 事务的原子性确保了设备号生成与计数器更新的同步,避免了数据不一致。

3.2 注意事项与潜在问题

  • 性能瓶颈: 悲观锁会阻塞其他并发事务对同一资源的访问。如果某个seriesId的设备号生成频率极高,可能会导致该seriesId成为性能瓶颈。对于这种极端情况,可能需要考虑更复杂的分布式ID生成方案(如Snowflake算法),但这些方案通常无法保证严格的无间隙性,或需要额外的补偿机制。
  • 死锁风险: 虽然本方案中只锁定了一个资源(series_counter的单行),死锁的风险较低。但在更复杂的业务场景中,如果一个事务需要锁定多个资源,并且这些资源的锁定顺序不一致,则可能发生死锁。良好的事务设计和统一的锁定顺序可以规避此风险。
  • 数据库兼容性: 悲观锁的具体实现和行为可能因数据库类型(如PostgreSQL、MySQL、Oracle)而异。例如,PostgreSQL的FOR UPDATE通常会锁定行,而MySQL的InnoDB引擎在某些隔离级别下可能锁定索引范围。但在JPA的PESSIMISTIC_WRITE抽象下,通常能获得预期的行级锁定行为。
  • 初始化: 确保series_counter表中所有预期的seriesId都有对应的初始计数器记录。在生产环境中,这通常通过数据初始化脚本或管理界面来完成。
  • 系列切换逻辑: 当一个系列的current_counter达到maxNumForSeries时,如何自动切换到下一个系列是一个业务决策。这部分逻辑需要根据实际需求在generateDeviceNumber方法中实现,例如通过查找下一个可用的seriesId并递归调用,或者抛出异常让调用方处理。

4. 总结

通过引入专用的series_counter表并结合Spring Data JPA的@Lock(LockModeType.PESSIMISTIC_WRITE)和@Transactional注解,我们能够构建一个在多应用实例环境下可靠、无间隙的序列号生成系统。该方案利用了数据库事务的原子性和悲观锁的排他性,确保了数据的一致性和并发安全。尽管悲观锁可能引入一定的性能开销,但对于那些对序列号的连续性和完整性有严格要求的业务场景,它提供了一个简洁而强大的解决方案。在实际应用中,应根据具体的并发量和性能需求,权衡其优缺点。

相关专题

更多
mysql修改数据表名
mysql修改数据表名

MySQL修改数据表:1、首先查看数据库中所有的表,代码为:‘SHOW TABLES;’;2、修改表名,代码为:‘ALTER TABLE 旧表名 RENAME [TO] 新表名;’。php中文网还提供MySQL的相关下载、相关课程等内容,供大家免费下载使用。

662

2023.06.20

MySQL创建存储过程
MySQL创建存储过程

存储程序可以分为存储过程和函数,MySQL中创建存储过程和函数使用的语句分别为CREATE PROCEDURE和CREATE FUNCTION。使用CALL语句调用存储过程智能用输出变量返回值。函数可以从语句外调用(通过引用函数名),也能返回标量值。存储过程也可以调用其他存储过程。php中文网还提供MySQL创建存储过程的相关下载、相关课程等内容,供大家免费下载使用。

246

2023.06.21

mongodb和mysql的区别
mongodb和mysql的区别

mongodb和mysql的区别:1、数据模型;2、查询语言;3、扩展性和性能;4、可靠性。本专题为大家提供mongodb和mysql的区别的相关的文章、下载、课程内容,供大家免费下载体验。

281

2023.07.18

mysql密码忘了怎么查看
mysql密码忘了怎么查看

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS 应用软件之一。那么mysql密码忘了怎么办呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

514

2023.07.19

mysql创建数据库
mysql创建数据库

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS 应用软件之一。那么mysql怎么创建数据库呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

253

2023.07.25

mysql默认事务隔离级别
mysql默认事务隔离级别

MySQL是一种广泛使用的关系型数据库管理系统,它支持事务处理。事务是一组数据库操作,它们作为一个逻辑单元被一起执行。为了保证事务的一致性和隔离性,MySQL提供了不同的事务隔离级别。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

386

2023.08.08

sqlserver和mysql区别
sqlserver和mysql区别

SQL Server和MySQL是两种广泛使用的关系型数据库管理系统。它们具有相似的功能和用途,但在某些方面存在一些显著的区别。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

528

2023.08.11

mysql忘记密码
mysql忘记密码

MySQL是一种关系型数据库管理系统,关系数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。那么忘记mysql密码我们该怎么解决呢?php中文网给大家带来了相关的教程以及其他关于mysql的文章,欢迎大家前来学习阅读。

599

2023.08.14

C++ 单元测试与代码质量保障
C++ 单元测试与代码质量保障

本专题系统讲解 C++ 在单元测试与代码质量保障方面的实战方法,包括测试驱动开发理念、Google Test/Google Mock 的使用、测试用例设计、边界条件验证、持续集成中的自动化测试流程,以及常见代码质量问题的发现与修复。通过工程化示例,帮助开发者建立 可测试、可维护、高质量的 C++ 项目体系。

3

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
MySQL 教程
MySQL 教程

共48课时 | 1.8万人学习

MySQL 初学入门(mosh老师)
MySQL 初学入门(mosh老师)

共3课时 | 0.3万人学习

简单聊聊mysql8与网络通信
简单聊聊mysql8与网络通信

共1课时 | 793人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号