1. 全局唯一ID的特性

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

  1. 唯一性
  2. 高可用性
  3. 递增性
  4. 安全性
  5. 高性能

2. Redis生成全局唯一ID分析

  1. 在Redis中,可以使用incr命令将某个键的值加1,这个操作是原子性的,能够有效解决并发问题。
  2. 然而,直接将每次incr后的值作为ID并不安全,因为这类似于数据库的自增ID,可能存在安全隐患。
  3. 为了提高安全性,我们可以将自增的值作为序列号,并在前面加上时间戳,形成一个唯一的ID。
  4. 通常,我们使用long类型来存储ID,这样ID就是一个64位的二进制数。
  5. 由于ID中包含了时间戳,因此序列号占用的位数是有限的。随着时间的推移,序列号会达到最大值。为了避免这种情况,我们需要周期性地更换Redis中的键。
  6. 一种常见的做法是,通过将日期拼接到键名中来实现周期性更换。例如,可以将日期作为键的一部分,这样每过一段时间就会自动更换键。
  7. 在实际开发中,一个ID通常由3部分组成:1位符号位、31位时间戳和32位序列号。
  8. 为了实现周期性更换,可以使用类似incr:id:20241212这样的键名,其中20241212表示日期,这样每一天都会生成一个新的键,从而避免序列号溢出。

3. Redis生成全局唯一ID实现

@Component  
public class RedisIdWorker {  
  
    // 开始时间戳  
    private static final long BEGIN_TIMESTAMP = 1684224000L;  
  
    // 序列号的位数  
    private static final int COUNT_BITS = 32;  
  
    private final StringRedisTemplate stringRedisTemplate;  
  
    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
  
    public long nextId(String keyPrefix) {  
        // 1. 生成时间戳  
        LocalDateTime now = LocalDateTime.now();  
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);  
        long timestamp = nowSecond - BEGIN_TIMESTAMP;  
  
        System.out.println("timestamp:" + timestamp);  
  
        // 2. 生成序列号  
        // 2.1 获取当前日期,精确到天  
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));  
        // 2.2 自增长  
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);  
        System.out.println("count:" + count);  
        // 3. 拼接返回id  
        return timestamp << COUNT_BITS | count;  
    }  
  
}

测试用例:

@SpringBootTest  
public class RedisIdWorkerTest {  
    @Autowired  
    private RedisIdWorker redisIdWorker;  
  
    private final ExecutorService es = Executors.newFixedThreadPool(500);  
  
    @Test  
    public void nextIdTest() throws InterruptedException {  
        CountDownLatch latch = new CountDownLatch(300);  
  
        Runnable task = () -> {  
            for (int i = 0; i < 100; i++) {  
                long id = redisIdWorker.nextId("order");  
                System.out.println("id = " + id);  
            }  
            latch.countDown();  
        };  
  
        long begin = System.currentTimeMillis();  
        for (int i = 0; i < 300; i++) {  
            es.submit(task);  
        }  
        latch.await();  
        long end = System.currentTimeMillis();  
        System.out.println("time = " + (end - begin));  
    }
}