雪花算法结合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生成器。

相关推荐
Asthenia04125 分钟前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom6 分钟前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04122 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04123 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04123 小时前
Spring 启动流程:比喻表达
后端