
在分布式系统或多应用实例的场景中,生成严格递增且不含间隙的序列号是一项常见的需求。例如,设备编号、订单号等业务场景,可能要求序列号在任何情况下都不能出现跳跃(即使事务回滚)或重复。传统的数据库自增id(如postgresql的serial或sequence)虽然能保证唯一性,但在事务回滚时可能会产生间隙,这不符合某些业务的严格要求。直接通过查询最大值(findmax())然后递增的方式,在并发环境下极易出现竞态条件,导致序列号重复或产生间隙,且锁定整个数据表或范围的开销巨大。
为了解决上述挑战,我们引入一个专门用于维护序列号当前值的独立计数器表,并结合数据库的悲观写锁(PESSIMISTIC_WRITE)机制。这种方法的核心思想是:为每个需要生成序列号的“系列”(例如,不同的设备系列或产品类别)维护一个独立的计数器,并在获取和更新该计数器时施加排他锁,确保操作的原子性和隔离性。
首先,创建一个名为 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,使用它,然后将其递增。
在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<SeriesCounter, String> {
/**
* 获取指定系列的最新的计数器值,并施加悲观写锁。
* 该方法必须在一个事务中执行。
* @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<Series, Long> {}悲观写锁 (PESSIMISTIC_WRITE): 当 generateDeviceNumber 方法被调用时,seriesCounterRepo.fetchLatest(seriesId) 会执行一个数据库查询,并在返回 SeriesCounter 记录的同时,对该记录施加一个排他写锁。这意味着:
事务原子性: generateDeviceNumber 方法被 @Transactional 注解修饰。这意味着整个操作(获取计数器、使用计数器生成新记录、递增计数器)被封装在一个数据库事务中。
避免 findMax() 的问题: 相比于每次都查询业务表(SERIES 表)的最大 NUMBER 值,这种方案的优势在于:
通过引入独立的计数器表并结合悲观写锁,我们能够可靠地在多应用实例环境下生成严格无间隙的序列号。这种方案通过在事务层面保证计数器操作的原子性和隔离性,有效地解决了并发和事务回滚带来的序列号间隙问题。虽然它可能引入一定的性能开销,但对于那些对序列号连续性有严格要求的业务场景,这是一种健壮且易于理解和实现的策略。在实际应用中,务必根据业务的具体需求和并发量来权衡和选择最合适的序列号生成方案。
以上就是生成多应用实例无间隙序列号的策略与实现的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号