全局唯一 ID
在电商项目中,经常会涉及到订单的生成,在生成订单时,每个订单都会对应一个 ID。一般情况下,我们会想到借助数据库的自增 ID 来生成订单 ID,如果你的系统规模较小,这是可行的,但是,当系统规模越来越大时,可能数据库的自增 ID 就无法满足我们的需求了。在分布式系统中,会使用分库分表,如果每张表都使用自增 ID,那么肯定会出现重复的 ID。并且,如果我们使用数据库的自增 ID,可能会暴露系统的规模,比如,我昨天下了一单,订单 ID 是 100,第二天,我又下了一单,订单 ID 是 200,这样的话,用户就能猜测到昨天一天系统大概有 100 个订单。
所以,使用数据库的自增 ID,会有以下两个明显的缺点:
- 受单表数据量的限制(无法满足分布式系统的需求)
- id 的规律性太明显
在这种情况下,我们需要使用全局唯一 ID 来生成订单 ID。全局 ID 生成器是一种在分布式系统下用来生成全局唯一 ID 的工具,一般要满足下列特性:
- 唯一性
- 递增性
- 高可用
- 高性能
- 安全性
由于本笔记记录的是 Redis 相关的信息,所以,我们这里只介绍基于 Redis 的全局唯一 ID 生成器。而 Redis 中提供了一个原子操作,可以满足我们的需求,那就是 INCR
命令,它类似于 MySQL 的自增 ID。为了增加 ID 的安全性,我们可以不直接使用 Redis 自增的数值,而是拼接一些其它信息,最终,我们生成的 ID 看起来像这样:
所以,最终生成的 ID 是 64 位,ID 的组成部分如下:
- 符号位:1 bit,永远为 0
- 时间戳:31 bit,以秒为单位,可以使用 69 年(231 / 3600 / 24 / 365 = 69)
- 序列号:32 bit,秒内的计数器,支持每秒产生 232 个不同 ID
下面是一个简单的实现:
RedisIdWoker.java
@Component
public class RedisIdWoker {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy:MM:dd");
/**
* 开始时间戳, 2020-01-01 00:00:00
*/
private static final long BEGIN_EPOCH_SECONDS = LocalDateTime.of(2020, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.of("+8"));
@Resource
private StringRedisTemplate stringRedisTemplate;
public long generateId(String keyPrefix) {
// 生成时间戳
LocalDateTime now = LocalDateTime.now();
long timestamp = now.toEpochSecond(ZoneOffset.of("+8")) - BEGIN_EPOCH_SECONDS;
// 生成自增序列
String dateFormat = now.format(formatter);
// 自增的 key 为 incr:前缀:日期, 这样可以保证每天的自增序列是独立的, 同时也方便我们统计每天的订单数量
// 如果我们把所有的自增序列都放在一个 key 下面, 有可能会超过 redis 的单个 key 大小的限制
// 对于自增的 key, redis 支持最大的自增序列为 2 ^ 64, 见 https://redis.io/commands/incr/
String incrKey = "incr:" + keyPrefix + ":" + dateFormat;
// 当然, 如果你不想统计每天的订单数量, 那么也可以为所有的自增序列都使用同一个 key, 或者给 key 设置过期时间为 1 天
Long id = stringRedisTemplate.opsForValue().increment(incrKey);
// 最终生成的 ID 是 64 位的, 高 32 位是时间戳, 低 32 位是自增序列
return timestamp << 32 | id;
}
}
IDTest.java
/**
* 生成 30000 个 ID
*/
@Test
void test8() throws InterruptedException {
int threadCount = 300;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
Runnable runnable = () -> {
for (int i = 0; i < 100; i++) {
redisIdWoker.generateId("order");
}
countDownLatch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
ID_WOKER_EXECUTOR.execute(runnable);
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("耗时: " + (end - begin) + " ms");
}
一些其它的全局唯一 ID 生成方案:
UUID(不推荐)
snowflake 算法(雪花算法)
数据库自增
这里的数据库自增说的是,单独使用一张表来记录自增 ID,然后,每次生成 ID 时,都去查询这张表,获取自增 ID,然后,再更新这张表的自增 ID。这种方案的缺点是,每次生成 ID 都需要访问数据库,会对数据库造成压力,而且,性能没有 Redis 高。