多应用无间隙序列号生成方法解析
时间:2025-08-13 21:30:35 323浏览 收藏
本文针对多应用实例环境下生成无间隙序列号的难题,提出了一种基于独立计数器表和悲观写锁的解决方案。该方案通过为每个序列维护独立的计数器,并利用数据库的悲观锁机制,保证在高并发和事务回滚场景下序列号的严格递增和唯一性,有效避免了传统自增ID可能产生的间隙问题以及直接查询最大值带来的竞态条件。文章提供了基于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) 注解。
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. 工作原理与机制解释
悲观写锁 (PESSIMISTIC_WRITE): 当 generateDeviceNumber 方法被调用时,seriesCounterRepo.fetchLatest(seriesId) 会执行一个数据库查询,并在返回 SeriesCounter 记录的同时,对该记录施加一个排他写锁。这意味着:
- 在当前事务提交或回滚之前,其他任何尝试获取同一 seriesId 记录写锁的事务都会被阻塞,直到锁被释放。
- 其他事务也无法读取(取决于数据库隔离级别,但在大多数情况下,悲观写锁会阻止脏读、不可重复读)。
- 这保证了在任何给定时刻,只有一个事务能够“看到”并操作特定 seriesId 的 current_counter 值。
事务原子性: generateDeviceNumber 方法被 @Transactional 注解修饰。这意味着整个操作(获取计数器、使用计数器生成新记录、递增计数器)被封装在一个数据库事务中。
- 如果事务成功完成,series_counter 表中的 current_counter 会被更新,新生成的设备记录也会被持久化。
- 如果事务在任何步骤中失败(例如,网络中断、数据库错误、业务逻辑异常),整个事务会回滚。由于 series_counter 的更新也是事务的一部分,回滚会撤销对 current_counter 的任何修改,确保不会产生“跳跃”的序列号。例如,如果 current_counter 被读取为 X,但在保存新设备时失败,那么 current_counter 不会变成 X+1,下次尝试时仍会从 X 开始。
避免 findMax() 的问题: 相比于每次都查询业务表(SERIES 表)的最大 NUMBER 值,这种方案的优势在于:
- 它锁定的是一个非常小的、专门用于计数的记录,而不是整个业务表或其索引。这大大降低了锁的粒度,减少了资源争用。
- findMax() 方式需要更复杂的锁定策略来确保没有间隙,例如锁定整个表或使用范围锁,这通常效率低下且难以正确实现。而锁定一个独立的计数器记录则简单高效。
4. 注意事项与考量
- 性能影响: 悲观锁虽然能保证严格的无间隙序列,但它会引入串行化操作,在高并发场景下可能成为性能瓶颈。如果序列号的生成频率非常高,需要评估这种方案的吞吐量是否满足需求。对于极高并发,可以考虑批量获取序列号(例如,一次性获取100个,然后在应用内存中分配),但这会引入间隙的风险,需要仔细权衡。
- 事务隔离级别: 确保数据库的事务隔离级别能够支持悲观锁的预期行为(通常是 READ COMMITTED 或 REPEATABLE READ 即可,因为锁会强制串行化)。
- 错误处理: 确保业务逻辑中的异常处理能够正确触发事务回滚,从而避免计数器被错误地递增。
- 初始化: 确保 series_counter 表在系统启动或新系列首次使用前,有正确的初始值(例如,0或1)。
- 数据库支持: 大多数关系型数据库(如PostgreSQL、MySQL、Oracle等)都支持悲观锁。
总结
通过引入独立的计数器表并结合悲观写锁,我们能够可靠地在多应用实例环境下生成严格无间隙的序列号。这种方案通过在事务层面保证计数器操作的原子性和隔离性,有效地解决了并发和事务回滚带来的序列号间隙问题。虽然它可能引入一定的性能开销,但对于那些对序列号连续性有严格要求的业务场景,这是一种健壮且易于理解和实现的策略。在实际应用中,务必根据业务的具体需求和并发量来权衡和选择最合适的序列号生成方案。
今天关于《多应用无间隙序列号生成方法解析》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于的内容请关注golang学习网公众号!
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
161 收藏
-
161 收藏
-
415 收藏
-
460 收藏
-
500 收藏
-
150 收藏
-
272 收藏
-
445 收藏
-
258 收藏
-
492 收藏
-
331 收藏
-
102 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习