Redis 分布式全局唯一 ID 生成方案:时间戳 + Redis 自增

📅 发布时间:2026/7/3 21:13:36 👁️ 浏览次数:
Redis 分布式全局唯一 ID 生成方案:时间戳 + Redis 自增
Redis 分布式全局唯一 ID 生成方案时间戳 Redis 自增这篇文章介绍一种在分布式系统中常见的全局唯一 ID 生成方式使用时间戳与Redis 原子自增序列进行组合通过位运算拼接成一个long类型 ID。该方案实现简单、性能较高、趋势递增尤其适合订单、优惠券、支付流水等业务场景。一、为什么我们需要自己生成 ID在业务系统里很多对象都需要唯一标识例如订单 ID支付单号优惠券领取记录 ID秒杀订单 ID用户业务编号最容易想到的做法是数据库自增主键但在分布式场景下它往往不够理想。1. 数据库自增 ID 的问题数据库自增虽然简单但有几个明显缺点依赖单库扩展性一般高并发下容易成为瓶颈不适合多服务、多节点同时生成业务上有时不希望直接暴露连续主键2. UUID 的问题UUID 也能保证唯一但它也有不足字符串过长不利于存储和索引无序数据库索引性能不够友好可读性较差因此很多系统会选择一种折中方案生成一个 long 型、全局唯一、趋势递增、适合分布式场景的业务 ID。二、这套方案的核心思路这套方案的核心思想很简单ID 时间戳部分 序列号部分其中时间戳部分用于体现大致时间顺序序列号部分用于保证同一时刻生成多个 ID 时仍不重复而序列号不是在本地内存里递增而是交给Redis来完成因为 Redis 的INCR操作具有原子性非常适合在分布式环境下生成全局递增序号。三、示例代码下面是一段典型实现publiclongnextId(StringkeyPrefix){// 1. 生成时间戳LocalDateTimenowLocalDateTime.now();longnowSecondnow.toEpochSecond(ZoneOffset.UTC);longtimestampnowSecond-BEGIN_TIMESTAMP;// 2. 生成序列号Stringdatenow.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));longcountstringRedisTemplate.opsForValue().increment(icr:keyPrefix:date);// 3. 拼接并返回returntimestampCOUNT_BITS|count;}从方法名可以看出这段代码的作用就是生成下一个唯一 ID。四、逐步拆解这段代码1. 获取当前时间LocalDateTimenowLocalDateTime.now();这里获取当前时间。例如当前可能是2025-03-08 15:30:202. 转换为秒级时间戳longnowSecondnow.toEpochSecond(ZoneOffset.UTC);这一步会将当前时间转换为 Unix 时间戳单位秒。也就是从1970-01-01 00:00:00到现在经过的总秒数。3. 减去自定义起始时间longtimestampnowSecond-BEGIN_TIMESTAMP;这里并没有直接使用完整的 Unix 时间戳而是减去了一个固定起点BEGIN_TIMESTAMP。例如privatestaticfinallongBEGIN_TIMESTAMP1640995200L;这样做的好处有两个减少时间戳数值节省位数更方便通过位运算拼接到高位中换句话说这里保存的是相对时间戳而不是完整时间戳。4. 生成日期字符串Stringdatenow.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));得到类似这样的结果2025:03:08为什么要保留“天”这个维度因为后面 Redis 的自增 key 会按天区分也就是每天使用一个新的计数器。5. 使用 Redis 生成当天自增序列longcountstringRedisTemplate.opsForValue().increment(icr:keyPrefix:date);这句是整个方案里最关键的一步。它本质上相当于执行了 Redis 命令INCR icr:order:2025:03:08如果keyPrefix order那么第一次调用返回1第二次调用返回2第三次调用返回3……这样就能在分布式环境下拿到一个全局唯一的递增序列号。五、为什么要加keyPrefixkeyPrefix用来区分业务类型。例如订单order用户user优惠券voucher最终 Redis key 可能长这样icr:order:2025:03:08 icr:user:2025:03:08 icr:voucher:2025:03:08这样不同业务之间的计数器互不影响。六、为什么要按天分隔计数器如果 Redis key 不带日期那么计数器会一直增长长期运行后数值会越来越大。按天区分的好处是每天从一个新的计数器开始避免序列号无限膨胀更容易管理和排查问题即使第二天计数器又从 1 开始也不会发生重复因为高位的时间戳已经变化了。七、最终 ID 是如何拼接的returntimestampCOUNT_BITS|count;这句代码的含义是把时间戳左移若干位给低位留出空间存放序列号使用按位或|将序列号拼进去假设COUNT_BITS32那么就意味着高位存储时间戳低 32 位存储 Redis 自增序列号可以抽象理解为[时间戳][序列号]八、位结构示意如果按照常见设计一个long的结构可以表示为0 | 时间戳 | 序列号更具体一点1 bit符号位固定为 031 bit时间戳32 bit序列号这样最终得到的就是一个正数类型的long。九、举一个简单例子假设timestamp 1000count 25COUNT_BITS 32那么longid(1000L32)|25;这里的含义就是把1000放在高位把25放在低位组合成一个完整的 long 值由于时间戳和序列号的组合是唯一的所以最终 ID 也唯一。十、为什么这种方式能保证唯一唯一性来自两部分共同约束1. 不同时刻生成只要时间戳不同最终 ID 一定不同。2. 同一时刻生成多个 ID哪怕时间戳相同只要 Redis 自增得到的count不同最终 ID 也不同。因此时间戳负责区分时间范围Redis 自增负责区分同一时间内的并发请求。两者配合起来就可以保证全局唯一。十一、为什么它适合分布式系统因为 Redis 的INCR操作是原子的。假设有两个服务实例同时生成订单 ID实例 A 执行一次INCR返回1001实例 B 执行一次INCR返回1002即使它们部署在不同机器上也不会拿到重复值。这就是 Redis 在这个方案中的价值将“全局递增序列”的生成交给一个天然支持原子操作的中间件。十二、这种方案的优点1. 全局唯一借助 Redis 自增可以在分布式环境下保证唯一性。2. 趋势递增由于高位是时间戳所以整体趋势上是递增的。3. long 型更适合数据库索引相比 UUID 字符串long更节省存储空间也更利于索引。4. 实现简单相较于更复杂的分布式 ID 算法这种方式逻辑清晰容易落地。5. 业务隔离方便通过keyPrefix可以轻松区分不同业务类型。十三、这种方案的局限性任何方案都不是完美的这种实现也有自己的边界。1. 依赖 Redis如果 Redis 不可用那么 ID 生成就会受到影响。2. 每次生成都需要一次远程调用相比完全本地生成 ID 的算法这种方式会多一次网络开销。3. 位数设计需要提前规划例如时间戳占多少位序列号占多少位能支撑多少年每天最多支持多大量级的自增序列这些都需要结合业务体量提前设计。十四、它和雪花算法有什么区别很多人看到这种写法会联想到 Snowflake雪花算法。它们确实很像因为都采用了时间戳序列号位运算拼接但两者并不完全一样。雪花算法通常包含时间戳机房 ID机器 ID毫秒内序列号本文这种方案包含时间戳Redis 全局自增序列号也就是说雪花算法依赖机器号区分节点而本文方案依赖 Redis 统一发号。对比来看本文方案的优势逻辑更直观不需要维护机器号和机房号分布式唯一性更容易理解本文方案的不足强依赖 Redis远程调用成本高于本地雪花算法十五、适合哪些业务场景这种方式非常适合下面这些场景订单 ID秒杀订单 ID支付流水号优惠券领取记录 ID业务侧唯一编号这些场景通常都有共同特点并发量高需要全局唯一最好趋势递增不适合直接使用数据库自增主键十六、实现时的注意事项1. 注意时区问题代码里如果这样写LocalDateTimenowLocalDateTime.now();longnowSecondnow.toEpochSecond(ZoneOffset.UTC);需要确保你的系统时区和转换逻辑是一致的。更稳妥的方式通常是直接使用longnowSecondInstant.now().getEpochSecond();这样语义会更清晰一些。2. 序列号位数要足够如果低位留给序列号的 bit 太少在高并发场景下可能不够用。例如 32 bit 的序列号理论上已经非常大但实际设计时仍然应结合业务规模评估。3. Redis key 可以考虑设置过期时间由于 key 按天生成可以根据业务需要给这类自增 key 设置较长的过期时间避免无限积累。十七、一个更完整的示例下面给出一个更完整的 Java 示例便于理解ComponentpublicclassRedisIdWorker{privatestaticfinallongBEGIN_TIMESTAMP1640995200L;privatestaticfinalintCOUNT_BITS32;ResourceprivateStringRedisTemplatestringRedisTemplate;publiclongnextId(StringkeyPrefix){// 1. 当前时间戳LocalDateTimenowLocalDateTime.now();longnowSecondnow.toEpochSecond(ZoneOffset.UTC);longtimestampnowSecond-BEGIN_TIMESTAMP;// 2. 当天序列号Stringdatenow.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));longcountstringRedisTemplate.opsForValue().increment(icr:keyPrefix:date);// 3. 拼接返回return(timestampCOUNT_BITS)|count;}}这个工具类可以作为通用组件注入到订单、优惠券、支付等业务模块中统一使用。十八、总结本文介绍了一种非常实用的分布式 ID 生成方式利用相对时间戳作为高位利用 Redis 原子自增序列作为低位再通过位运算拼接成一个 long 型全局唯一 ID。它的特点是全局唯一趋势递增存储友好实现简单分布式可用如果你的项目已经引入 Redis并且希望快速实现一套可靠的业务 ID 生成机制那么这种方案会是一个非常合适的选择。十九、参考一句话总结可以把它理解成先用时间区分“大范围”再用 Redis 自增区分“同一时间内的顺序”最后把两部分合成一个 long 值。这就是这套 ID 生成方案的本质。