雪花算法结合Redis实现机器id的动态获取和回收

雪花算法是一个分布式ID的生成算法,能够生成全局唯一和有序递增的id,生成的时候不依赖于其他中间件,仅仅一个工具类即可生成,这在单体应用中十分好用,但是在微服务上就有些局限了,因为雪花算法依赖于机器id的不同,才能保证多台机器上生成的id不重复。

为了解决多台机器上生成id重复的问题,我们可以在每台机器部署服务的时候,手动指定不同的机器id到环境变量,并赋值到雪花id生成器中,虽然这是一个解决办法,但并不高明,因为我们不仅要拿个本子手动记录每台服务器的机器id,还要逐台服务器的去处理。

为此社区内出现了许多解决办法,其中百度、滴滴、美团都推出了自己的id生成器方案,有雪花算法的变种,有基于数据库号段模式的改造,都是不错的处理方式。

本文所分享的是定时任务结合Redis分布式锁实现的机器id的获取和回收

实现步骤

  1. 提前在Redis中设置好可用的机器id范围,如有1024个机器id可用:

    shell 复制代码
    EVAL "for i=1,1023 do redis.call('SADD', 'worker_id_pool', i) end" 0

    这会在Redis中保存一个集合,里面是可用的机器码。

  2. Spring Boot程序启动,定时任务执行,去向Redis获取一个未被使用的机器id:

    java 复制代码
    final Boolean flag = redisTemplate.opsForValue().setIfAbsent(useKey, workerId);
    if (flag != null && flag) {
        log.info("保存机器id成功:{}", workerId);
        redisTemplate.expire(useKey, 1, TimeUnit.HOURS);
        IdGenerator.setWorkerId(workerId);
    } else {
        log.info("保存机器id失败,该机器id已被使用:{}", workerId);
    }

关于机器id回收

申请得到的机器id要设置一个过期时间,然后定时任务每次都向Redis发起一个更新操作,保持这个机器id是一直被使用的状态。

代码

java 复制代码
import com.cc.utils.IdGenerator;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 雪花id获取机器id后定时的心跳机制,保证机器id唯一,如果停止心跳,Redis可以认为该机器id可以回收
 * 需要提前在Redis上创建好机器id池,操作命令为:EVAL "for i=1,1023 do redis.call('SADD', 'worker_id_pool', i) end" 0
 *
 * @author cc
 * @date 2023-09-19 15:32
 */
public class IdHeartbeatJob extends QuartzJobBean {
    /**
     * 机器id池
     */
    private static final String WORKER_ID_POOL = "worker_id_pool";

    /**
     * 被申请使用着的机器id池
     */
    private static final String WORKER_ID_POOL_USED = "worker_id_pool_used";

    private static final Logger log = LoggerFactory.getLogger(IdHeartbeatJob.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        /**
         * 判断是否有workerId
         * 没有:
         *      申请
         * 有:
         *      发送心跳,重置过期时间
         */

        log.info("定时向Redis发送雪花id申请心跳");

        long workerId = IdGenerator.INSTANCE.getWorkerId();
        if (workerId == 0) {
            log.info("本机器id为0,开始申请机器id");

            final Set<Object> ids = redisTemplate.opsForSet().members(WORKER_ID_POOL);
            if (ids == null || ids.isEmpty()) {
                log.error("申请机器id失败,请检查Redis中是否有机器id池");
                return;
            }

            Set<String> keys = redisTemplate.keys(WORKER_ID_POOL_USED + "*");
            if (keys == null) {
                keys = new HashSet<>();
            }

            String useKey = null;
            for (Object item : ids) {
                useKey = WORKER_ID_POOL_USED + "_" + item;

                boolean useFlag = false;
                for (String key : keys) {
                    if (useKey.equals(key)) {
                        useFlag = true;
                        break;
                    }
                }
                if (useFlag) {
                    continue;
                }

                workerId = ((Number) item).longValue();
                break;
            }

            if (workerId == 0) {
                log.error("无可用的机器id");
                return;
            }

            log.info("有id可用:{}", workerId);

            /**
             * 用分布式锁避免并发问题
             */
            final Boolean flag = redisTemplate.opsForValue().setIfAbsent(useKey, workerId);
            if (flag != null && flag) {
                log.info("保存机器id成功:{}", workerId);
                redisTemplate.expire(useKey, 1, TimeUnit.HOURS);
                IdGenerator.setWorkerId(workerId);
            } else {
                log.info("保存机器id失败,该机器id已被使用:{}", workerId);
            }
        } else {
            log.info("有机器id:{},发送心跳保存", workerId);

            String useKey = WORKER_ID_POOL_USED + "_" + workerId;
            redisTemplate.opsForValue().set(useKey, workerId);
            redisTemplate.expire(useKey, 1, TimeUnit.HOURS);
        }
    }
}

总结

这种方式的好处是逻辑简单,目的是为了解决手动操作每台服务器的工作。

雪花id虽好,但也有他的毛病,比如时钟回拨、id太长等,所以根据项目情况,也需要考虑使用其他的id生成器。

相关推荐
磊磊磊磊磊8 分钟前
用AI做了个排版工具,分享一下如何高效省钱地用AI!
前端·后端·react.js
❥ღ Komo·8 分钟前
K8s蓝绿发布实战:零停机部署秘籍
java·开发语言
小安同学iter13 分钟前
天机学堂-排行榜功能-day08(六)
java·redis·微服务·zset·排行榜·unlink·天机学堂
hgz071016 分钟前
Spring Boot Starter机制
java·spring boot·后端
daxiang1209220517 分钟前
Spring boot服务启动报错 java.lang.StackOverflowError 原因分析
java·spring boot·后端
我家领养了个白胖胖18 分钟前
极简集成大模型!Spring AI Alibaba ChatClient 快速上手指南
java·后端·ai编程
jiayong2318 分钟前
Markdown编辑完全指南
java·编辑器
heartbeat..36 分钟前
深入理解 Redisson:分布式锁原理、特性与生产级应用(Java 版)
java·分布式·线程·redisson·
一代明君Kevin学长39 分钟前
快速自定义一个带进度监控的文件资源类
java·前端·后端·python·文件上传·文件服务·文件流
aiopencode39 分钟前
上架 iOS 应用到底在做什么?从准备工作到上架的流程
后端