测试结果基于:
redis 3.2.100 Windows、jdk 1.8.0_201、mysql 8.0.22
机器配置:16G、R7-5800H、Windows 10专业版
测试配置:2000 线程,10s 启动时长,随机时间 1s ,同时并发请求访问 5 个服务
数据库序列化ID
逐个获取
首先在数据库中进行下列的初始化步骤。
DROP TABLE IF EXISTS `sequence_id`;
CREATE TABLE `sequence_id` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`value` char(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
我们可以使用 value 来进行记录我们申请 ID 的服务项目。
每次需要使用的时候,我们只需要向目标数据库中插入新的数据即可。
SequenceId id = new SequenceId();
id.setValue(value);
int insert = sequenceIdMapper.insert(id);
if (insert == 1) {
return id.getId();
}
因为 Mybatis 会在进行插入操作之后完成对象数据回填,所以可以直接获取。
优缺点:
- 可以实现序列化ID的生成,且能够记录每次请求的源服务。
- 效率低,数据库 IO 会成为性能瓶颈。且在长时间提供服务之后,会造成数据量过大,拖累数据库效率。
批量获取
在数据库中执行以下操作:
CREATE TABLE `id_generator` (
`id` int NOT NULL,
`max_id` bigint NOT NULL COMMENT '当前最大id',
`step` int NOT NULL COMMENT '号段的布长',
`biz_type` int NOT NULL COMMENT '业务类型',
`version` int NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `id_generator` VALUES (1, 1, 2000, 1, 1);
添加了新的表格之后,我们就可以通过每次对语句执行 update 操作。
@Override
public List<Long> getGenerateId(Integer step) {
IdGeneratorDTO maxId = idGeneratorMapper.getMaxId();
if (Objects.isNull(maxId)) {
idGeneratorMapper.initGenerateId();
}
IdGeneratorDTO currentMax = idGeneratorMapper.getMaxId();
Integer version = currentMax.getVersion();
Long max = currentMax.getMaxId();
Integer id = currentMax.getId();
IdGenerator idGenerator = new IdGenerator(id, max + step, step, 1, version + 1);
List<Long> ids = new ArrayList<>();
idGeneratorMapper.updateById(idGenerator);
for (Long i = max; i < max + step; i++) {
ids.add(i);
}
return ids;
}
每次执行之后,一次性返回批量的号段数据,完成 ID 的分发。这样的操作,获得的是批量的 ID 。
Redis生成
Redis 生成 ID 的可行性,主要是依靠 redis 中的 incry 命令。
主要实现方案:
@Service
public class RedisIdServiceImpl implements RedisIdService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public synchronized Long getMaxId() {
String key = "id";
redisTemplate.opsForValue().setIfAbsent(key, 0);
redisTemplate.opsForValue().increment(key);
Object max = redisTemplate.opsForValue().get(key);
String maxStr = String.valueOf(max);
return Long.valueOf(maxStr);
}
}
这样的方案设计下,可以利用 Redis 的原子性实现 ID 的获取。
UUID
UUID 全称 Universally Unique Identifier
,即通用唯一识别码。
现在目前 UUID 一共有五个实现版本:
- 版本1:严格按照 UUID 定义的每个字段的意义来实现,使用的变量因子是时间戳+时钟序列+节点信息(Mac地址)
- 版本2:基本和版本1一致,但是它主要是和 DCE( IBM 的一套分布式计算环境)。但是这个版本在 ietf 中也没有具体描述,反而在DCE 1.1: Authentication and Security Services这篇文档中说到了具体实现。所以这个版本现在很少使用到,并且很多地方的实现也都忽略了它。
- 版本3:基于 name 和 namespace 的 hash 实现变量因子,版本3使用的是 md5 进行 hash 算法。
- 版本4:使用随机或者伪随机实现变量因子。
- 版本5:dfsg 基于 name 和 namespace 的 hash 实现变量因子,版本5使用的是 sha1 进行 hash 算法。
实现方案很简单,在 Java 中有对应的内置实现,直接调用即可生成 UUID 。
@Service
public class UUIDServiceImpl implements UUIDService {
@Override
public String getUUID() {
ObjectIdGenerators.UUIDGenerator uuidGenerator = new ObjectIdGenerators.UUIDGenerator();
UUID uuid = uuidGenerator.generateId(this);
String s = uuid.toString();
return s.replaceAll("-", "").toUpperCase(Locale.ROOT);
}
}
测试之后的性能表现如下:
Snowflake雪花
- 强依赖机器时间,如果时间回拨 Id 可能会重复
- 不是严格的趋势递增,极端情况在机器时间不同步的情况下后生成的 Id 可能会小于先生成的 Id,即只能在 worker 级别保证递增
- 服务需要保证 workerId 唯一(如果需要保证严格唯一的话会比较麻烦,简单可以基于服务 IP 跟 Port 来生成,但由于 workerId 只有 10 位,因此 workerId 可能会重复)
优点:雪花算法提供了一个很好的设计思想,雪花算法生成的 ID 是趋势递增,不依赖数据库等第三方系统,生成 ID 的性能也是非常高的,而且可以根据自身业务特性分配 bit 位,非常灵活。
缺点:雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。
在 MP 中,有对应的工具类为 IdWorker,可以直接用来雪花 ID 的生成。性能测试如下:
Leaf
leaf 的实现中,主要使用 Leaf Snowflake 算法。
参考文档:
分布式ID生成方案_YY迪迪的博客-CSDN博客_分布式id生成方案
雪花算法SnowFlake全方位详细解读,结合位运算的使用解读_追寻光的方向的博客-CSDN博客_雪花算法使用
Leaf 详解_yin__ren的博客-CSDN博客_leaf算法
Leaf——美团点评分布式ID生成系统
分布式唯一 ID 解析.md
GitHub - Meituan-Dianping/Leaf: Distributed ID Generate Service