欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

基于Redis生成分布式全局唯一ID的3种策略,

来源: javaer 分享于  点击 3127 次 点评:154

基于Redis生成分布式全局唯一ID的3种策略,


目录
  • 分布式ID的核心需求
  • 1. 基于INCR命令的简单自增ID
    • 原理
    • 代码实现
    • 优缺点
    • 适用场景
  • 2. 基于Lua脚本的批量ID生成
    • 原理
    • 代码实现
    • 优缺点
    • 适用场景
  • 3. 基于Redis的分段式ID分配(号段模式)
    • 原理
    • 代码实现
    • 优缺点
    • 适用场景
  • 4. 性能对比与选型建议
    • 5. 实践优化技巧
      • 1. Redis高可用配置
      • 2. ID预热策略
      • 3. 降级策略
    • 6. 结论

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

      到此这篇关于基于Redis生成分布式全局唯一ID的3种策略的文章就介绍到这了,更多相关Redis生成分布式全局唯一ID内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

      您可能感兴趣的文章:
      • 分布式使用Redis实现数据库对象自增主键ID
      • redis实现分布式全局唯一id的示例代码
      • redis分布式ID解决方案示例详解
      • Redis生成分布式系统全局唯一ID的实现
      • 基于Redis实现分布式单号及分布式ID(自定义规则生成)
      相关栏目:

      用户点评