0

0

生成多应用实例无间隙序列号的策略与实现

花韻仙語

花韻仙語

发布时间:2025-08-08 11:32:32

|

795人浏览过

|

来源于php中文网

原创

生成多应用实例无间隙序列号的策略与实现

本文探讨了在多应用实例环境下生成无间隙序列号的挑战与解决方案。通过引入一个独立的计数器表,并结合悲观写锁机制,确保在并发操作和事务回滚场景下,序列号能够严格递增且不产生任何跳跃或重复。文章提供了基于Java/JPA的实现示例,并详细解释了其工作原理和关键注意事项。

在分布式系统或多应用实例的场景中,生成严格递增且不含间隙的序列号是一项常见的需求。例如,设备编号、订单号等业务场景,可能要求序列号在任何情况下都不能出现跳跃(即使事务回滚)或重复。传统的数据库自增id(如postgresql的serial或sequence)虽然能保证唯一性,但在事务回滚时可能会产生间隙,这不符合某些业务的严格要求。直接通过查询最大值(findmax())然后递增的方式,在并发环境下极易出现竞态条件,导致序列号重复或产生间隙,且锁定整个数据表或范围的开销巨大。

解决方案:基于独立计数器表的悲观锁机制

为了解决上述挑战,我们引入一个专门用于维护序列号当前值的独立计数器表,并结合数据库的悲观写锁(PESSIMISTIC_WRITE)机制。这种方法的核心思想是:为每个需要生成序列号的“系列”(例如,不同的设备系列或产品类别)维护一个独立的计数器,并在获取和更新该计数器时施加排他锁,确保操作的原子性和隔离性。

1. 计数器表设计

首先,创建一个名为 series_counter 的独立表,用于存储每个系列当前的序列号值。

CREATE TABLE series_counter (
    series_id VARCHAR(50) PRIMARY KEY, -- 系列标识符,例如 'AA', 'BB'
    current_counter BIGINT NOT NULL    -- 当前序列号值
);

-- 示例数据
INSERT INTO series_counter (series_id, current_counter) VALUES ('AA', 0);
INSERT INTO series_counter (series_id, current_counter) VALUES ('BB', 0);
-- ... 为每个系列初始化计数器

series_id 用于唯一标识不同的序列系列,而 current_counter 则保存了该系列下一个可用的序列号。每次需要生成新序列号时,我们都会从这个表中获取 current_counter,使用它,然后将其递增。

2. 核心业务逻辑实现(Java/JPA示例)

在Java应用中,我们可以使用JPA(Java Persistence API)结合Spring Data JPA来实现这一机制。

2.1 SeriesCounter 实体类

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

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

    @Id
    private String seriesId; // 对应数据库的 series_id

    private Long currentCounter; // 对应数据库的 current_counter

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

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

    // Getter 和 Setter 方法
    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++;
    }
}

2.2 SeriesCounterRepo 接口

这是一个Spring Data JPA仓库接口,用于访问 series_counter 表。关键在于 fetchLatest 方法上的 @Lock(LockModeType.PESSIMISTIC_WRITE) 注解。

Type
Type

生成草稿,转换文本,获得写作帮助-等等。

下载
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 org.springframework.stereotype.Repository;
import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional; // 注意:这里是jakarta.transaction.Transactional

@Repository
public interface SeriesCounterRepo extends JpaRepository {

    /**
     * 获取指定系列的最新的计数器值,并施加悲观写锁。
     * 该方法必须在一个事务中执行。
     * @param seriesId 系列ID
     * @return SeriesCounter 对象
     */
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT sc FROM SeriesCounter sc WHERE sc.seriesId = :seriesId")
    // 尽管外部方法会有@Transactional,但为了确保在获取锁时就处于事务中,
    // 某些情况下此处也需要@Transactional,具体取决于JPA提供商的行为。
    @Transactional 
    SeriesCounter fetchLatest(@Param("seriesId") String seriesId);
}

2.3 业务逻辑服务类

import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional; // 注意:这里是jakarta.transaction.Transactional

@Service
public class DeviceNumberGeneratorService {

    private final SeriesCounterRepo seriesCounterRepo;
    private final SeriesRepository seriesRepository; // 假设有一个用于保存最终序列号的Repository

    public DeviceNumberGeneratorService(SeriesCounterRepo seriesCounterRepo, SeriesRepository seriesRepository) {
        this.seriesCounterRepo = seriesCounterRepo;
        this.seriesRepository = seriesRepository;
    }

    /**
     * 生成并分配设备编号的核心业务逻辑。
     * 整个方法必须在一个事务中执行,以确保原子性。
     * @param seriesId 要生成编号的系列ID
     * @return 生成的完整设备编号(例如:AA-1, BB-2)
     */
    @Transactional
    public String generateDeviceNumber(String seriesId) {
        // 1. 获取并锁定计数器
        // 这一步会从数据库中获取指定 seriesId 的 SeriesCounter 记录,并对其施加悲观写锁。
        // 其他并发请求尝试获取同一 seriesId 的锁时,将会被阻塞,直到当前事务完成。
        SeriesCounter latestCounter = seriesCounterRepo.fetchLatest(seriesId);

        // 2. 获取当前可用的序列号
        Long currentNumber = latestCounter.getCurrentCounter();

        // 3. 构建完整的设备编号
        String deviceNumber = seriesId + "-" + (currentNumber + 1); // 假设从1开始,所以先+1

        // 4. 创建并保存新的设备记录
        // 假设 Series 是你的业务实体,用于存储生成的设备信息
        Series newDevice = new Series(); // 你的设备实体类
        newDevice.setSeries(seriesId);
        newDevice.setNumber(currentNumber + 1); // 存储当前使用的序列号
        seriesRepository.save(newDevice);

        // 5. 递增计数器并保存
        // 在内存中递增计数器的值
        latestCounter.incrementValue();
        // 将递增后的值保存回数据库。
        // 因为 latestCounter 是在当前事务中被管理的JPA实体,
        // 它的状态改变会在事务提交时自动同步到数据库。
        // seriesCounterRepo.save(latestCounter); // 显式保存通常不是必需的,JPA会自动脏检查并更新

        return deviceNumber;
    }
}

// 假设的 Series 实体和 Repository
// public class Series {
//     private String series;
//     private Long number;
//     // Getters, Setters
// }
// public interface SeriesRepository extends JpaRepository {}

3. 工作原理与机制解释

  1. 悲观写锁 (PESSIMISTIC_WRITE): 当 generateDeviceNumber 方法被调用时,seriesCounterRepo.fetchLatest(seriesId) 会执行一个数据库查询,并在返回 SeriesCounter 记录的同时,对该记录施加一个排他写锁。这意味着:

    • 在当前事务提交或回滚之前,其他任何尝试获取同一 seriesId 记录写锁的事务都会被阻塞,直到锁被释放。
    • 其他事务也无法读取(取决于数据库隔离级别,但在大多数情况下,悲观写锁会阻止脏读、不可重复读)。
    • 这保证了在任何给定时刻,只有一个事务能够“看到”并操作特定 seriesId 的 current_counter 值。
  2. 事务原子性: generateDeviceNumber 方法被 @Transactional 注解修饰。这意味着整个操作(获取计数器、使用计数器生成新记录、递增计数器)被封装在一个数据库事务中。

    • 如果事务成功完成,series_counter 表中的 current_counter 会被更新,新生成的设备记录也会被持久化。
    • 如果事务在任何步骤中失败(例如,网络中断、数据库错误、业务逻辑异常),整个事务会回滚。由于 series_counter 的更新也是事务的一部分,回滚会撤销对 current_counter 的任何修改,确保不会产生“跳跃”的序列号。例如,如果 current_counter 被读取为 X,但在保存新设备时失败,那么 current_counter 不会变成 X+1,下次尝试时仍会从 X 开始。
  3. 避免 findMax() 的问题: 相比于每次都查询业务表(SERIES 表)的最大 NUMBER 值,这种方案的优势在于:

    • 它锁定的是一个非常小的、专门用于计数的记录,而不是整个业务表或其索引。这大大降低了锁的粒度,减少了资源争用。
    • findMax() 方式需要更复杂的锁定策略来确保没有间隙,例如锁定整个表或使用范围锁,这通常效率低下且难以正确实现。而锁定一个独立的计数器记录则简单高效。

4. 注意事项与考量

  • 性能影响: 悲观锁虽然能保证严格的无间隙序列,但它会引入串行化操作,在高并发场景下可能成为性能瓶颈。如果序列号的生成频率非常高,需要评估这种方案的吞吐量是否满足需求。对于极高并发,可以考虑批量获取序列号(例如,一次性获取100个,然后在应用内存中分配),但这会引入间隙的风险,需要仔细权衡。
  • 事务隔离级别: 确保数据库的事务隔离级别能够支持悲观锁的预期行为(通常是 READ COMMITTED 或 REPEATABLE READ 即可,因为锁会强制串行化)。
  • 错误处理: 确保业务逻辑中的异常处理能够正确触发事务回滚,从而避免计数器被错误地递增。
  • 初始化: 确保 series_counter 表在系统启动或新系列首次使用前,有正确的初始值(例如,0或1)。
  • 数据库支持: 大多数关系型数据库(如PostgreSQL、MySQL、Oracle等)都支持悲观锁。

总结

通过引入独立的计数器表并结合悲观写锁,我们能够可靠地在多应用实例环境下生成严格无间隙的序列号。这种方案通过在事务层面保证计数器操作的原子性和隔离性,有效地解决了并发和事务回滚带来的序列号间隙问题。虽然它可能引入一定的性能开销,但对于那些对序列号连续性有严格要求的业务场景,这是一种健壮且易于理解和实现的策略。在实际应用中,务必根据业务的具体需求和并发量来权衡和选择最合适的序列号生成方案。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

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

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

668

2023.06.20

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

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

247

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中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

516

2023.07.19

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

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

256

2023.07.25

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

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

387

2023.08.08

sqlserver和mysql区别
sqlserver和mysql区别

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

533

2023.08.11

mysql忘记密码
mysql忘记密码

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

602

2023.08.14

java入门学习合集
java入门学习合集

本专题整合了java入门学习指南、初学者项目实战、入门到精通等等内容,阅读专题下面的文章了解更多详细学习方法。

1

2026.01.29

热门下载

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

精品课程

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

共48课时 | 2万人学习

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

共3课时 | 0.3万人学习

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

共1课时 | 812人学习

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

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