简单实用!利用Redis轻松实现高并发全局ID生成器

我相信你会经常遇到要生成唯一 ID 的场景,比如标识每次请求、生成一个订单编号、创建用户需要创建一个用户 ID。

谢霸戈:这还不简单,用 UUID 不就行了。

UUID 确实是个好东西,生成的 ID 全球唯一,但是有两个致命缺陷。

不是递增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。

谢霸戈:那咋办呢?

别急,今天我就给大家带来一个神器级的解决方案——Redis 分布式 ID 生成器!配合 SpringBoot3.0,让你的 ID 生成变得既简单又高效。

分布式 ID 要满足什么要求

在进入正文前,先介绍下分布式 ID 应该满足哪些特性。

分布式 ID 生成器需要满足以下特性。

有序性之单调递增,想要分而治之、二分法查找就必须实现。另外,MySQL 是你们用的最多的数据库,B+ 树为了维护 ID 的有序性,就会频繁的在索引的中间位置插入而挪动后面节点的位置,甚至导致频繁的页分裂,这对于性能的影响是极大的。全局唯一性,ID 不唯一就会出现主键冲突。高性能,生成 ID 是高频操作,如果性能缓慢,系统的整体性能都会受到限制。高可用,也就是在给定的时间间隔内,一个系统总的可用时间占的比例。存储空间小,用 MySQL 的 InnoDB B+树来说,普通索引(非聚集索引)会存储主键值,主键越大,每个 Page 页可以存储的数据就越少,访问磁盘 I/O 的次数就会增加。

Redis String 实现分布式 ID

Redis 集群能保证高可用和高性能,为了节省内存,ID 可以使用数字的形式,并且通过递增的方式来创建新的 ID。

防止重启数据丢失,你还需要把 Redis AOF 持久化开启。

MySQL:“开启 AOF 持久,为了性能设置成 everysec 策略还是有可能丢失一秒的数据,所以你还可以使用一个异步机制将生成的最大 ID 持久化到一个 MySQL。”

好主意,在生成 ID 之后发送一条消息到 MQ 消息队列中,把值持久化到 MySQL 中。

我们可以使用 Redis String 数据类型来实现,key 用于区分不同业务场景的 ID 生成器,value 存储 ID。

String 数据类型提供了 INCR 指令,它能把 key 中存储的数字加 1 并返回客户端。如果 key 不存在,那么 key 的 value 先被初始化成 0,再执行加 1 操作并返回给客户端。

Redis,作为一个高性能的内存数据库,天生就适合处理高并发的场景。它的“单线程”模型更是让它在处理 ID 生成时如鱼得水。

Redis 的操作是原子性的,这就意味着在整个过程中,不会有任何的并发问题出现,从而确保了 ID 的唯一性。

设计思路

设计思路如下图所示。

图 2-4

假设订单 ID 生成器的 key 是“counter:order”,当应用服务启动的时候先从数据库中查询出最大值 M。执行 EXISTS counter:order 判断是否存在 key。Redis 中不存在 key “counter:order”,执行 SET counter:order M 将 M 值作写入 Redis。Redis 中存在 key “counter:order”,值为 K,那么就比较 M 和 K 的值,执行 SET counter:order max(M, N)将最大值写入 Redis,相等的话就不操作。应用服务启动完成后,每次需要生成 ID 的时候,应用程序就向 Redis 服务器发送 INCR counter:order指令。应用程序将获取到的 ID 值发送到 MQ 消息队列,消费者监听队列把值更新到 MySQL。

SpringBoot 代码实现

接下来,我们结合 SpringBoot3.0 来打造一个强大且易用的 Redis 分布式 ID 生成器。

首先,我们需要在 SpringBoot 项目中引入 redis 的依赖。在pom.xml文件中添上这行代码:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

搞定依赖后,我们得告诉 SpringBoot 怎么连接到 Redis。打开application.yml文件,填上 Redis 的服务地址和端口:

spring: application: name: redis redis: host: 127.0.0.1 port: 6379 password: magebyte timeout: 6000

万事俱备,只欠东风!接下来,我们编写一个 ID 生成器工具类。这个工具类负责与 Redis 交互,生成唯一的 ID。这里我们使用 Redis 的INCR命令,它能让 ID 自增,确保每次获取的 ID 都是唯一的。

@Component public class OrderIdGenerator implements InitializingBean { private final StringRedisTemplate redisTemplate; /** * 操作数据库 dao */ private final IdGeneratorMapper idGeneratorMapper; private static final String KEY = "counter:order"; /** * 数据库中的 ID 值 */ private String dbId; @Autowired public OrderIdGenerator(StringRedisTemplate redisTemplate, IdGeneratorMapper idGeneratorMapper) { this.redisTemplate = redisTemplate; this.idGeneratorMapper = idGeneratorMapper; } public Long generateId(String key) { return redisTemplate.opsForValue().increment(key, 1); } @Override public void afterPropertiesSet() throws Exception { // 从数据库查询最大 ID this.dbId = idGeneratorMapper.getMaxID(KEY); Boolean hasKey = redisTemplate.hasKey(KEY); if (Boolean.TRUE.equals(hasKey)) { // key 存在,比较 dbId 与 redisValue,取出最大值 String redisValue = redisTemplate.opsForValue().get(KEY); String targetValue = max(this.dbId, redisValue); } else {
自定义 ID 规则

不过呢,光有唯一的 ID 还不够,我们还得让它更符合业务的实际需求。比如订单编号吧,我们可能希望它的格式是ORD-20240528-0001,其中ORD是业务标识,20240528是日期,0001是当天的序号。

public String generateCustomId(String key, String prefix, String datePattern) { long sequence = redisTemplate.opsForValue().increment(key, 1); return String.format("%s-%s-%04d", prefix, new SimpleDateFormat(datePattern).format(new Date()), sequence); }

那具体怎么用呢?让我们在业务代码中一探究竟!想象一下,在一个电商系统中,当一个新的订单如流星般划过天际,我们迫不及待地想要一个独一无二的 ID 来标记它时——很简单,只需调用我们的generateCustomId方法,传入订单相关的参数即可。

@Service public class OrderService { private final RedisIdGenerator idGenerator; @Autowired public OrderService(RedisIdGenerator idGenerator) { this.idGenerator = idativeIdGenerator; } public Order createOrder(OrderRequest request) { String orderId = idGenerator.generateCustomId("order:id", "ORD", "yyyyMMdd"); Order order = new Order(); order.setId(orderId); // 其他业务逻辑... return order; } }

THE END
本站服务器由亿华云赞助提供-企业级高防云服务器