【Redis】利用Redis构造全局唯一ID

参考笔记:

https://blog.csdn.net/nj0128/article/details/158810077https://blog.csdn.net/nj0128/article/details/158810077


1.问题提出

商业项目通常都会有订单表,当用户查看个人订单时,会返回订单信息(包括订单号)给用户,订单表如果使用数据库 自增ID策略 就会存在一些问题:

**① id的规律性太明显:**用户或商业对手很容猜测出敏感信息,比如商城在一天的时间内,产出了多少个订单

随着项目规模越来越大**,Mysql** 的单表容量不宜超过 500W ,数据量过大则必须进行分表存储。但 Mysql 每张表都是计算自己的自增长,多张表情况下自增的ID一定会出现重复,但ID是需要具有唯一性的,以区分不同的订单

2. 生成全局唯一ID的要求

2.1 id生成规则的硬性要求

**① 全局唯一:**分布式多服务、多节点、多线程并发下永不重复

② 递增性: 生成的 ID 通常会作为主键存储到数据库中,有顺序的 ID 可以保证 Mysql 的写入性能

③ 安全性: 生成的 ID 规律性不能太明显,不能是简单的递增

④ 含时间戳: 通过 ID 的时间戳可以分析出生成该 ID 的时间,帮助定位排查问题


2.2 id生成系统的可用性要求

**① 高可用:**单点故障不影响服务

② 低延迟: 生成 ID 的速度要足够快

③ 高QPS: 数万个生成 ID 的请求同时过来,仍能生成不重复的 ID


3. 利用Redis生成全局唯一ID

3.1 介绍

生成全局唯一ID的方案很多,Redis是最优解之一,主要原因如下:

  • 单线程串行执行模型 :所有命令按到达顺序排队执行,天然无并发冲突

  • 原子自增命令: INCR/INCRBY 是 Redis 原生原子操作,执行过程不中断

  • 高性能:纯内存操作,单节点每秒可处理 10万+ 次自增请求

Redis 的 INCR key 命令执行过程:

  1. 查找 key 对应的内存地址

  2. 将值加 1

  3. 返回新值

整个过程是原子的:即使 1000 个客户端同时对同一个 key 执行 INCR,Redis 也会将它们排成队列,一个接一个执行,绝不会出现 "两个线程同时读到同一个值,同时加 1,返回相同结果" 的情况

这是 Redis 实现全局唯一 ID 的根本保障

3.2 原理 + 代码实现

为了增加 ID的安全性,我们不直接使用 Redis 自增的数值,而是拼接一些其它信息,采用结构化 ID方案,这也是很多全局ID生成器采用的方案

Redis 构造的 ID 组成部分如下:

符号位: 1bit,永远为0,因为 ID 一定是正数

时间戳: 31bit,以为单位,支持使用69年

序列号: 32bit,每秒内的计数器,也就是每秒支持产生 个不同 ID 。作用:当时间戳部分重复时,可以通过序列号来区分;

代码实现(来自《黑马点评》):

java 复制代码
/**
 * 使用Redis生成全局唯一ID
 */

@Component
public class RedisIdWorker {
    /**
     * 自定义一个开始时间(以秒为单位)
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数(支持每秒内生成2^32个不同的ID)
     */
    private static final int COUNT_BITS = 32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // keyPrefix:相关业务前缀
    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now(); // 获取当前时间
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC); // 转换为秒
        long timestamp = nowSecond - BEGIN_TIMESTAMP; // 时间戳 = 当前时间 - 开始时间

        // 2.生成序列号
        // 2.1.获取当前日期(具体到天),按日期拆分Key,每天生成一个新的Key
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

3.3 代码分析

(1)获取当前时间

java 复制代码
LocalDateTime now = LocalDateTime.now();

这里获取当前时间。例如当前可能是:2026-05-05 12:34:40

java 复制代码
2026-05-05T12:34:40.434878500

(2)转换为秒级

java 复制代码
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);

这一步是先将时间转换成世界统一时间(UTC 0 时区),再将其转换为秒

也就是从 1970-01-01 00:00:00 到现在经过的总秒数


(3)减去自定义的开始时间

java 复制代码
long timestamp = nowSecond - BEGIN_TIMESTAMP;

时间戳 = 现在时间 - 自定义开始时间,有了时间戳我们可以分析出生成该ID的具体时间


(4)生成日期字符串

java 复制代码
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));

得到类似这样的结果:

java 复制代码
2026:05:06

为什么要这样做?

因为后面 Redis 的自增 key 可以按天区分,也就是每天使用一个新的计数器


(5)使用 Redis 生成当天自增序列

java 复制代码
long count = stringRedisTemplate.opsForValue()
        .increment("icr:" + keyPrefix + ":" + date);

这句是整个方案里最关键的一步。它本质上相当于执行了 Redis 命令:

java 复制代码
INCR icr:order:2025:05:06

如果 keyPrefix = "order",那么:

  • 第一次调用返回1
  • 第二次调用返回2
  • ....
  • 第n次调用返回n

这样就能在分布式环境下拿到一个全局唯一的递增序列号


3.4 为什么要加 KeyPrefix?

keyPrefix 可以用来区分业务类型

例如:

  • 订单:order
  • 用户:user
  • 优惠券:voucher

最终 Redis key 可能长这样:

java 复制代码
icr:order:2026:05:08
icr:user:2026:05:08
icr:voucher:2026:05:08

这样可以使得不同业务之间的计数器互不影响


3.3 为什么要按天拆分Key?

如果 Redis key 不带日期,那么计数器会一直增长,长期运行后数值会越来越大

按天拆分的好处是:

  • 每天生成一个新的 Redis key,例如 icr :order :20260506,即每天从一个新的计数器开始
  • 避免序列号无限膨胀
  • 更容易管理和排查问题

即新的一天,计数器又从 1 开始,不会与历史ID发生重复,因为高位的时间戳已经变化了


3.4 最终ID是如何拼接的?

java 复制代码
return timestamp << COUNT_BITS | count;

这句代码的含义是:

  1. 把时间戳左移 32 位,给低位留出 32 位的空间
  2. 此时低位是 32 位的 0000.....
  3. 使用按位或 | 运算将序列号拼进去(0 | 1 = 1,0 | 0 = 0,所以低位的32位正好是序列号本身)

最终返回的是 long 类型数据, long 类型正好是 8字节 = 64bit


3.5 为什么这种方式能保证ID唯一?

唯一性来自两部分共同约束:

1. 不同时刻生成

只要时间戳不同,最终 ID 一定不同

2. 同一时刻生成多个 ID

哪怕时间戳相同,只要 Redis 自增得到的 count 不同,最终 ID 也不同。而且 Redis 的 INCR key 命令具有原子性

因此:

时间戳负责区分时间范围,Redis 自增负责区分同一时间内的并发请求

两者配合起来,就可以保证全局唯一


3.6 为什么它适合分布式系统?

因为 Redis 的 INCR 操作具有原子性

假设有两个服务实例同时生成订单 ID:

  • 实例 A 执行一次 INCR,返回 1001
  • 实例 B 执行一次 INCR,返回 1002

即使它们部署在不同机器上,也不会拿到重复值

这就是 Redis 在这个方案中的价值

3.7 该方案的优点和局限性

优点

实现简单

原子性强,无重复

支付跨服务共享

局限性

① 单点故障问题:Redis 单点故障会导致 ID 生成不可用,必须部署主从集群 + 哨兵模式,确保Redis 宕机时自动切换

相比完全本地生成 ID 的算法,这种方式会多一次网络开销

位数设计需要提前规划,这些都需要结合业务体量提前设计:

  • 时间戳占多少位
  • 序列号占多少位
  • 能支撑多少年
  • 每天最多支持多大量级的自增序列

4. 其他生成全局唯一ID方案

后续再补充...

4.1 UUID

UUID最大的问题是生成的ID是无序的,作为主键会导致索引分裂,大大降低索引的性能

补充.....

4.2 雪花算法

雪花算法和Redi实现全局唯一ID比较类

补充....

4.3 美团Leaf算法

....

4.4 数据库自增ID

...

相关推荐
夕除1 小时前
springboot--06
数据库·spring boot·mybatis
2401_833033622 小时前
golang如何实现MQTT主题通配符路由_golang MQTT主题通配符路由实现策略
jvm·数据库·python
运维小子2 小时前
JumpServer Applet 发布自定义远程应用:Oracle SQL Developer 自动登录
数据库·sql·oracle·jumpserver
m0_596749092 小时前
Golang怎么实现方法集与接口的匹配_Golang如何理解值类型和指针类型实现接口的区别【详解】
jvm·数据库·python
学习 来了来了2 小时前
权限相关代码-表
数据库
薪火铺子2 小时前
MySQL 分库分表实战:ShardingSphere 深度解析
数据库·mysql
lifewange2 小时前
查询【学过 001 号同学所有课程】的学生
数据库
ErizJ2 小时前
Redis|腾讯面经总结
数据库·redis
瀚高PG实验室3 小时前
left link changed unexpectedly in block xxxx of index ““index_xxxxx“
数据库·postgresql·瀚高数据库