
在分布式系统或多应用实例环境中,生成具备特定系列(series)且连续递增(number)的设备号是一项常见的需求。例如,设备号可能呈现aa|1、aa|2、aa|3、bb|1等格式,其中每个系列都有其最大允许数量。核心挑战在于:
传统的SELECT MAX(NUMBER)方法在并发环境下存在严重问题。当一个事务查询到最大值并准备插入新记录时,另一个事务可能也同时查询到相同最大值,导致两者都尝试插入下一个相同的序列号,从而引发唯一性冲突或需要复杂的重试机制。即使通过行锁锁定查询到的最大值记录,也可能无法完全避免问题,因为锁定的只是现有记录,而不是“下一个”序列号的生成权。
为了解决上述挑战,一种健壮且可靠的方案是引入一个专门的计数器表,并结合数据库的悲观锁(PESSIMISTIC_WRITE)机制。
独立计数器表: 创建一个独立的数据库表,例如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的那一行记录。这确保了在当前事务完成之前,其他任何尝试读取或修改该行记录的事务都将被阻塞,直到锁被释放。
事务原子性: 在同一个数据库事务中,完成以下操作:
假设我们有以下实体:
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;
}
}通过引入专用的series_counter表并结合Spring Data JPA的@Lock(LockModeType.PESSIMISTIC_WRITE)和@Transactional注解,我们能够构建一个在多应用实例环境下可靠、无间隙的序列号生成系统。该方案利用了数据库事务的原子性和悲观锁的排他性,确保了数据的一致性和并发安全。尽管悲观锁可能引入一定的性能开销,但对于那些对序列号的连续性和完整性有严格要求的业务场景,它提供了一个简洁而强大的解决方案。在实际应用中,应根据具体的并发量和性能需求,权衡其优缺点。
以上就是生成多应用实例无间隙序列号指南的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号