雪花id改多workerID依赖redis

复制代码
package com.meisoo.clearingplat.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * IdUtil.getSnowflake(SnowflakeConfig.staticWorkerId).nextId()
 * @author Hj
 * @date 2025/11/20
 */
@Slf4j
@Configuration
public class SnowflakeConfig {

    public static Integer staticWorkerId;

    @Value("${spring.application.name}")
    private String appName;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private String WORKER_ID_KEY = ":snowflake:workerids";
    private String WORKER_ID_LOCK_KEY = ":snowflake:workerid:lock";
    private final long expireTTL = 20 * 60 * 1000; // 20分钟过期

    @PostConstruct
    public void init() {
        this.staticWorkerId = generateDistributedWorkerId();
    }

    public int generateDistributedWorkerId() {
        Integer workerId = null;
        String appNameMap = appName + WORKER_ID_KEY;
        String lockKey = appName + WORKER_ID_LOCK_KEY;

        // 生成唯一的锁标识,防止误删其他线程的锁
        String lockValue = UUID.randomUUID().toString();

        try {
            // 尝试获取分布式锁
            boolean locked = tryLock(lockKey, lockValue);
            if (!locked) {
                // 如果获取锁失败,重试几次
                for (int i = 0; i < 1000; i++) {
                    Thread.sleep(100); // 等待100ms后重试
                    locked = tryLock(lockKey, lockValue);
                    if (locked) break;
                }
            }

            if (!locked) {
                throw new RuntimeException("无法获取分布式锁,workerId分配失败");
            }

            // 获取锁成功,分配workerId
            workerId = allocateWorkerId(appNameMap);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("workerId分配过程被中断", e);
        } finally {
            // 释放锁
            unlock(lockKey, lockValue);
        }

        if (workerId == null || workerId > 1023) {
            throw new RuntimeException("workerId分配失败,没有可用的workerId");
        }

        // 启动心跳更新任务
        startHeartbeatTask(appNameMap, workerId);

        return workerId;
    }

    /**
     * 尝试获取分布式锁
     */
    private boolean tryLock(String lockKey, String lockValue) {
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放分布式锁
     */
    private void unlock(String lockKey, String lockValue) {
        try {
            // 使用Lua脚本保证原子性,只有锁的值匹配时才删除
            String luaScript =
                    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                            "    return redis.call('del', KEYS[1]) " +
                            "else " +
                            "    return 0 " +
                            "end";

            RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
            Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue);

            if (result == null || result == 0) {
                log.warn("释放锁失败,锁可能已被其他线程释放或已过期");
            }
        } catch (Exception e) {
            log.error("释放锁时发生异常", e);
        }
    }

    /**
     * 分配workerId(在锁的保护下执行)
     */
    private Integer allocateWorkerId(String appNameMap) {
        for (int workerId = 0; workerId <= 1023; workerId++) {
            if (!redisTemplate.opsForHash().hasKey(appNameMap, String.valueOf(workerId))) {
                // 找到可用的workerId,立即占用
                redisTemplate.opsForHash().put(appNameMap, String.valueOf(workerId), System.currentTimeMillis());
                log.info("成功分配workerId: {} for application: {}", workerId, appName);
                return workerId;
            }
        }
        return null;
    }

    /**
     * 启动心跳更新任务
     */
    private void startHeartbeatTask(String appNameMap, Integer workerId) {
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

        scheduledExecutorService.scheduleAtFixedRate(() -> {
            try {
                log.debug("定期更新workerId: {} 的心跳", workerId);
                // 更新自己的心跳
                redisTemplate.opsForHash().put(appNameMap, String.valueOf(workerId), System.currentTimeMillis());

                // 清理过期节点(这里也需要加锁,避免并发清理)
                cleanupExpiredWorkers(appNameMap);

            } catch (Exception e) {
                log.error("心跳更新任务执行异常", e);
            }
        }, 2, 10, TimeUnit.MINUTES); // 每10分钟执行一次,比过期时间短
    }

    /**
     * 清理过期的worker节点(需要加锁)
     */
    private void cleanupExpiredWorkers(String appNameMap) {
        String lockKey = appName + WORKER_ID_LOCK_KEY + ":cleanup";
        String lockValue = UUID.randomUUID().toString();

        try {
            // 尝试获取清理锁,避免多个实例同时清理
            boolean locked = tryLock(lockKey, lockValue); // 10秒锁超时
            if (!locked) {
                return; // 如果没有获取到锁,跳过本次清理
            }

            long currentTime = System.currentTimeMillis();
            Set<Object> workerIds = redisTemplate.opsForHash().keys(appNameMap);

            for (Object workerIdObj : workerIds) {
                try {
                    String workerIdStr = String.valueOf(workerIdObj);
                    Long lastHeartbeat = (Long) redisTemplate.opsForHash().get(appNameMap, workerIdStr);

                    if (lastHeartbeat != null && currentTime - lastHeartbeat > expireTTL) {
                        log.info("清理过期的workerId: {}, 最后心跳时间: {}", workerIdStr, lastHeartbeat);
                        redisTemplate.opsForHash().delete(appNameMap, workerIdStr);
                    }
                } catch (Exception e) {
                    log.warn("清理workerId: {} 时发生异常", workerIdObj, e);
                }
            }

        } finally {
            unlock(lockKey, lockValue);
        }
    }
}
相关推荐
BD_Marathon1 小时前
Eclipse 代码自动补全设置
android·java·eclipse
L.EscaRC1 小时前
深入解析SpringBoot中的循环依赖机制与解决方案
java·spring boot·spring·循环依赖
曾经的三心草1 小时前
JavaEE初阶-网络原理1
java·网络·java-ee
一 乐2 小时前
健身达人小程序|基于java+vue健身达人小程序的系统设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·小程序
u***42074 小时前
Ubuntu介绍、与centos的区别、基于VMware安装Ubuntu Server 22.04、配置远程连接、安装jdk+Tomcat
java·ubuntu·centos
charlie1145141914 小时前
从 0 开始:在 WSL + VSCode 上利用 Maven 构建 Java Spring Boot 工程
java·笔记·vscode·后端·学习·maven·springboot
tuokuac5 小时前
Maven中的属性占位符的用法
java·maven
芒克芒克5 小时前
Maven 项目管理从入门到进阶:基础与高级实战全解析
java·maven
憧憬blog7 小时前
【Kiro开发集训营】拒绝“屎山”堆积:在 Kiro 中重构“需求-代码”的血缘关系
java·开发语言·kiro