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

在分布式系统设计中,全局唯一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生成解决方案。

相关推荐
懒羊羊大王呀6 分钟前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
千|寻15 分钟前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱29 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯35 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响38 分钟前
枚举在实际开发中的使用小Tips
后端
wuhunyu43 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi43 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
John Song2 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员
苏三说技术2 小时前
给你1亿的Redis key,如何高效统计?
后端