AI摘要

作者因分库分表放弃数据库自增ID,对比UUID、Leaf-segment、Redis后选型Snowflake,借助ZooKeeper持久化workerId并校验时间戳,解决时钟回拨,实现Spring Boot开箱即用的IdGenerator,线上稳定运行。
当数据库开始分库分表,自增主键的局限性暴露无遗。我经历了从UUID到Redis序列,最终选择并深度实践Snowflake算法的全过程。本文不仅对比方案,更分享了我们在生产环境中解决Snowflake时钟回拨问题的最佳实践。
  1. 为什么需要分布式ID?
  • ​场景:​​ 用户表数据量过大,决定分库分表(如分成4个库,每个库8张表)。
  • ​问题:​​ 如果继续使用数据库自增ID,会导致不同表、甚至不同库中出现相同的ID,无法作为全局唯一标识。同时,自增ID有连续性,容易暴露业务量,且可能被猜测。
  1. 各种方案对比与我踩的坑
  • 方案一:UUID

    • ​优点:​​ 生成简单,本地生成,无需网络调用,全球唯一。
    • 缺点(我踩的坑):

      1. ​无序性:​​ 作为数据库主键,插入时会导致B+树频繁的页分裂,严重影响写入性能。
      2. ​长度长:​​ 存储空间大,查询效率相对较低。
    • ​结论:​​ 不适合作为数据库主键,尤其在高并发写入场景下。
  • 方案二:数据库号段模式(Leaf-segment)

    • ​原理:​​ 在数据库中维护一张表,记录业务标识和当前最大ID。每次申请一个号段(如1-1000),用完再申请。
    • ​优点:​​ 趋势递增,生成的ID是数字,性能尚可。
    • ​缺点:​​ 强依赖数据库,数据库挂则系统停。有网络开销。
  • 方案三:Redis INCR

    • ​原理:​​ 利用Redis的原子操作INCRINCRBY来生成序列。
    • ​优点:​​ 性能比数据库好。
    • ​缺点:​​ 同样存在单点依赖问题。需要保证Redis的高可用,增加了系统复杂性。
  1. 最终选择:Snowflake算法及其深度实践
  • ​算法原理:​​ 一个64位的Long型数字,结构为:符号位(0) + 时间戳(41位) + 工作机器ID(10位) + 序列号(12位)

    • ​时间戳:​​ 毫秒级,可用约69年。
    • ​工作机器ID:​​ 可配置,支持最多1024个节点。
    • ​序列号:​​ 同一毫秒内最多生成4096个ID。
  • ​优点:​​ 本地生成,性能极高(每秒百万级);ID趋势递增;数字类型,存储查询效率高。
  • 核心挑战:时钟回拨问题及我们的解决方案

    • ​问题:​​ 当服务器本地时钟发生回拨(如与网络时间服务器同步时),可能会导致生成的ID重复。
    • ​解决方案1(默认,轻量级):​​ 如果回拨时间很短(如毫秒级、秒级),则让算法等待相应的毫秒数再继续生成。
// 伪代码
long currentTimestamp = timeGen();
if (currentTimestamp < lastTimestamp) { // 发生时钟回拨
    long offset = lastTimestamp - currentTimestamp;
    if (offset <= 5) { // 假设最大容忍5毫秒的回拨
        try {
            wait(offset); // 等待直到时间追平
            currentTimestamp = timeGen();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    } else {
        // 回拨时间过长,无法等待,需要更复杂的处理
        throw new ClockMovedBackwardsException("时钟回拨异常");
    }
}

​解决方案2(我们的生产级方案):​​ 将工作机器ID(workerId)与序列号序列分开持久化。我们使用ZooKeeper(或Etcd)的持久顺序节点来分配workerId。即使时钟发生较大回拨,服务重启后,通过ZooKeeper能保证获取到与之前相同的workerId,然后我们可以在启动时检查上次服务停止时的时间戳,如果当前时间小于它,则不允许启动,并发出严重报警,需要人工介入校对时钟

  1. 落地与使用
  • 我们使用Hutool工具包封装的Snowflake类,并对其进行了二次封装,解决了时钟回拨问题,并通过Spring Boot自动配置,在应用中注入一个IdGeneratorBean即可使用。
  • 代码示例:
@Service
public class OrderService {
    @Autowired
    private IdGenerator idGenerator; // 我们封装的雪花算法生成器

    public void createOrder(OrderDTO orderDTO) {
        Order order = new Order();
        // 生成分布式ID
        order.setId(idGenerator.nextId());
        // ... 其他业务逻辑
        orderMapper.insert(order);
    }
}

总结

  • ​选型思考:​​ 没有完美的方案,只有最适合的。Snowflake在性能、存储、全局唯一性上取得了很好的平衡,是当前互联网公司最主流的选择。
  • ​关键点:​​ 实现Snowflake算法不难,难的是在生产环境中稳定运行。​时钟回拨问题是核心​,必须根据业务的容忍度设计可靠的应对策略。
版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:分布式ID生成器:从数据库自增ID到Snowflake算法的选型与实践
▶ 本文链接:https://www.huangleicole.com/backend-related/40.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏