【分布式利器:分布式ID】3、号段模式:中高并发首选,数据库+本地缓存实现

# 第3篇:号段模式:中高并发首选,数据库+本地缓存实现

系列导读

上一篇的数据库自增ID方案,在中高并发场景(如电商订单每秒10万+请求)会出现瓶颈——频繁访问数据库导致数据库压力过大,甚至卡顿。
今天的“号段模式”完美解决了这个问题:通过“预分配号段+本地缓存”,大幅减少数据库访问次数,QPS可达10万+,同时保留ID有序性,是中高并发场景的首选方案。
本文详解原理、实战代码和避坑点。

一、适用场景

  • 中高并发(QPS=1万~10万+);
  • 需ID有序(支持分页查询、排序);
  • 核心业务(电商订单、支付流水、物流单号);
  • 不想引入复杂中间件,希望基于数据库实现。

二、核心原理:预分配号段+本地缓存

号段模式的核心思路是“一次从数据库拿一段ID,本地缓存后按需分配,用完再拿”,类似餐厅“一次领100个号,叫完再领”,减少排队次数:

  1. 号段定义:一段连续的ID区间(如1000-2000,步长=1000);
  2. 预分配:服务启动时,从数据库申请一段号段,缓存到本地内存;
  3. 本地分配:业务需要ID时,直接从本地缓存的号段中递增分配(无需访问数据库);
  4. 号段耗尽:当本地号段分配到80%(如1800)时,异步申请下一段号段,避免阻塞;
  5. 数据库存储:数据库只存储“当前最大ID”和“步长”,无需存储每一个ID。

号段模式流程示意图

服务A本地缓存号段:1000-2000
↓
业务请求→从1000开始分配(1000→1001→1002...)
↓
分配到1800(号段80%)→异步申请下一段(2001-3000)
↓
本地号段耗尽(2000)→直接使用新申请的号段(2001开始)

三、实战实现:数据库+Java代码(含预申请优化)

1. 数据库表设计(存储号段信息)

CREATE TABLE `id_generator_segment` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `biz_type` varchar(32) NOT NULL COMMENT '业务类型(order/pay/logistics)',
  `current_max_id` bigint NOT NULL COMMENT '当前最大ID(号段结束值)',
  `step` int NOT NULL COMMENT '号段步长(一次申请的ID数量)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-可用,0-禁用',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_biz_type` (`biz_type`) COMMENT '按业务类型隔离'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='号段模式ID生成表';

-- 初始化订单业务号段(步长=1000)
INSERT INTO `id_generator_segment` (`biz_type`, `current_max_id`, `step`) 
VALUES ('order', 1000, 1000);

2. Java核心代码实现(Spring Boot)

2.1 号段实体类
@Data
public class Segment {
    private String bizType;       // 业务类型
    private Long startId;        // 号段起始ID
    private Long endId;          // 号段结束ID
    private Long currentId;      // 当前分配到的ID
    private Integer step;        // 步长
    private boolean isNeedRefresh; // 是否需要申请下一段
}
2.2 号段生成服务(核心逻辑)
@Service
public class SegmentIdGeneratorService {
    @Autowired
    private IdGeneratorSegmentMapper segmentMapper;

    // 本地缓存号段(key=业务类型,value=号段信息)
    private ConcurrentHashMap<String, Segment> segmentCache = new ConcurrentHashMap<>();
    // 步长80%时触发预申请
    private static final double REFRESH_THRESHOLD = 0.8;

    // 获取分布式ID
    public Long getDistributedId(String bizType) {
        // 1. 本地缓存中获取号段,无则初始化
        Segment segment = segmentCache.get(bizType);
        if (segment == null) {
            segment = initSegment(bizType);
            segmentCache.put(bizType, segment);
        }

        // 2. 本地分配ID(原子递增)
        Long id = segment.getCurrentId() + 1;

        // 3. 检查是否需要预申请下一段(避免号段耗尽阻塞)
        if (id >= segment.getEndId() * REFRESH_THRESHOLD && !segment.isNeedRefresh()) {
            segment.setNeedRefresh(true);
            // 异步申请下一段号段(不阻塞当前ID分配)
            CompletableFuture.runAsync(() -> applyNextSegment(bizType));
        }

        // 4. 检查当前号段是否耗尽,耗尽则切换到新号段
        if (id > segment.getEndId()) {
            segment = waitForNewSegment(bizType); // 等待新号段
        }

        // 5. 更新当前ID
        segment.setCurrentId(id);
        return id;
    }

    // 初始化号段(从数据库获取第一段)
    private Segment initSegment(String bizType) {
        IdGeneratorSegment dbSegment = segmentMapper.selectByBizType(bizType);
        Segment segment = new Segment();
        segment.setBizType(bizType);
        segment.setStartId(dbSegment.getCurrentMaxId() - dbSegment.getStep() + 1); // 1000-1000+1=1
        segment.setEndId(dbSegment.getCurrentMaxId()); // 1000
        segment.setCurrentId(segment.getStartId() - 1); // 0(初始值)
        segment.setStep(dbSegment.getStep());
        segment.setNeedRefresh(false);
        return segment;
    }

    // 申请下一段号段(更新数据库)
    @Transactional
    private void applyNextSegment(String bizType) {
        // 乐观锁更新(避免并发更新冲突)
        IdGeneratorSegment dbSegment = segmentMapper.selectByBizType(bizType);
        Long newMaxId = dbSegment.getCurrentMaxId() + dbSegment.getStep();
        int affected = segmentMapper.updateCurrentMaxId(
            bizType, newMaxId, dbSegment.getCurrentMaxId() // 乐观锁条件:当前最大ID未被修改
        );

        if (affected > 0) {
            // 新号段生效
            Segment newSegment = new Segment();
            newSegment.setBizType(bizType);
            newSegment.setStartId(dbSegment.getCurrentMaxId() + 1); // 1001
            newSegment.setEndId(newMaxId); // 2000
            newSegment.setCurrentId(newSegment.getStartId() - 1); // 1000
            newSegment.setStep(dbSegment.getStep());
            newSegment.setNeedRefresh(false);
            segmentCache.put(bizType, newSegment);
        } else {
            // 更新失败(并发冲突),重试
            applyNextSegment(bizType);
        }
    }

    // 等待新号段(号段耗尽时阻塞)
    private Segment waitForNewSegment(String bizType) {
        int retryCount = 0;
        while (retryCount < 30) { // 最多等待3秒
            Segment segment = segmentCache.get(bizType);
            if (segment.getCurrentId() < segment.getEndId()) {
                return segment;
            }
            try {
                Thread.sleep(100);
                retryCount++;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        throw new RuntimeException("申请新号段超时");
    }
}
2.3 Mapper接口(MyBatis)
public interface IdGeneratorSegmentMapper {
    // 按业务类型查询号段
    IdGeneratorSegment selectByBizType(@Param("bizType") String bizType);

    // 更新当前最大ID(乐观锁)
    int updateCurrentMaxId(
        @Param("bizType") String bizType,
        @Param("newMaxId") Long newMaxId,
        @Param("oldMaxId") Long oldMaxId
    );
}

3. 调用示例

@Controller
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private SegmentIdGeneratorService idGeneratorService;

    @PostMapping("/create")
    public ResponseEntity<?> createOrder() {
        // 生成订单ID(号段模式)
        Long orderId = idGeneratorService.getDistributedId("order");
        // 后续订单创建逻辑
        return ResponseEntity.ok("订单创建成功,订单ID:" + orderId);
    }
}

四、优点&缺点

  • 优点:
    1. 性能高:本地缓存号段,数据库访问频率极低(QPS可达10万+);
    2. 有序性:同一号段内ID连续递增,支持排序和分页;
    3. 高可用:数据库主从切换时,本地缓存的号段仍可分配ID,不阻塞业务;
    4. 扩展性好:步长可动态调整(如大促前增大步长=10000)。
  • 缺点:
    1. 服务宕机可能丢失未分配的号段(如号段1000-2000,只分配到1500,服务宕机后1501-2000丢失,ID会不连续,但不影响唯一性);
    2. 号段耗尽时可能短暂阻塞(需依赖预申请优化,提前申请下一段)。

五、避坑指南:3个关键问题的解决方案

1. 服务宕机导致的号段丢失

  • 问题:服务宕机后,本地缓存的未分配号段会丢失,ID出现断层;
  • 解决方案:
    • 号段丢失是可接受的(ID只需唯一,无需连续);
    • 若业务要求ID连续(如金融流水号),可在数据库中记录已分配的最大ID,服务重启后从数据库获取最新ID,重新申请号段。

2. 号段预申请失败

  • 问题:预申请下一段号段时,数据库宕机导致申请失败,当前号段耗尽后阻塞;
  • 解决方案:
    • 数据库部署主从+哨兵,确保高可用;
    • 预申请时重试3次,失败则记录日志,人工介入处理。

3. 并发更新号段冲突

  • 问题:多服务实例同时申请下一段号段,导致数据库更新冲突;
  • 解决方案:
    • 用乐观锁(update ... where current_max_id = 旧值)避免冲突;
    • 冲突后重试,确保最终更新成功。

实战Tips

  1. 步长设置:根据业务并发量调整(低并发步长=1000,高并发步长=10000),步长越大,数据库访问越少;
  2. 预申请阈值:建议设置为80%(如号段1000-2000,分配到1800时预申请),预留足够时间避免阻塞;
  3. 多业务隔离:通过biz_type字段隔离不同业务(如订单、支付、物流),避免ID冲突;
  4. 监控告警:监控号段剩余量,当剩余量低于20%且预申请失败时,触发告警(如钉钉通知)。

下一篇预告

号段模式适合中高并发,但超高并发场景(如秒杀每秒100万+ID)仍有优化空间——本地缓存分配虽快,但仍有内存计算开销。
下一篇我们拆解“雪花算法(Snowflake)”:纯内存计算,无数据库依赖,QPS可达百万+,是超高并发场景的终极方案!
你在项目中用过号段模式吗?评论区聊聊~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无心水

您的鼓励就是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值