一、什么是号段模式?

号段模式的核心思想是:不再每次生成 ID 都访问数据库,而是批量“预取”一段连续的 ID 到内存中使用

举个生活中的例子:
你去银行取号,工作人员不是每次只给你一张号码纸,而是直接递给你一本 100 张的号段本。你可以在接下来的一段时间内自己撕下号码使用,用完再回去领新的一本。这样既减少了排队次数,又保证了号码不重复。

在技术上:

  • 每次从数据库申请 step 个 ID(比如 10 万)
  • 应用本地用原子变量递增分配
  • 当前号段快用完时,异步或同步申请下一段
  • 所有状态通过数据库持久化,支持多实例部署和重启恢复

二、为什么选择号段模式?

  • 高性能:99% 的 ID 获取是纯内存操作(纳秒级)
  • 强一致性:依赖数据库事务和行锁,保证多实例下绝对唯一
  • 无中心节点:每个服务实例独立工作,无单点故障
  • 可扩展:通过增加 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              -- 每次申请的步长
);

四、核心逻辑详解

1. 初始化:确保记录存在

当第一次使用某个 bizTag 时,需在数据库中创建初始记录:

if (!repo.existsById(bizTag)) {
    try {
        repo.save(new IdSegment(bizTag, step));
    } catch (Exception ignored) {
        // 并发插入可能失败,由 DB 唯一约束兜底,后续仍可正常 fetch
    }
}

这里允许多个实例同时尝试插入,但只有第一个成功,其余因主键冲突失败,不影响后续流程。


2. 申请新号段:原子更新 + 查询

这是号段模式最关键的一步,必须保证原子性

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);

3. 构建号段区间

假设 step = 100_000,更新后 max_id = 200_000,那么本次分配的号段就是:

  • 起始值:200_000 - 100_000 + 1 = 100_001
  • 结束值:200_000
  • 可用 ID:[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 表示耗尽
    }
}

4. 双缓冲机制:避免切换阻塞

为了在当前号段用完时不阻塞用户请求,我们采用“双缓冲”策略:

  • 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 万个请求才一次),对整体性能影响微乎其微。


五、完整代码实现(Spring Boot+JPA举例)

1. 实体类

@Entity
@Table(name = "id_segment")
public class IdSegment {
    @Id
    private String bizTag;
    private Long maxId;
    private Integer step;
}

2. Repository

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);
}

3. ID 生成器

@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;
        }
    }
}

六、使用建议

  1. 合理设置 step

    • 小型应用:1 万 ~ 5 万
    • 中大型应用:10 万 ~ 100 万
    • 过大会导致 ID 浪费(重启丢失未用完的号段)
  2. 监控号段切换频率

    • 通过日志或指标监控 fetchRange() 调用次数
    • 如果太频繁,说明 step 太小
  3. 支持多业务线隔离

    • 不同业务使用不同 bizTag(如 "order", "user"
    • 避免相互影响
  4. 未来优化方向

    • current 使用到 80% 时,异步线程预加载 next
    • 支持动态调整 step
    • 增加重试机制应对数据库临时故障

七、总结与注意事项

号段模式是一种简单却极其有效的分布式 ID 生成方案。它巧妙地平衡了性能、一致性、可靠性三大要素,特别适合短链、订单、消息等需要趋势递增、全局唯一 ID 的场景。

另外,号段模式也不是完美的,存在几个明显的缺陷:

相关文章:分布式 ID 生成策略全景图:UUID、号段、Snowflake、Leaf、TinyID,如何选型?

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]