【分布式利器:分布式ID】6、中间件方案:Redis/ZooKeeper分布式ID实现

系列导读

上一篇的UUID方案适合无依赖场景,但如果你的系统已经部署了Redis(缓存)或ZooKeeper(服务注册中心),没必要再引入雪花算法、号段模式等新方案------直接复用现有中间件就能实现分布式ID,减少系统依赖和维护成本。

本文详解Redis和ZooKeeper的分布式ID实现方案,附实战代码,帮你快速复用现有组件落地。

一、适用场景

  • 系统已部署Redis/ZooKeeper(避免重复引入中间件);
  • 中低并发场景(Redis:QPS万级+;ZooKeeper:QPS千级);
  • 需ID有序(支持排序、分页);
  • 代表业务:
    • Redis:电商优惠券ID、活动ID、中小型系统订单ID;
    • ZooKeeper:配置中心标识、分布式锁关联ID、低并发业务ID。

二、方案1:Redis分布式ID(基于INCR原子操作)

1. 核心原理

Redis的INCR/INCRBY命令是原子操作,支持跨客户端递增,天然适合生成自增ID:

  • 原理:用Redis键(如order_id_generator)存储当前最大ID,每次生成ID时执行INCR key,返回递增后的ID;
  • 唯一性:Redis是单线程模型,INCR命令原子执行,不会出现并发冲突;
  • 有序性:ID严格递增,支持排序和分页。

2. 进阶优化:避免ID重置+增加业务前缀

  • 问题:Redis重启后,键值会丢失,ID会从1重新开始,导致重复;
  • 解决方案:
    1. Redis开启持久化(RDB+AOF),避免重启后数据丢失;
    2. ID中加入时间戳前缀(如20240520_1001),即使Redis重置,也不会与历史ID重复。

3. 实战代码(Spring Boot+Redis)

3.1 核心依赖(pom.xml)
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 Redis配置(application.yml)
yaml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:  # 若Redis有密码则填写
    database: 0
    lettuce:
      pool:
        max-active: 8 # 最大连接数
        max-idle: 8  # 最大空闲连接数
3.3 核心代码实现
java 复制代码
@Service
public class RedisIdGeneratorService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 业务前缀(避免不同业务ID冲突)
    private static final String PREFIX_ORDER = "ORDER_";
    private static final String PREFIX_COUPON = "COUPON_";

    // Redis键(按业务区分)
    private static final String REDIS_KEY_ORDER = "id_generator:order";
    private static final String REDIS_KEY_COUPON = "id_generator:coupon";

    // 生成订单ID(时间戳前缀+Redis自增ID)
    public String generateOrderId() {
        return generateId(PREFIX_ORDER, REDIS_KEY_ORDER);
    }

    // 生成优惠券ID
    public String generateCouponId() {
        return generateId(PREFIX_COUPON, REDIS_KEY_COUPON);
    }

    // 通用生成逻辑
    private String generateId(String prefix, String redisKey) {
        // 1. 获取当前日期前缀(yyyyMMdd,如20240520)
        String datePrefix = new SimpleDateFormat("yyyyMMdd").format(new Date());
        // 2. Redis原子递增(步长=1)
        Long incrId = redisTemplate.opsForValue().increment(redisKey, 1);
        // 3. 拼接ID(前缀+日期+6位自增ID,不足6位补0)
        String suffix = String.format("%06d", incrId);
        return prefix + datePrefix + "_" + suffix;
    }

    // 测试方法
    public static void main(String[] args) {
        // 模拟生成订单ID
        RedisIdGeneratorService generator = new RedisIdGeneratorService();
        String orderId = generator.generateOrderId();
        System.out.println("订单ID:" + orderId); // 输出示例:ORDER_20240520_000101
    }
}
3.4 调用示例
java 复制代码
@Controller
@RequestMapping("/coupon")
public class CouponController {
    @Autowired
    private RedisIdGeneratorService idGeneratorService;

    @PostMapping("/create")
    public ResponseEntity<?> createCoupon() {
        // 生成优惠券ID(Redis方案)
        String couponId = idGeneratorService.generateCouponId();
        // 后续优惠券创建逻辑
        return ResponseEntity.ok("优惠券创建成功,ID:" + couponId);
    }
}

4. 优点&缺点

  • 优点:
    1. 复用现有Redis组件,无需额外部署;
    2. 原子操作,ID唯一有序;
    3. 实现简单,支持业务前缀和日期前缀,可读性好;
    4. 性能高(Redis单机QPS万级+,支持主从复制)。
  • 缺点:
    1. 依赖Redis高可用(Redis宕机后无法生成ID,需主从+哨兵);
    2. Redis持久化配置不当,重启后ID可能重复;
    3. 高并发场景(10万+QPS)下,Redis可能成为瓶颈。

三、方案2:ZooKeeper分布式ID(基于持久顺序节点)

1. 核心原理

ZooKeeper支持创建"持久顺序节点",每次创建节点时,ZooKeeper会自动在节点名称后追加一个有序编号(如id_order_00000001),截取该编号作为分布式ID:

  • 原理:
    1. 创建父节点(如/id_generator/order);
    2. 每次生成ID时,在父节点下创建临时顺序节点(如/id_generator/order/temp_);
    3. 获取节点名称,截取末尾的有序编号(如00000001)作为ID;
    4. 删除临时节点(避免节点过多占用资源)。
  • 唯一性:ZooKeeper保证顺序节点的编号全局唯一;
  • 有序性:编号递增,支持排序。

2. 实战代码(Java+ZooKeeper)

2.1 核心依赖(pom.xml)
xml 复制代码
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
2.2 核心代码实现
java 复制代码
public class ZkIdGenerator {
    // ZooKeeper连接地址
    private static final String ZK_CONNECT_STR = "127.0.0.1:2181";
    // 会话超时时间(3秒)
    private static final int SESSION_TIMEOUT = 3000;
    // 父节点路径(按业务区分)
    private static final String PARENT_NODE_ORDER = "/id_generator/order";

    private ZooKeeper zk;

    // 初始化ZooKeeper连接
    public ZkIdGenerator() throws IOException, InterruptedException {
        zk = new ZooKeeper(ZK_CONNECT_STR, SESSION_TIMEOUT, event -> {
            // 监听连接状态
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                synchronized (this) {
                    this.notify();
                }
            }
        });
        synchronized (this) {
            this.wait(); // 等待连接建立
        }
        // 创建父节点(不存在则创建)
        createParentNode(PARENT_NODE_ORDER);
    }

    // 创建父节点
    private void createParentNode(String parentNode) {
        try {
            if (zk.exists(parentNode, false) == null) {
                // 持久节点(PERSISTENT)
                zk.create(parentNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException("创建ZooKeeper父节点失败", e);
        }
    }

    // 生成分布式ID
    public Long generateId() throws Exception {
        // 1. 创建临时顺序节点(EPHEMERAL_SEQUENTIAL)
        String tempNode = PARENT_NODE_ORDER + "/temp_";
        String nodePath = zk.create(tempNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        // 2. 截取有序编号(如"/id_generator/order/temp_0000000123" → "0000000123")
        String sequence = nodePath.substring(nodePath.lastIndexOf("_") + 1);
        // 3. 转换为Long型ID
        Long id = Long.parseLong(sequence);
        // 4. 删除临时节点(可选,避免节点过多)
        zk.delete(nodePath, -1);
        return id;
    }

    // 测试方法
    public static void main(String[] args) throws Exception {
        ZkIdGenerator generator = new ZkIdGenerator();
        Long orderId = generator.generateId();
        System.out.println("ZooKeeper生成订单ID:" + orderId); // 输出示例:123
    }
}

3. 优点&缺点

  • 优点:
    1. 复用现有ZooKeeper组件,无需额外部署;
    2. 天然支持分布式协同,ID唯一有序;
    3. 临时顺序节点自动删除,无资源残留。
  • 缺点:
    1. 性能一般(ZooKeeper QPS千级,不适合高并发);
    2. 部署复杂(ZooKeeper需集群部署,保证高可用);
    3. 网络开销大(每次生成ID需创建节点、删除节点,涉及网络通信)。

四、避坑指南:2个方案的关键问题解决

1. Redis方案避坑

  • 坑1:Redis主从切换导致ID重复;
    解决方案:用Redis哨兵或集群模式,确保主从切换时数据同步完成,避免从库未同步最新自增ID就提供服务。
  • 坑2:Redis持久化配置不当导致ID重置;
    解决方案:开启AOF持久化(appendonly yes),并设置appendfsync everysec(每秒同步),确保自增ID数据不丢失。

2. ZooKeeper方案避坑

  • 坑1:ZooKeeper集群脑裂导致ID重复;
    解决方案:部署ZooKeeper集群(至少3台节点),配置合理的选举参数(如tickTime=2000),避免脑裂。
  • 坑2:节点过多导致ZooKeeper性能下降;
    解决方案:用临时顺序节点(自动删除),或定时清理历史节点。

五、Redis vs ZooKeeper方案对比

特性 Redis分布式ID ZooKeeper分布式ID
性能 高(QPS万级+) 低(QPS千级)
实现复杂度 简单(原子操作) 中等(节点创建/删除)
部署成本 低(单机即可,集群可选) 高(需集群部署)
适用并发 中高并发 低并发
典型场景 优惠券ID、活动ID、中小型订单ID 配置中心标识、分布式锁关联ID

实战Tips

  1. 优先复用Redis:如果系统已部署Redis,优先选Redis方案(性能高、实现简单);
  2. Redis ID前缀:建议加入业务前缀+日期前缀,避免ID重复和泄露敏感信息;
  3. ZooKeeper慎用高并发:ZooKeeper适合低并发场景,高并发下建议选雪花算法或号段模式;
  4. 高可用配置:Redis/ZooKeeper作为ID生成组件,必须保证高可用(集群/主从),否则会阻塞业务。

下一篇预告

如果你的系统已经使用了分布式数据库(如TiDB、OceanBase),还有更简单的方案------直接使用分布式数据库自带的全局ID功能,无需额外开发。

下一篇我们拆解"分布式数据库方案":TiDB和OceanBase的全局ID配置,帮你无缝集成分布式ID!

你在项目中用Redis还是ZooKeeper生成ID?评论区聊聊~

相关推荐
j***51894 小时前
Redis 安装及配置教程(Windows)【安装】
数据库·windows·redis
A***F1577 小时前
Redis开启远程访问
数据库·redis·缓存
8***23557 小时前
【Golang】——Gin 框架中间件详解:从基础到实战
中间件·golang·gin
7***37457 小时前
后端中间件趋势:消息队列与缓存的新发展
缓存·中间件
j***29488 小时前
Redis 设置密码(配置文件、docker容器、命令行3种场景)
数据库·redis·docker
bailaoshi6668 小时前
reactor-kafka无traceId
分布式·kafka
瑞思蕊萌8 小时前
redis实战篇--完结篇
数据库·redis·缓存
Monly219 小时前
Java八股文:Redis篇
java·开发语言·redis
X***C8629 小时前
Redis开启远程连接
数据库·redis·缓存