All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.shoulder.cluster.guid.RedisInstanceIdProvider Maven / Gradle / Ivy

The newest version!
package org.shoulder.cluster.guid;

import jakarta.annotation.Nonnull;
import org.shoulder.core.concurrent.PeriodicTask;
import org.shoulder.core.concurrent.Threads;
import org.shoulder.core.guid.AbstractInstanceIdProvider;
import org.shoulder.core.log.Logger;
import org.shoulder.core.log.ShoulderLoggers;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.time.Duration;
import java.time.Instant;
import java.util.List;

/**
 * 基于缓存的实现 (非幂等)
 * 执行 LUA 脚本的 redisTemplate.valueSerializer 不能为 jdk 的策略
 *
 * @author lym
 */
@SuppressWarnings("rawtypes, unchecked")
public class RedisInstanceIdProvider extends AbstractInstanceIdProvider implements ApplicationListener, DisposableBean {

    private final Logger log = ShoulderLoggers.SHOULDER_DEFAULT;

    /**
     * redis key
     */
    private final String idAssignCacheKey;

    /**
     * 最大可以申请到的 instanceId
     */
    private final long maxId;

    /**
     * 不能是 StringRedisTemplate
     */
    private final RedisTemplate redis;

    private volatile boolean alreadyStop = false;

    public RedisInstanceIdProvider(String idAssignCacheKey, long maxId, RedisTemplate redisTemplate) {
        this.idAssignCacheKey = idAssignCacheKey;
        this.maxId = maxId;
        this.redis = redisTemplate;
    }

    /**
     * @return id
     */
    @Override
    protected long assignInstanceId() {
        // 遍历目标 map 所有机器标识 
        // 心跳时间大于 10 分钟的,抢占,并续命心跳
        // 添加一项
        final String luaScript =
                "local currentTime=redis.call('time')[1];\n" +
                        "local existsKeys = redis.call('hkeys', KEYS[1]);\n" +
                        // 第一台机器,hash 为空,直接使用 0
                        "if(existsKeys == nil or #existsKeys == nil)\n" +
                        "then\n" +
                        "   local opResult = redis.call('hsetnx', KEYS[1], 0, currentTime);\n" +
                        "   if(opResult == 1)\n" +
                        "   then\n" +
                        "       return 0;\n" +
                        "   else\n" +
                        "       return -2;\n" +
                        "   end;\n" +
                        "end;\n" +
                        // 机器数小于 maxId,直接使用机器数作为 id
                        "if #existsKeys < tonumber(ARGV[1])\n" +
                        "then\n" +
                        "   local id = #existsKeys;\n" +
                        "   local opResult = redis.call('hsetnx', KEYS[1], id, currentTime);\n" +
                        "   if(opResult == 1)\n" +
                        "   then\n" +
                        "       return id;\n" +
                        "   else\n" +
                        "       return -3;\n" +
                        "   end;\n" +
                        "end;\n" +
                        // 机器数 >= maxId,遍历前面的,看是否有很久(10min)不用的,抢占他
                        "for i=1,KEYS[1] do\n" +
                        "   if currentTime - redis.call('hget', KEYS[1], existsKeys[i]) > 900\n" +
                        "   then\n" +
                        "       local opResult = redis.call('hsetnx', KEYS[1], existsKeys[i], currentTime);\n" +
                        "       if(opResult == 1)\n" +
                        "       then\n" +
                        "           return existsKeys[i];\n" +
                        "       else\n" +
                        "           return -4;\n" +
                        "       end;\n" +
                        "   end;\n" +
                        "end;\n" +
                        "return -1;\n";
        RedisScript tryInstanceIdScript = new DefaultRedisScript<>(luaScript, Long.class);

        Long result = (Long) redis.execute(tryInstanceIdScript, List.of(idAssignCacheKey), maxId);
        if (result != null && result > 0) {
            log.debug("redisInstanceIdProvider assignInstanceId SUCCESS.");
        } else {
            result = ILLEGAL;
            log.error("redisInstanceIdProvider assignInstanceId FAIL({}): idAssignCacheName={}, maxId={}", result, idAssignCacheKey, maxId);
        }

        return result;
    }

    @Override
    public void onApplicationEvent(@Nonnull ContextRefreshedEvent event) {
        PeriodicTask hearBeatTask = new PeriodicTask() {

            @Override public String getTaskName() {
                return "RedisInstanceIdProviderHeartBeat";
            }

            @Override public void process() {
                final String luaScript =
                    """
                            local currentTime=redis.call('time')[1];
                            redis.call('hset', KEYS[1], ARGV[1], currentTime);
                            return 1;
                            """;
                RedisScript heartbeatScript = new DefaultRedisScript<>(luaScript, Long.class);

                try {
                    Long result = (Long) redis.execute(heartbeatScript, List.of(idAssignCacheKey), getCurrentInstanceId());
                    if (result != null && result == 1) {
                        log.debug("redisInstanceIdProvider heartbeat SUCCESS.");
                    } else {
                        log.warn("redisInstanceIdProvider heartbeat FAIL: idAssignCacheName={}, instanceId={}", idAssignCacheKey, getCurrentInstanceId());
                    }
                } catch (Exception e) {
                    log.error("redisInstanceIdProvider heartbeat ex FAIL!", e);
                }
            }

            @Override public Instant calculateNextRunTime(Instant now, int runCount) {
                // 每分钟执行一次
                return alreadyStop ? NO_NEED_EXECUTE : now.plus(Duration.ofMinutes(1));
            }
        };
        Threads.schedule(hearBeatTask, Instant.now());
    }

    @Override
    public void destroy() {
        releaseInstanceId();
    }

    private void releaseInstanceId() {
        alreadyStop = true;
        final String luaScript =
                """
                        redis.call('hdel', KEYS[1], ARGV[1]);
                        return 1;
                        """;
        RedisScript releaseInstanceIdScript = new DefaultRedisScript<>(luaScript, Long.class);

        Long result = (Long) redis.execute(releaseInstanceIdScript, List.of(idAssignCacheKey), super.getCurrentInstanceId());
        if (result != null && result == 1) {
            log.debug("releaseInstanceId SUCCESS.");
            instanceId = ILLEGAL;
        } else {
            log.debug("releaseInstanceId FAIL: idAssignCacheName={}, instanceId={}", idAssignCacheKey, super.getCurrentInstanceId());
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy