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

相关推荐
Daniel 大东19 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
huaqianzkh22 分钟前
了解Hadoop:大数据处理的核心框架
大数据·hadoop·分布式
wind瑞25 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen25 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)31 分钟前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿32 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
修道-032332 分钟前
【JAVA】二、设计模式之策略模式
java·设计模式·策略模式
九圣残炎38 分钟前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
当归10241 小时前
若依项目-结构解读
java
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端