分布式ID(或叫全局唯一ID)的实现

分布式ID特性

  1. 全局唯一性:保证在整个分布式系统中唯一性,不会出现重复的ID。
  2. 高可用性:可以通过水平扩展、冗余备份或集群部署来确保。即使某个节点或组件发生故障,仍然能够正常。
  3. 安全性:分布式ID生成器独立于 业务逻辑的。设计为一个单独的组件或服务,可以被各种服务共享使用。
  4. 高性能:要求在很短的时间内生成唯一的标识符。
  5. 递增性:可按时间顺序排序,以便ID进行索引。

分布式ID实现

时间戳+计数器(每天一个key,方便统计当日订单)

  • 符号位:1bit,永远为0(表示正数)
  • 时间戳:31bit,以秒为单位,可以使用69年(\(2^{31}/3600/24/365≈69\)
  • 序列号:32bit,秒内的计数器,支持每秒产生\(2^{32}\)个不同ID

序列号生成的解释

String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);
  1. 格式化当前日期:
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
  • now 对象: 这是一个 LocalDateTime 类型的实例,表示当前的日期和时间。

  • DateTimeFormatter.ofPattern("yyyy:MM:dd"):创建了一个日期时间格式化器,指定了格式模式为 "yyyy:MM:dd"。在这个模式中:

    • yyyy 表示四位数的年份。
    • MM 表示两位数的月份。
    • dd 表示两位数的日期。
    • 冒号 : 作为日期部分的分隔符。
  • now.format(...): 将当前的 LocalDateTime 按照指定的格式转换为字符串。例如,如果当前日期是 2025 年 4 月 9 日,那么 date 的值将是 "2025:04:09"

  1. 在 Redis 中递增特定键的值:
long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);
  • 键的构造: 通过字符串拼接,生成了一个特定格式的键名:"inc:" + keyPrefix + ":" + date
    • "inc:" 是固定的前缀,表示这是一个计数器(increment)的键。
    • keyPrefix 是传入的方法参数,用于区分不同的业务场景或计数类别。
    • date 是上一步生成的日期字符串,确保计数器是按天分隔的。

例如,如果 keyPrefix 的值是 "order",并且 date 的值是 "2025:04:09",那么最终的键名将是 "inc:order:2025:04:09"

  • stringRedisTemplate.opsForValue().increment(...) 这是 Spring Data Redis 提供的操作,用于对指定的键执行递增操作。具体来说:
  • 功能: 在 Redis 中,将键 "inc:order:2025:04:09" 对应的值加一。如果该键不存在,Redis 会先将其初始化为 0,然后再执行递增操作。
  • 返回值: 返回递增后的值,即当前的计数值。

代码详解

/**
 * 通过 Redis 实现了一个全局唯一 ID 生成器,适用于分布式系统中需要生成唯一标识符的场景。
 */

/**
 * 1. 类的定义与依赖注入:
 *
 * @Component:将该类声明为 Spring 的组件,使其能够被 Spring 容器管理和自动扫描。
 * @Autowired:自动注入 StringRedisTemplate,这是 Spring 提供的用于操作 Redis 的模板类。
 */
@Component
public class RedisIdWorker {
    /**
     * 2. 常量定义:
     * BEGIN_TIMESTAMP:定义了一个起始时间戳,表示从该时间点开始计算。
     * 这里的值 1640995200L 对应的是 2022 年 1 月 1 日 00:00:00 的 UNIX 时间戳(以秒为单位)。
     * COUNT_BIT:表示序列号占用的位数,这里设置为 32 位。
     */
    // 设置起始时间,我这里设定的是2022.01.01 00:00:00
    public static final Long BEGIN_TIMESTAMP = 1640995200L;
    // 序列号长度
    public static final Long COUNT_BIT = 32L;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * nextId 方法:
     *
     * @param keyPrefix
     * @return
     */
    public long nextId(String keyPrefix) {
        /* 1. 生成时间戳
        LocalDateTime.now():获取当前的本地日期时间。
        now.toEpochSecond(ZoneOffset.UTC):将当前时间转换为 UTC 时区的秒级时间戳。
        timeStamp = currentSecond - BEGIN_TIMESTAMP:计算当前时间与起始时间的差值,得到从起始时间到现在的秒数。
         */
        LocalDateTime now = LocalDateTime.now();
        long currentSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = currentSecond - BEGIN_TIMESTAMP;
        /* 2. 生成序列号
        now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")):
        将当前日期格式化为字符串(格式为 "yyyy:MM:dd"),用于构造 Redis 的键。
        stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date):
        在 Redis 中以 "inc:" + keyPrefix + ":" + date 为键,
        对其值执行自增操作。该键的命名方式确保了不同业务(通过 keyPrefix 区分)在不同日期下的计数是独立的。
         */
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);
        // 3. 拼接并返回,简单位运算
        return timeStamp << COUNT_BIT | count;
    }
}
posted @ 2025-04-09 19:28  kuki'  阅读(43)  评论(0)    收藏  举报