基于Redis的3种分布式ID生成策略

发布于:2025-04-22 ⋅ 阅读:(16) ⋅ 点赞:(0)

在分布式系统设计中,全局唯一ID是一个基础而关键的组件。随着业务规模扩大和系统架构向微服务演进,传统的单机自增ID已无法满足需求。高并发、高可用的分布式ID生成方案成为构建可靠分布式系统的必要条件。

Redis具备高性能、原子操作及简单易用的特性,因此我们可以基于Redis实现全局唯一ID的生成。

分布式ID的核心需求

一个优秀的分布式ID生成方案应满足以下要求

  • 全局唯一性:在整个分布式系统中保证ID不重复
  • 高性能:能够快速生成ID,支持高并发场景
  • 高可用:避免单点故障,确保服务持续可用
  • 趋势递增:生成的ID大致呈递增趋势,便于数据库索引和分片
  • 安全性(可选) :不包含敏感信息,不易被推测和伪造

1. 基于INCR命令的简单自增ID

原理

这是最直接的Redis分布式ID实现方式,利用Redis的INCR命令原子性递增一个计数器,确保在分布式环境下ID的唯一性。

代码实现

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisSimpleIdGenerator {
    private final RedisTemplate<String, String> redisTemplate;
    private final String ID_KEY;
    
    public RedisSimpleIdGenerator(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.ID_KEY = "distributed:id:generator";
    }
    
    /**
     * 生成下一个ID
     * @return 唯一ID
     */
    public long nextId() {
        Long id = redisTemplate.opsForValue().increment(ID_KEY);
        if (id == null) {
            throw new RuntimeException("Failed to generate id");
        }
        return id;
    }
    
    /**
     * 为指定业务生成ID
     * @param bizTag 业务标签
     * @return 唯一ID
     */
    public long nextId(String bizTag) {
        String key = ID_KEY + ":" + bizTag;
        Long id = redisTemplate.opsForValue().increment(key);
        if (id == null) {
            throw new RuntimeException("Failed to generate id for " + bizTag);
        }
        return id;
    }
    
    /**
     * 获取当前ID值但不递增
     * @param bizTag 业务标签
     * @return 当前ID值
     */
    public long currentId(String bizTag) {
        String key = ID_KEY + ":" + bizTag;
        String value = redisTemplate.opsForValue().get(key);
        return value != null ? Long.parseLong(value) : 0;
    }
}

优缺点

优点

  • 实现极其简单,仅需一次Redis操作
  • ID严格递增,适合作为数据库主键
  • 支持多业务ID隔离

缺点

  • Redis单点故障会导致ID生成服务不可用
  • 主从切换可能导致ID重复
  • 无法包含业务含义

适用场景

  • 中小规模系统的自增主键生成
  • 对ID连续性有要求的业务场景
  • 单数据中心部署的应用

2. 基于Lua脚本的批量ID生成

原理

通过Lua脚本一次性获取一批ID,减少网络往返次数,客户端可在内存中顺序分配ID,显著提高性能。

代码实现

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Component
public class RedisBatchIdGenerator {
    private final RedisTemplate<String, String> redisTemplate;
    private final String ID_KEY = "distributed:batch:id";
    private final DefaultRedisScript<Long> batchIncrScript;
    
    // 批量获取的大小
    private final int BATCH_SIZE = 1000;
    
    // 本地计数器和锁
    private AtomicLong currentId = new AtomicLong(0);
    private AtomicLong endId = new AtomicLong(0);
    private final Lock lock = new ReentrantLock();
    
    public RedisBatchIdGenerator(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
        
        // 创建Lua脚本
        String scriptText = 
            "local key = KEYS[1] " +
            "local step = tonumber(ARGV[1]) " +
            "local currentValue = redis.call('incrby', key, step) " +
            "return currentValue";
        
        this.batchIncrScript = new DefaultRedisScript<>();
        this.batchIncrScript.setScriptText(scriptText);
        this.batchIncrScript.setResultType(Long.class);
    }
    
    /**
     * 获取下一个ID
     */
    public long nextId() {
        // 如果当前ID超过了分配范围,则重新获取一批
        if (currentId.get() >= endId.get()) {
            lock.lock();
            try {
                // 双重检查,防止多线程重复获取
                if (currentId.get() >= endId.get()) {
                    // 执行Lua脚本获取一批ID
                    Long newEndId = redisTemplate.execute(
                        batchIncrScript, 
                        Collections.singletonList(ID_KEY),
                        String.valueOf(BATCH_SIZE)
                    );
                    
                    if (newEndId == null) {
                        throw new RuntimeException("Failed to generate batch ids");
                    }
                    
                    // 设置新的ID范围
                    endId.set(newEndId);
                    currentId.set(newEndId - BATCH_SIZE);
                }
            } finally {
                lock.unlock();
            }
        }
        
        // 分配下一个ID
        return currentId.incrementAndGet();
    }
    
    /**
     * 为指定业务生成ID
     */
    public long nextId(String bizTag) {
        // 实际项目中应该为每个业务标签维护独立的计数器和范围
        // 这里简化处理,仅使用不同的Redis key
        String key = ID_KEY + ":" + bizTag;
        
        Long newEndId = redisTemplate.execute(
            batchIncrScript, 
            Collections.singletonList(key),
            String.valueOf(1)
        );
        
        return newEndId != null ? newEndId : -1;
    }
}

优缺点

优点

  • 显著减少Redis网络请求次数
  • 客户端缓存ID段,大幅提高性能
  • 降低Redis服务器压力
  • 支持突发流量处理

缺点

  • 实现复杂度增加
  • 服务重启可能导致ID段浪费

适用场景

  • 高并发系统,需要极高ID生成性能的场景
  • 对ID连续性要求不严格的业务
  • 能容忍小部分ID浪费的场景

3. 基于Redis的分段式ID分配(号段模式)

原理

号段模式是一种优化的批量ID生成方案,通过预分配号段(ID范围)减少服务间竞争,同时引入双Buffer机制提高可用性。

代码实现

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Component
public class RedisSegmentIdGenerator {
    private final RedisTemplate<String, String> redisTemplate;
    private final String SEGMENT_KEY = "distributed:segment:id";
    private final DefaultRedisScript<Long> segmentScript;
    
    // 号段大小
    private final int SEGMENT_STEP = 1000;
    // 加载因子,当前号段使用到这个百分比时就异步加载下一个号段
    private final double LOAD_FACTOR = 0.7;
    
    // 存储业务号段信息的Map
    private final Map<String, SegmentBuffer> businessSegmentMap = new ConcurrentHashMap<>();
    
    public RedisSegmentIdGenerator(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
        
        // 创建Lua脚本
        String scriptText = 
            "local key = KEYS[1] " +
            "local step = tonumber(ARGV[1]) " +
            "local value = redis.call('incrby', key, step) " +
            "return value";
        
        this.segmentScript = new DefaultRedisScript<>();
        this.segmentScript.setScriptText(scriptText);
        this.segmentScript.setResultType(Long.class);
    }
    
    /**
     * 获取下一个ID
     * @param bizTag 业务标签
     * @return 唯一ID
     */
    public long nextId(String bizTag) {
        // 获取或创建号段缓冲区
        SegmentBuffer buffer = businessSegmentMap.computeIfAbsent(
            bizTag, k -> new SegmentBuffer(bizTag));
        
        return buffer.nextId();
    }
    
    /**
     * 内部号段缓冲区类,实现双Buffer机制
     */
    private class SegmentBuffer {
        private String bizTag;
        private Segment[] segments = new Segment[2]; // 双Buffer
        private volatile int currentPos = 0; // 当前使用的segment位置
        private Lock lock = new ReentrantLock();
        private volatile boolean isLoadingNext = false; // 是否正在异步加载下一个号段
        
        public SegmentBuffer(String bizTag) {
            this.bizTag = bizTag;
            segments[0] = new Segment(0, 0);
            segments[1] = new Segment(0, 0);
        }
        
        /**
         * 获取下一个ID
         */
        public long nextId() {
            // 获取当前号段
            Segment segment = segments[currentPos];
            
            // 如果当前号段为空或已用完,切换到另一个号段
            if (!segment.isInitialized() || segment.getValue() > segment.getMax()) {
                lock.lock();
                try {
                    // 双重检查当前号段状态
                    segment = segments[currentPos];
                    if (!segment.isInitialized() || segment.getValue() > segment.getMax()) {
                        // 切换到另一个号段
                        currentPos = (currentPos + 1) % 2;
                        segment = segments[currentPos];
                        
                        // 如果另一个号段也未初始化或已用完,则同步加载
                        if (!segment.isInitialized() || segment.getValue() > segment.getMax()) {
                            loadSegmentFromRedis(segment);
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }
            
            // 检查是否需要异步加载下一个号段
            long value = segment.incrementAndGet();
            if (value > segment.getMin() + (segment.getMax() - segment.getMin()) * LOAD_FACTOR
                    && !isLoadingNext) {
                isLoadingNext = true;
                // 异步加载下一个号段
                new Thread(() -> {
                    Segment nextSegment = segments[(currentPos + 1) % 2];
                    loadSegmentFromRedis(nextSegment);
                    isLoadingNext = false;
                }).start();
            }
            
            return value;
        }
        
        /**
         * 从Redis加载号段
         */
        private void loadSegmentFromRedis(Segment segment) {
            String key = SEGMENT_KEY + ":" + bizTag;
            
            // 执行Lua脚本获取号段最大值
            Long max = redisTemplate.execute(
                segmentScript, 
                Collections.singletonList(key),
                String.valueOf(SEGMENT_STEP)
            );
            
            if (max == null) {
                throw new RuntimeException("Failed to load segment from Redis");
            }
            
            // 设置号段范围
            long min = max - SEGMENT_STEP + 1;
            segment.setMax(max);
            segment.setMin(min);
            segment.setValue(min - 1); // 设置为min-1,第一次incrementAndGet返回min
            segment.setInitialized(true);
        }
    }
    
    /**
     * 内部号段类,存储号段的范围信息
     */
    private class Segment {
        private long min; // 最小值
        private long max; // 最大值
        private AtomicLong value; // 当前值
        private volatile boolean initialized; // 是否已初始化
        
        public Segment(long min, long max) {
            this.min = min;
            this.max = max;
            this.value = new AtomicLong(min);
            this.initialized = false;
        }
        
        public long getValue() {
            return value.get();
        }
        
        public void setValue(long value) {
            this.value.set(value);
        }
        
        public long incrementAndGet() {
            return value.incrementAndGet();
        }
        
        public long getMin() {
            return min;
        }
        
        public void setMin(long min) {
            this.min = min;
        }
        
        public long getMax() {
            return max;
        }
        
        public void setMax(long max) {
            this.max = max;
        }
        
        public boolean isInitialized() {
            return initialized;
        }
        
        public void setInitialized(boolean initialized) {
            this.initialized = initialized;
        }
    }
}

优缺点

优点

  • 双Buffer设计,高可用性
  • 异步加载下一个号段,性能更高
  • 大幅降低Redis访问频率
  • 即使Redis短暂不可用,仍可分配一段时间的ID

缺点

  • 实现复杂,代码量大
  • 多实例部署时,各实例获取的号段不连续
  • 重启服务时号段内的ID可能浪费
  • 需要在内存中维护状态

适用场景

  • 对ID生成可用性要求高的业务
  • 需要高性能且多服务器部署的分布式系统

4. 性能对比与选型建议

策略 性能 可用性 ID长度 实现复杂度 单调递增
INCR命令 ★★★☆☆ ★★☆☆☆ 递增整数 严格递增
Lua批量生成 ★★★★★ ★★★☆☆ 递增整数 批次内递增
分段式ID ★★★★★ ★★★★☆ 递增整数 段内递增

5. 实践优化技巧

1. Redis高可用配置

// 配置Redis哨兵模式,提高可用性
@Bean
public RedisConnectionFactory redisConnectionFactory() {
    RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
        .master("mymaster")
        .sentinel("127.0.0.1", 26379)
        .sentinel("127.0.0.1", 26380)
        .sentinel("127.0.0.1", 26381);
    
    return new LettuceConnectionFactory(sentinelConfig);
}

2. ID预热策略

// 系统启动时预热ID生成器
@PostConstruct
public void preWarmIdGenerator() {
    // 预先获取一批ID,确保系统启动后立即可用
    for (int i = 0; i < 10; i++) {
        try {
            segmentIdGenerator.nextId("order");
            segmentIdGenerator.nextId("user");
            segmentIdGenerator.nextId("payment");
        } catch (Exception e) {
            log.error("Failed to pre-warm ID generator", e);
        }
    }
}

3. 降级策略

// Redis不可用时的降级策略
public long nextIdWithFallback(String bizTag) {
    try {
        return segmentIdGenerator.nextId(bizTag);
    } catch (Exception e) {
        log.warn("Failed to get ID from Redis, using local fallback", e);
        // 使用本地UUID或其他替代方案
        return Math.abs(UUID.randomUUID().getMostSignificantBits());
    }
}

6. 结论

选择合适的分布式ID生成策略时,需要综合考虑系统规模、性能需求、可靠性要求和实现复杂度。无论选择哪种方案,都应注重高可用性设计,增加监控和预警机制,确保ID生成服务的稳定运行。

在实践中,可以基于业务需求对这些方案进行组合和优化,例如为不同业务选择不同策略,或者在ID中嵌入业务标识等,打造更适合自身系统的分布式ID生成解决方案。


网站公告

今日签到

点亮在社区的每一天
去签到