如何利用 Redis 实现高并发全局 ID 生成器

科技   2024-10-29 17:06   河北  

大家好,我是华仔呀。


截至周四涨价前继续送福利,文末有优惠券,有需要的小伙伴可以领取一下。


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

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

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

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

谢霸戈:那咋办呢?

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

分布式 ID 要满足什么要求

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

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

  1. 有序性之单调递增,想要分而治之、二分法查找就必须实现。另外,MySQL 是你们用的最多的数据库,B+ 树为了维护 ID 的有序性,就会频繁的在索引的中间位置插入而挪动后面节点的位置,甚至导致频繁的页分裂,这对于性能的影响是极大的。
  2. 全局唯一性,ID 不唯一就会出现主键冲突。
  3. 高性能,生成 ID 是高频操作,如果性能缓慢,系统的整体性能都会受到限制。
  4. 高可用,也就是在给定的时间间隔内,一个系统总的可用时间占的比例。
  5. 存储空间小,用 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
  1. 假设订单 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;
        }
    }


    本周四星球要涨价到 199 元,周四星球要涨价到 199 元周四星球要涨价到 199 元,现在只需 160 元即可,认可且需要的抓紧来吧。加入星球的会送技术小册一份。

    加入之前请一定要把这些内容看完

    关于公众号介绍:强烈推荐大佬的 Kafka 系列,看完觉得自愧不如!!

    关于公众号原创文章:华仔原创精品20+篇,真香。

    关于星球介绍:

    彻底理解 Kafka,这一篇文章就够了!!!

    小红书实战


    另外今天加入星球且不退出的老铁们,送下面小册一份,

    相当于 179 - 19 = 160 元,原价 189 元,涨价后 199 元,

    现在只需 160 元,记得加我好友兑换福利哦。



    加入星球如果不满意3天内包退。

    还在观望的小伙伴们真的要抓紧了,犹豫时间真的不多了


    另外必须要注意的是上车的老铁一定要加我微信好友,拉你们加入星球专属交流群。

    华仔聊技术
    聊聊后端技术架构以及中间件源码
     最新文章