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

com.github.dts.sdk.client.RedisDiscoveryService Maven / Gradle / Ivy

package com.github.dts.sdk.client;

import com.github.dts.sdk.conf.DtsSdkConfig;
import com.github.dts.sdk.util.DifferentComparatorUtil;
import com.github.dts.sdk.util.ReferenceCounted;
import com.github.dts.sdk.util.SnowflakeIdWorker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

public class RedisDiscoveryService implements DiscoveryService, DisposableBean {
    public static final String DEVICE_ID = SnowflakeIdWorker.INSTANCE.nextId() + "";
    private static final Logger log = LoggerFactory.getLogger(RedisDiscoveryService.class);
    private static final int MIN_REDIS_INSTANCE_EXPIRE_SEC = 2;
    private static volatile ScheduledExecutorService scheduled;
    private final int redisInstanceExpireSec;
    private final byte[] keySdkPubSubBytes;
    private final byte[] keyServerPubSubBytes;
    private final byte[] keyServerPubUnsubBytes;
    private final byte[] keySdkSetBytes;
    private final ScanOptions keyServerSetScanOptions;
    private final MessageListener messageServerListener;
    private final Jackson2JsonRedisSerializer instanceServerSerializer = new Jackson2JsonRedisSerializer<>(ServerInstance.class);
    private final DtsSdkConfig.ClusterConfig clusterConfig;
    private final RedisTemplate redisTemplate = new RedisTemplate<>();
    private final SdkInstance sdkInstance;
    private final byte[] sdkInstanceBytes;
    private final ReferenceCounted> serverInstanceClientListRef = new ReferenceCounted<>(new CopyOnWriteArrayList<>());
    private final Collection serverListenerList = new CopyOnWriteArrayList<>();
    private final int updateInstanceTimerMs;
    private long serverHeartbeatCount;
    private ScheduledFuture sdkHeartbeatScheduledFuture;
    private ScheduledFuture updateServerInstanceScheduledFuture;
    private boolean destroy;
    private Map serverInstanceMap = Collections.emptyMap();
    private int serverUpdateInstanceCount;

    public RedisDiscoveryService(Object redisConnectionFactory,
                                 String redisKeyRootPrefix,
                                 int redisInstanceExpireSec,
                                 DtsSdkConfig.ClusterConfig clusterConfig,
                                 String ip,
                                 Integer port) {
        Jackson2JsonRedisSerializer instanceSdkSerializer = new Jackson2JsonRedisSerializer<>(SdkInstance.class);
        String account = "sdk" + DEVICE_ID;
        SdkInstance sdkInstance = new SdkInstance();
        sdkInstance.setDeviceId(DEVICE_ID);
        sdkInstance.setAccount(account);
        sdkInstance.setPassword(UUID.randomUUID().toString().replace("-", ""));
        sdkInstance.setIp(ip);
        sdkInstance.setPort(port);
        this.sdkInstance = sdkInstance;
        this.sdkInstanceBytes = instanceSdkSerializer.serialize(sdkInstance);

        this.updateInstanceTimerMs = clusterConfig.getRedis().getUpdateInstanceTimerMs();
        this.redisInstanceExpireSec = Math.max(redisInstanceExpireSec, MIN_REDIS_INSTANCE_EXPIRE_SEC);
        this.clusterConfig = clusterConfig;

        StringRedisSerializer keySerializer = StringRedisSerializer.UTF_8;
        this.keySdkSetBytes = keySerializer.serialize(redisKeyRootPrefix + "sdk:ls:" + DEVICE_ID);
        this.keySdkPubSubBytes = keySerializer.serialize(redisKeyRootPrefix + "sdk:mq:sub");

        this.keyServerPubSubBytes = keySerializer.serialize(redisKeyRootPrefix + "svr:mq:sub");
        this.keyServerPubUnsubBytes = keySerializer.serialize(redisKeyRootPrefix + "svr:mq:unsub");
        this.keyServerSetScanOptions = ScanOptions.scanOptions()
                .count(20)
                .match(redisKeyRootPrefix + "svr:ls:*")
                .build();

        this.messageServerListener = (message, pattern) -> {
            if (this.destroy) {
                return;
            }
            byte[] channel = message.getChannel();
            if (Arrays.equals(channel, keyServerPubSubBytes)) {
                updateServerInstance(getServerInstanceMap());
            } else if (Arrays.equals(channel, keyServerPubUnsubBytes)) {
                updateServerInstance(getServerInstanceMap());
            }
        };

        this.redisTemplate.setConnectionFactory((RedisConnectionFactory) redisConnectionFactory);
        this.redisTemplate.afterPropertiesSet();
    }

    private static ScheduledExecutorService getScheduled() {
        if (scheduled == null) {
            synchronized (RedisDiscoveryService.class) {
                if (scheduled == null) {
                    scheduled = new ScheduledThreadPoolExecutor(1, r -> {
                        Thread result = new Thread(r, "RedisDiscoveryServiceHeartbeat");
                        result.setDaemon(true);
                        return result;
                    });
                }
            }
        }
        return scheduled;
    }

    public List newServerInstanceClient(Collection instanceList) {
        int size = instanceList.size();
        List list = new ArrayList<>(size);
        for (ServerInstance instance : instanceList) {
            boolean socketConnected = ServerInstance.isSocketConnected(instance, clusterConfig.getTestSocketTimeoutMs());
            try {
                RedisServerInstanceClient service = new RedisServerInstanceClient(socketConnected, sdkInstance, instance, clusterConfig);
                list.add(service);
            } catch (Exception e) {
                throw new IllegalStateException(
                        String.format("newServerInstanceClient  fail!  account = '%s', IP = '%s', port = %d ",
                                instance.getAccount(), instance.getIp(), instance.getPort()), e);
            }
        }
        return list;
    }

    @Override
    public void registerSdkInstance() {
        // server : set, pub, sub, get
        Map serverInstanceMap = redisTemplate.execute(connection -> {
            redisSetSdkInstance(connection);
            connection.publish(keySdkPubSubBytes, sdkInstanceBytes);
            connection.subscribe(messageServerListener, keyServerPubSubBytes, keyServerPubUnsubBytes);
            return getServerInstanceMap(connection);
        }, true);

        updateServerInstance(serverInstanceMap);
        this.updateServerInstanceScheduledFuture = scheduledUpdateServerInstance();
        this.sdkHeartbeatScheduledFuture = scheduledSdkHeartbeat();
    }

    @Override
    public void addServerListener(ServerListener serverListener) {
        serverListenerList.add(serverListener);
    }

    @Override
    public ReferenceCounted> getServerListRef() {
        while (true) {
            try {
                return serverInstanceClientListRef.open();
            } catch (IllegalStateException ignored) {

            }
        }
    }

    private ScheduledFuture scheduledUpdateServerInstance() {
        if (updateInstanceTimerMs <= 0) {
            return null;
        }
        ScheduledFuture scheduledFuture = this.updateServerInstanceScheduledFuture;
        if (scheduledFuture != null) {
            scheduledFuture.cancel(false);
        }
        return getScheduled().scheduleWithFixedDelay(() -> updateServerInstance(getServerInstanceMap()), updateInstanceTimerMs, updateInstanceTimerMs, TimeUnit.MILLISECONDS);
    }

    private ScheduledFuture scheduledSdkHeartbeat() {
        ScheduledFuture scheduledFuture = this.sdkHeartbeatScheduledFuture;
        if (scheduledFuture != null) {
            scheduledFuture.cancel(false);
        }
        int delay;
        if (redisInstanceExpireSec == MIN_REDIS_INSTANCE_EXPIRE_SEC) {
            delay = 500;
        } else {
            delay = (redisInstanceExpireSec * 1000) / 3;
        }
        return getScheduled().scheduleWithFixedDelay(() -> {
            redisTemplate.execute(connection -> {
                // 续期过期时间
                Boolean success = connection.expire(keySdkSetBytes, redisInstanceExpireSec);
                if (success == null || !success) {
                    redisSetSdkInstance(connection);
                }
                serverHeartbeatCount++;
                return null;
            }, true);
        }, delay, delay, TimeUnit.MILLISECONDS);
    }

    private Boolean redisSetSdkInstance(RedisConnection connection) {
        return connection.set(keySdkSetBytes, sdkInstanceBytes, Expiration.seconds(redisInstanceExpireSec), RedisStringCommands.SetOption.UPSERT);
    }

    public synchronized void updateServerInstance(Map serverInstanceMap) {
        if (serverInstanceMap == null) {
            // serverInstanceMap == null is Redis IOException
            return;
        }
        DifferentComparatorUtil.ListDiffResult diff = DifferentComparatorUtil.listDiff(this.serverInstanceMap.keySet(), serverInstanceMap.keySet());
        if (diff.isEmpty()) {
            return;
        }
        List insertList = diff.getInsertList().stream().map(serverInstanceMap::get).collect(Collectors.toList());
        List deleteList = diff.getDeleteList().stream().map(this.serverInstanceMap::get).collect(Collectors.toList());
        log.info("updateServerInstance insert={}, delete={}", insertList, deleteList);
        List deleteClientList;
        List insertClientList;
        List refs = serverInstanceClientListRef.get();
        if (deleteList.isEmpty()) {
            deleteClientList = Collections.emptyList();
        } else {
            Set deleteAccountSet = deleteList.stream().map(ServerInstance::getAccount).collect(Collectors.toSet());
            deleteClientList = refs.stream().filter(e -> deleteAccountSet.contains(e.getAccount())).collect(Collectors.toList());
            for (RedisServerInstanceClient client : deleteClientList) {
                client.discoveryClose();
            }
            refs.removeIf(next -> {
                for (RedisServerInstanceClient client : deleteClientList) {
                    if (next == client) {
                        return true;
                    }
                }
                return false;
            });
        }
        if (insertList.isEmpty()) {
            insertClientList = Collections.emptyList();
        } else {
            insertClientList = newServerInstanceClient(insertList);
            refs.addAll(insertClientList);
        }
        try {
            notifyServerChangeEvent(insertClientList, deleteClientList);
        } catch (Exception e) {
            log.warn("updateServerInstance notifyChangeEvent error {}", e.toString(), e);
        }
        this.serverInstanceMap = serverInstanceMap;
        this.serverUpdateInstanceCount++;
    }

    private void notifyServerChangeEvent(List insertList,
                                         List deleteList) {
        if (serverListenerList.isEmpty()) {
            return;
        }
        ServerChangeEvent event = new ServerChangeEvent<>(serverUpdateInstanceCount, insertList, deleteList);
        for (ServerListener listener : serverListenerList) {
            listener.onChange(event);
        }
    }

    public Map getServerInstanceMap() {
        RedisCallback> redisCallback = this::getServerInstanceMap;
        return redisTemplate.execute(redisCallback);
    }

    public Map getServerInstanceMap(RedisConnection connection) {
        Map map = new LinkedHashMap<>();
        try (Cursor cursor = connection.scan(keyServerSetScanOptions)) {
            while (cursor.hasNext()) {
                byte[] key = cursor.next();
                if (key == null) {
                    continue;
                }
                byte[] body = connection.get(key);
                if (body == null) {
                    continue;
                }
                ServerInstance instance = instanceServerSerializer.deserialize(body);
                if (instance != null) {
                    map.put(instance.getAccount(), instance);
                }
            }
        } catch (Exception ignored) {
            return null;
        }
        return map;
    }

    @Override
    public void destroy() {
        this.destroy = true;
        redisTemplate.execute(connection -> {
            connection.expire(keySdkSetBytes, 0);
            connection.publish(keyServerPubUnsubBytes, sdkInstanceBytes);
            return null;
        }, true);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy