低模之战古代文明之战
89.73 MB · 2025-12-15
号段模式的核心思想是:不再每次生成 ID 都访问数据库,而是批量“预取”一段连续的 ID 到内存中使用。
举个生活中的例子:
你去银行取号,工作人员不是每次只给你一张号码纸,而是直接递给你一本 100 张的号段本。你可以在接下来的一段时间内自己撕下号码使用,用完再回去领新的一本。这样既减少了排队次数,又保证了号码不重复。
在技术上:
step 个 ID(比如 10 万)step 大小轻松应对更高吞吐相比雪花算法(Snowflake),它不依赖系统时钟,避免了时钟回拨问题;相比 UUID,它生成的是趋势递增的数字 ID,更适合数据库索引。
首先,我们需要一张数据库表来记录每个业务标签(bizTag)当前的最大 ID。
CREATE TABLE id_segment (
biz_tag VARCHAR(64) PRIMARY KEY, -- 业务标识,如 'short_url', 'order_id'
max_id BIGINT NOT NULL, -- 当前已分配的最大 ID
step INT NOT NULL -- 每次申请的步长
);
当第一次使用某个 bizTag 时,需在数据库中创建初始记录:
if (!repo.existsById(bizTag)) {
try {
repo.save(new IdSegment(bizTag, step));
} catch (Exception ignored) {
// 并发插入可能失败,由 DB 唯一约束兜底,后续仍可正常 fetch
}
}
这里允许多个实例同时尝试插入,但只有第一个成功,其余因主键冲突失败,不影响后续流程。
这是号段模式最关键的一步,必须保证原子性:
UPDATE id_segment
SET max_id = max_id + #{step}
WHERE biz_tag = #{bizTag};
-- 然后查询新的 max_id
SELECT max_id FROM id_segment WHERE biz_tag = #{bizTag};
在 Spring Data JPA 中(你也可以换成Mybatis,这里用JPA举例更简单),你可以这样写:
@Modifying
@Query("UPDATE IdSegment s SET s.maxId = s.maxId + :step WHERE s.bizTag = :bizTag")
int updateMaxId(@Param("bizTag") String bizTag, @Param("step") int step);
@Query("SELECT s.maxId FROM IdSegment s WHERE s.bizTag = :bizTag")
Long findMaxIdByBizTag(@Param("bizTag") String bizTag);
假设 step = 100_000,更新后 max_id = 200_000,那么本次分配的号段就是:
200_000 - 100_000 + 1 = 100_001200_000[100001, 100002, ..., 200000]我们将这个区间封装为一个 Range 对象:
class Range {
private final long end;
private final AtomicLong cursor; // 当前已分配的位置
Range(long start, long end) {
this.end = end;
this.cursor = new AtomicLong(start - 1); // 下一次 get 就是 start
}
long next() {
long v = cursor.incrementAndGet();
return v <= end ? v : -1; // -1 表示耗尽
}
}
为了在当前号段用完时不阻塞用户请求,我们采用“双缓冲”策略:
current:正在使用的号段next:预加载的下一个号段当 current 耗尽时,立即切换到 next,然后在同步方法中再去申请新的 next(未来可优化为异步)。
synchronized long nextId() {
long id = current.next();
if (id != -1) return id;
// 切换到预加载段
current = next;
next = allocateNewRange(); // 同步拉取新段
return current.next();
}
虽然切换时仍会短暂阻塞,但由于 step 足够大(如 10 万),切换频率极低(每 10 万个请求才一次),对整体性能影响微乎其微。
@Entity
@Table(name = "id_segment")
public class IdSegment {
@Id
private String bizTag;
private Long maxId;
private Integer step;
}
public interface IdSegmentRepository extends JpaRepository<IdSegment, String> {
@Modifying
@Query("UPDATE IdSegment s SET s.maxId = s.maxId + :step WHERE s.bizTag = :bizTag")
int updateMaxId(@Param("bizTag") String bizTag, @Param("step") int step);
@Query("SELECT s.maxId FROM IdSegment s WHERE s.bizTag = :bizTag")
Long findMaxIdByBizTag(@Param("bizTag") String bizTag);
}
@Service
public class SegmentIdGenerator implements IdGenerator {
private final IdSegmentRepository segmentRepo;
private final Map<String, Buffer> bufferCache = new ConcurrentHashMap<>();
public SegmentIdGenerator(IdSegmentRepository segmentRepo) {
this.segmentRepo = segmentRepo;
}
@Override
public long nextId(String bizTag) {
// 每个 bizTag 对应一个 Buffer(懒加载)
return bufferCache.computeIfAbsent(bizTag, Buffer::new).nextId();
}
/**
* 每个业务标签(bizTag)对应一个独立的 ID 缓冲区
*/
class Buffer {
private final String bizTag;
private static final int STEP = 100_000; // 每次预取 10w 个 ID
// 双缓冲:current 正在使用,next 预加载(避免切换时阻塞)
private volatile Range currentRange;
private volatile Range nextRange;
Buffer(String bizTag) {
this.bizTag = bizTag;
initialize(); // 初始化两个号段
}
/**
* 获取下一个可用 ID
*/
synchronized long nextId() {
long id = currentRange.next();
if (id != -1) {
return id; // 当前号段还有余量
}
// 当前号段耗尽,切换到预加载的下一段
currentRange = nextRange;
nextRange = allocateNewRange(); // 同步拉取新段(可后续优化为异步)
return currentRange.next();
}
/**
* 初始化:确保 DB 有记录,并加载前两段
*/
private void initialize() {
ensureSegmentExistsInDb();
this.currentRange = allocateNewRange();
this.nextRange = allocateNewRange();
}
/**
* 确保数据库中存在该 bizTag 的记录(首次使用时创建)
*/
private void ensureSegmentExistsInDb() {
// 先检查是否存在
if (!segmentRepo.existsById(bizTag)) {
try {
IdSegment segment = new IdSegment();
segment.setBizTag(bizTag);
segment.setMaxId(1L);
segment.setStep(STEP);
segmentRepo.save(segment);
} catch (Exception ex) {
// 并发场景下可能多个实例同时插入,由 DB 唯一约束保证最终只有一个成功
// 这里忽略异常,后续 fetch 仍能成功
}
}
}
/**
* 从数据库原子地申请一个新的 ID 号段 [start, end]
*/
private Range allocateNewRange() {
// 1. 原子更新 max_id(关键:DB 行锁保证并发安全)
int updatedRows = segmentRepo.updateMaxId(bizTag, STEP);
if (updatedRows == 0) {
throw new RuntimeException("Failed to update max_id for bizTag: " + bizTag);
}
// 2. 查询更新后的 max_id(即新区间的结束值)
Long newMaxId = segmentRepo.findMaxIdByBizTag(bizTag);
if (newMaxId == null) {
throw new IllegalStateException("Segment record missing after update: " + bizTag);
}
long end = newMaxId;
long start = end - STEP + 1;
return new Range(start, end);
}
}
/**
* 表示一个连续的 ID 区间 [start, end]
*/
static class Range {
private final long end;
private final AtomicLong cursor; // 当前已分配到的位置
Range(long start, long end) {
this.end = end;
this.cursor = new AtomicLong(start - 1); // 下一次 incrementAndGet 得到 start
}
/**
* 返回下一个 ID,若耗尽返回 -1
*/
long next() {
long value = cursor.incrementAndGet();
return (value <= end) ? value : -1;
}
}
}
合理设置 step:
监控号段切换频率:
fetchRange() 调用次数step 太小支持多业务线隔离:
bizTag(如 "order", "user")未来优化方向:
current 使用到 80% 时,异步线程预加载 nextstep号段模式是一种简单却极其有效的分布式 ID 生成方案。它巧妙地平衡了性能、一致性、可靠性三大要素,特别适合短链、订单、消息等需要趋势递增、全局唯一 ID 的场景。
另外,号段模式也不是完美的,存在几个明显的缺陷:
相关文章:分布式 ID 生成策略全景图:UUID、号段、Snowflake、Leaf、TinyID,如何选型?