多应用无间隙序列号生成方法
在多应用实例环境下,如何确保序列号无间隙生成?本文提供了一份详尽的指南,着重介绍了利用数据库悲观锁和事务机制,构建一个可靠的序列号生成系统。通过创建一个专用的计数器表,并结合JPA的PESSIMISTIC_WRITE锁模式,有效避免了并发场景下的序列号跳跃或重复问题,确保每个序列号的唯一性和连续递增。该方案适用于对序列号顺序和完整性有严格要求的业务场景,例如设备号生成。本文还深入探讨了实现细节,包括实体定义、Repository接口以及服务层实现,并分析了该方案的优点、注意事项和潜在问题,为开发者提供了一个实用且高效的解决方案。
1. 问题背景与挑战
在分布式系统或多应用实例环境中,生成具备特定系列(SERIES)且连续递增(NUMBER)的设备号是一项常见的需求。例如,设备号可能呈现AA|1、AA|2、AA|3、BB|1等格式,其中每个系列都有其最大允许数量。核心挑战在于:
- 无间隙生成: 序列号必须连续,即使在事务回滚或系统崩溃的情况下,也不能出现跳号(如从1直接跳到3)。传统的数据库自增序列或findMax()然后递增的方式,在并发和回滚场景下,往往难以保证无间隙。
- 并发安全: 多个应用实例或线程同时请求生成设备号时,必须确保序列号的唯一性和顺序性,避免竞态条件。
- 系列管理: 当一个系列的序列号达到上限时,需要能够自动切换到下一个系列并从1开始重新计数。
传统的SELECT MAX(NUMBER)方法在并发环境下存在严重问题。当一个事务查询到最大值并准备插入新记录时,另一个事务可能也同时查询到相同最大值,导致两者都尝试插入下一个相同的序列号,从而引发唯一性冲突或需要复杂的重试机制。即使通过行锁锁定查询到的最大值记录,也可能无法完全避免问题,因为锁定的只是现有记录,而不是“下一个”序列号的生成权。
2. 解决方案:专用计数器表与悲观锁
为了解决上述挑战,一种健壮且可靠的方案是引入一个专门的计数器表,并结合数据库的悲观锁(PESSIMISTIC_WRITE)机制。
2.1 核心思路
独立计数器表: 创建一个独立的数据库表,例如series_counter,用于存储每个SERIES的当前下一个可用序列号。
series_counter ----------------------- series_id | current_counter ----------------------- AA | 1 BB | 1 CC | 1 ...
current_counter字段表示对应series_id下一次将要分配的序列号。
悲观锁锁定: 当需要为某个SERIES生成序列号时,首先通过悲观写锁(PESSIMISTIC_WRITE)锁定series_counter表中对应series_id的那一行记录。这确保了在当前事务完成之前,其他任何尝试读取或修改该行记录的事务都将被阻塞,直到锁被释放。
事务原子性: 在同一个数据库事务中,完成以下操作:
- 读取被锁定的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<SeriesCounter, Long> { /** * 根据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<SeriesCounter> findBySeriesIdWithLock(@Param("seriesId") String seriesId); } public interface DeviceRepository extends JpaRepository<Device, Long> { // 基础的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注解,我们能够构建一个在多应用实例环境下可靠、无间隙的序列号生成系统。该方案利用了数据库事务的原子性和悲观锁的排他性,确保了数据的一致性和并发安全。尽管悲观锁可能引入一定的性能开销,但对于那些对序列号的连续性和完整性有严格要求的业务场景,它提供了一个简洁而强大的解决方案。在实际应用中,应根据具体的并发量和性能需求,权衡其优缺点。
文中关于的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《多应用无间隙序列号生成方法》文章吧,也可关注golang学习网公众号了解相关技术文章。

- 上一篇
- Golang函数优化:内联与逃逸分析技巧

- 下一篇
- TypeScript安全分组求和函数实现方法
-
- 文章 · java教程 | 20分钟前 |
- SpringBootJPA空指针解决方法大全
- 181浏览 收藏
-
- 文章 · java教程 | 25分钟前 |
- AndroidJava字符串比较技巧与优化
- 334浏览 收藏
-
- 文章 · java教程 | 44分钟前 |
- CustomOptionalor方法实现及变体解析
- 483浏览 收藏
-
- 文章 · java教程 | 54分钟前 |
- Java邮件SSL配置详解与设置教程
- 253浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java性能调优工具与实战案例详解
- 155浏览 收藏
-
- 文章 · java教程 | 2小时前 | Java异常处理 throws 非受检异常 受检异常 try-catch-finally
- Java异常处理技巧:代码如何应对运行时错误
- 288浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- Semaphore如何控制并发,Java信号量原理详解
- 182浏览 收藏
-
- 文章 · java教程 | 3小时前 |
- Java获取当前时间的多种方式
- 241浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- Java字符串比较正确方法全解析
- 364浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- JavaXMLJSON转换性能优化技巧
- 286浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- Glide加载SVG失败?AndroidSVG解决方法分享
- 270浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- Gradle升级compile错误解决方法
- 233浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 千音漫语
- 千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
- 274次使用
-
- MiniWork
- MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
- 263次使用
-
- NoCode
- NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
- 263次使用
-
- 达医智影
- 达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
- 273次使用
-
- 智慧芽Eureka
- 智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
- 287次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览