当前位置:首页 > 问答 > 正文

分布式ID 唯一标识 通过Redis搭建高效分布式唯一ID生成服务,解析redis的分布式id方案

用Redis打造高并发下的分布式ID生成器,再也不怕订单号重复了!

场景:双十一的订单号灾难

去年双十一,我们电商系统遇到了一个棘手问题——订单号重复,凌晨流量高峰时,每秒上万笔订单涌入,原本好好的自增ID生成服务突然开始吐出重复的订单号,导致大量支付失败和售后纠纷,事后排查发现,单机版ID生成器在集群环境下根本扛不住高并发,多个实例同时分配ID导致冲突。

这次事故后,我们团队决定重构ID生成服务,经过多方对比,最终选择了基于Redis的分布式ID方案,它不仅解决了我们的燃眉之急,还带来了意想不到的性能提升,下面我就详细分享这个方案的实现细节。

为什么Redis适合做分布式ID生成器?

传统数据库自增ID最大的问题就是扩展性差,当系统需要水平扩展时,单点故障和性能瓶颈就会暴露无遗,而Redis在这方面有几个天然优势:

  1. 内存级速度:ID生成通常要频繁读写,Redis的10万+ QPS完全能hold住
  2. 原子操作:INCR命令保证原子性,不会出现并发冲突
  3. 持久化可选:根据业务需求选择RDB或AOF,平衡性能与可靠性
  4. 集群支持:Redis Cluster可以线性扩展,避免单点瓶颈

基础版:Redis原子计数器方案

最简单的实现就是利用Redis的INCR命令:

import redis
r = redis.StrictRedis(host='localhost', port=6379, db=0)
def generate_id():
    return r.incr('global_order_id')

这个方案虽然简单,但有几个明显缺陷:

  • ID是连续递增的,容易被猜测业务量
  • 单Redis实例存在性能上限
  • 重启后需要手动维护初始值

进阶版:分段ID生成方案

我们在生产环境使用的是改进后的分段批处理方案,核心思想是"预分配":

def get_id_batch():
    # 获取当前批次最大值
    current_max = r.get('id_max')
    if not current_max:
        # 初始化1000个ID空间
        r.set('id_max', 1000)
        return (1, 1000)
    # 申请下一个批次
    new_max = r.incrby('id_max', 1000)
    return (new_max - 999, new_max)

应用服务器每次获取一个ID区间(如1-1000),在内存中本地分配,这带来三个好处:

  1. 减少Redis访问次数,性能提升10倍以上
  2. 即使Redis短暂不可用,本地仍有ID可用
  3. 可以通过调整批次大小平衡性能与浪费

高可用方案:Redis Cluster+本地缓存

对于千万级并发的场景,我们进一步优化架构:

分布式ID 唯一标识 通过Redis搭建高效分布式唯一ID生成服务,解析redis的分布式id方案

  1. 多维度ID:将64位ID拆解为「时间戳(32位)+节点ID(10位)+序列号(22位)」
  2. 集群部署:使用Redis Cluster分散压力,每个节点负责特定范围的ID
  3. 双缓冲机制:当前批次使用量达80%时,异步预加载下一批次
  4. 降级策略:Redis故障时自动切换本地雪花算法

核心代码结构:

class DistributedIDGenerator:
    def __init__(self, node_id):
        self.node_id = node_id  # 节点标识
        self.current_batch = (0, 0)
        self.next_batch = (0, 0)
        self.lock = threading.Lock()
    def get_id(self):
        with self.lock:
            if self.__need_refill():
                self.__async_refill()
            return self.__consume_id()
    def __need_refill(self):
        return self.current_batch[0] >= self.current_batch[1] * 0.8

性能优化实战技巧

在实际压测中,我们总结出几个关键优化点:

  1. 管道技术:批量获取ID区间时使用pipeline减少网络往返

    pipe = r.pipeline()
    pipe.get('current_max')
    pipe.incrby('current_max', 1000)
    results = pipe.execute()
  2. Lua脚本:将判断与递增操作原子化

    local current = redis.call('GET', KEYS[1])
    if not current then
     redis.call('SET', KEYS[1], ARGV[1])
     return {0, ARGV[1]}
    end
    local new = redis.call('INCRBY', KEYS[1], ARGV[1])
    return {new-ARGV[1]+1, new}
  3. 热点分离:不同业务使用不同key前缀,如"order_id"、"user_id"

异常处理经验谈

在线上环境中,我们遇到过几个典型问题及解决方案:

分布式ID 唯一标识 通过Redis搭建高效分布式唯一ID生成服务,解析redis的分布式id方案

  1. 时钟回拨问题:服务器时间同步导致时间戳倒退

    解决方案:记录最后生成ID的时间戳,检测到回拨时短暂等待

  2. 批次浪费问题:服务重启时未使用的ID区间丢失

    解决方案:将未使用区间写入Redis临时key,启动时恢复

  3. 长尾请求:网络波动导致个别请求超时

    解决方案:设置分级超时,首次50ms,重试200ms

    分布式ID 唯一标识 通过Redis搭建高效分布式唯一ID生成服务,解析redis的分布式id方案

与其他方案对比

我们曾经对比过几种主流方案:

方案 QPS上限 依赖性 是否有序 实现复杂度
Redis方案 10万+
数据库自增 5千
UUID 无限
雪花算法 1万/节点

最终选择Redis方案是因为它在有序性、性能和复杂度之间取得了最佳平衡,特别是在已有Redis集群的场景下,几乎不需要额外运维成本。

未来演进方向

随着业务发展,我们的ID生成器还在持续进化:

  1. 混合时钟:结合物理时钟和逻辑时钟解决时间回拨问题
  2. 动态调整:根据负载自动调节批次大小
  3. 跨机房同步:通过Redis主从同步实现多机房ID分发

这个方案上线后,经历了三次618和双十一大促的考验,始终保持零故障,最重要的是,它让我们明白了:好的技术方案不一定要多么高大上,适合业务场景的才是最好的。

发表评论