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

相关推荐
这孩子叫逆7 分钟前
Spring Boot项目的创建与使用
java·spring boot·后端
星星法术嗲人10 分钟前
【Java】—— 集合框架:Collections工具类的使用
java·开发语言
一丝晨光29 分钟前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
天上掉下来个程小白31 分钟前
Stream流的中间方法
java·开发语言·windows
xujinwei_gingko42 分钟前
JAVA基础面试题汇总(持续更新)
java·开发语言
liuyang-neu44 分钟前
力扣 简单 110.平衡二叉树
java·算法·leetcode·深度优先
一丝晨光1 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
罗曼蒂克在消亡1 小时前
2.3MyBatis——插件机制
java·mybatis·源码学习
_GR1 小时前
每日OJ题_牛客_牛牛冲钻五_模拟_C++_Java
java·数据结构·c++·算法·动态规划
coderWangbuer1 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql