号段模式(分布式ID)上手指南:从原理到实战

2025-12-04 0 303

一、什么是号段模式

号段模式的核心思想是:不再每次生成 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 {
    @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 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,如何选型?

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 号段模式(分布式ID)上手指南:从原理到实战 https://www.zuozi.net/3616.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务