【Pigeon源码阅读】区域路由策略实现原理(十)


在几个集群访问策略中,都需要通过ClientManager拿到服务端连接实例,调用了ClientManger的getClient或getAvailableClients方法,在这两个方法内部,都先调用了DefaultClusterListener#getClientList拿到指定服务url的所有连接实例,紧接着调用RouteManger的route或getAvailableClients来过滤获取满足区域要求的连接实例。
ClientManger的两个方法实现如下:

public Client getClient(InvokerConfig<?> invokerConfig, InvocationRequest request, List<Client> excludeClients) {
    // 拿到指定服务url的所有连接实例
    List<Client> clientList = clusterListener.getClientList(invokerConfig);
    // 浅拷贝
    List<Client> clientsToRoute = new ArrayList<Client>(clientList);
    if (excludeClients != null) {
        // 排除部分库护短
        clientsToRoute.removeAll(excludeClients);
    }
    // 通过路由管理器过滤获取一个连接实例
    return routerManager.route(clientsToRoute, invokerConfig, request);
}

public List<Client> getAvailableClients(InvokerConfig<?> invokerConfig, InvocationRequest request) {
    // 拿到指定服务url的所有连接实例
    List<Client> clientList = clusterListener.getClientList(invokerConfig);
    // 通过路由管理器过滤获取所有可能的连接实例
    return routerManager.getAvailableClients(clientList, invokerConfig, request);
}

下面重点看看RouterManager如何应用特定的区域路由策略来过滤连接实例。

DefaultRouteManager

在RouterManager接口中,定义了两个路由策略的核心方法,分别是route和getAvailableClients,在默认子类DefaultRouteManager中实现。其中route方法中调用了getAvailableClients方法,然后选择特定的负载均衡策略选取特定的连接实例返回,具体实现如下:

public Client route(List<Client> clientList, InvokerConfig<?> invokerConfig, InvocationRequest request) {
    if (logger.isDebugEnabled()) {
        for (Client client : clientList) {
            if (client != null) {
                logger.debug("available service provider:\t" + client.getAddress());
            }
        }
    }
    // 获取所有可能的连接
    List<Client> availableClients = getAvailableClients(clientList, invokerConfig, request);
    // 通过客户端负载均衡策略选取合适的唯一客户端连接
    Client selectedClient = select(availableClients, invokerConfig, request);

    // 如果不是活跃的连接,重新选取
    while (!selectedClient.isActive()) {
        logger.info("[route] remove client:" + selectedClient);
        // 去除不活跃的
        availableClients.remove(selectedClient);
        if (availableClients.isEmpty()) {
            // 为空退出,避免死循环
            break;
        }
        selectedClient = select(availableClients, invokerConfig, request);
    }

    // 没有获取到满足要求的活跃的连接
    if (!selectedClient.isActive()) {
        throw new ServiceUnavailableException("no available server exists for service[" + invokerConfig + "], env:"
                + ConfigManagerLoader.getConfigManager().getEnv());
    }
    return selectedClient;
}

/**
 * 按照权重、分组、region规则过滤客户端选择 加入对oneway调用模式的优化判断
 */
public List<Client> getAvailableClients(List<Client> clientList, InvokerConfig<?> invokerConfig,
                                        InvocationRequest request) {
    // 是否启用区域路由策略
    if (regionPolicyManager.isEnableRegionPolicy()) {

        clientList = regionPolicyManager.getPreferRegionClients(clientList, invokerConfig, request);

    }

    List<Client> filteredClients = new ArrayList<Client>(clientList.size());
    for (Client client : clientList) {
        if (client != null) {
            String address = client.getAddress();
            // 获取地址连接权重
            int weight = RegistryManager.getInstance().getServiceWeightFromCache(address);
            if (client.isActive() && weight > 0) {
                // 连接是激活状态且存在权重
                filteredClients.add(client);
            } else if (logger.isDebugEnabled()) {
                logger.debug("provider status:" + client.isActive() + "," + weight);
            }
        }
    }
    // 为空说明没有活跃或地址权重>0的连接
    if (filteredClients.isEmpty()) {
        throw new ServiceUnavailableException("no available server exists for service[" + invokerConfig.getUrl()
                + "] and group[" + RegistryManager.getInstance().getGroup(invokerConfig.getUrl()) + "].");
    }
    return filteredClients;
}

在getAvailableClients方法实现中,开始的第一步操作是判断是否启用区域路由策略,如果启用了,则根据路由策略过滤出合适的连接实例。isEnableRegionPolicy和getPreferRegionClients方法定义在RegionPolicyManager,接下来看RegionPolicyManager的实现

RegionPolicyManager

RegionPolicyManager是一个枚举类,以此实现了单例对象的逻辑。在服务调用方初始化流程中,会调用InvokerBootStrap#startup方法,在这个方法里,会调用RegionPolicyManager.INSTANCE.init()来对区域路由管理器进行初始化,先来看这个方法实现:

public void init() {
    if (!isInitialized) {
        synchronized (RegionPolicyManager.class) {
            if (!isInitialized) {
                // 自动切换区域路由
                register(AutoSwitchRegionPolicy.NAME, null, AutoSwitchRegionPolicy.INSTANCE);
                // 基于权重区域路由
                register(WeightBasedRegionPolicy.NAME, null, WeightBasedRegionPolicy.INSTANCE);
                // 强制区域路由
                register(ForceRegionPolicy.NAME, null, ForceRegionPolicy.INSTANCE);

                // 如果启用路由策略,则初始化本地路由配置
                if (configManager.getBooleanValue(KEY_ENABLEREGIONPOLICY, DEFAULT_ENABLEREGIONPOLICY)) {
                    initRegionsConfig();
                } else {
                    logger.info("Region policy is disabled!");
                }

                // 注册配置监听器,监听路由相关配置变更,则重新初始化本地路由配置
                configManager.registerConfigChangeListener(new InnerConfigChangeListener());
                isInitialized = true;
            }
        }
    }
}

在初始化过程中,分为两大块逻辑,第一块是三种路由策略的注册,第二块是本地路由配置的初始化。

区域路由基础配置

下面先看看几个共用的基础配置:

  1. pigeon.regions.route.enable: 是否启用区域路由策略

  2. pigeon.regions: 以ip网段划分区域的归属,配置示例如:region1:110.120,172.23;region2:192.168;region3:120.128,注意所有的ip段都为xxx.xxx的形式,即只保留ip高16位

  3. pigeon.regions.prefer.xxx,其中xxx是划分的区域名,如前面的region1,region2,region3,具体配置示例如:

    1. pigeon.regions.prefer.region1=region1:3,region2:1,region3:0
    2. pigeon.regions.prefer.region2=region2:10,region3:3,region1:1
    3. pigeon.regions.prefer.region3=region3:3,region1:1,region2:0

    上面配置了特定region的访问优先级策略(冒号后面为region权重,用于weight based policy)

几点注意:

  1. 在pigeon中,region是地区概念,如北京、上海、深圳等,不做流量分配,只做灾备。即在一个 region 不可用时,可以切换到另一个 region。
  2. region层面不做流量分配的原因:一般跨region的访问延时很高,像北京到上海有30ms。正常情况下做流量分配会对服务性能造成比较大的影响。
  3. region 不可用的定义:region 内某一服务的可用服务器数量小于一定比例。
  4. 在特定的region下,有不同的ip段,每个ip段对应概念是逻辑机房(idc),在同region下的idc,在访问速度上没有明显区别,可以忽略地理上的差异。
  5. ldc 层面需要做流量分配,每个 ldc 需要定义各自承担的流量比例。

配置初始化

在initRegionsConfig方法中,对后两个上述配置进行了相关的初始化工作,看看具体的代码实现:

private synchronized void initRegionsConfig(String pigeonRegionsConfig, String regionsPreferConfig) {
    try {
        // 切分配置中的分号,参考示例配置:region1:110.120,172.24;region2:192.168;region3:120.128.223
        String[] regionConfigs = pigeonRegionsConfig.split(";");

        int regionCount = regionConfigs.length;
        // 至少配置一个区域
        if (regionCount <= 0) {
            logger.error("Error! Please check regions config!");
            return;
        }

        // 存储区域名
        Set<String> regionSet = new HashSet<String>();
        // 存储ip段->区域名的映射
        Map<String, String> patternRegionNameMappings = new HashMap<String, String>();
        for (String regionConfig : regionConfigs) {
            // 切分区域名和ip段
            String[] regionPatternMapping = regionConfig.split(":");
            // 区域名
            String regionName = regionPatternMapping[0];
            // 切分多个ip段
            String[] patterns = regionPatternMapping[1].split(",");

            regionSet.add(regionName);

            for (String pattern : patterns) {
                patternRegionNameMappings.put(pattern, regionName);
            }
        }

        //初始化local region
        // 解析获取本地所属ip段,只取前两个逗号以找到ip段归属
        String localRegionPattern = getPattern(configManager.getLocalIp());
        if (patternRegionNameMappings.containsKey(localRegionPattern)) {
            // 找到本机归属区域
            String localRegionName = patternRegionNameMappings.get(localRegionPattern);
            // 权重处理,先获取对应区域的路由优先配置
            if (StringUtils.isBlank(regionsPreferConfig)) {
                regionsPreferConfig = configManager.getStringValue(KEY_REGION_PREFER_BASE + localRegionName);
            }
            // 初始化本机的区域路由优先级配置
            List<Region> regions = initRegionsWithPriority(regionsPreferConfig);
            // 存储区域名->Region对象的一一对应
            Map<String, Region> _regionMap = new HashMap<>();
            // pigeon.regions中配置的所有区域都能找到对应的pigeon.regions.prefer.xxx区域路由优先级配置
            // 需要数量和名字一一对应
            if (regionSet.size() == regions.size()) {
                for (Region region : regions) {
                    if (!regionSet.contains(region.getName())) {
                        logger.error("Error! Regions prefer not match regions config: " + region.getName());
                        return;
                    }
                    _regionMap.put(region.getName(), region);
                }

                //(re)init
                regionArray = Collections.unmodifiableList(regions);// 下面的步骤都基于regionArray
                // 建立ip段->Region的一一映射关系
                initPatterRegionMappings(patternRegionNameMappings);// 初始化pattern region映射
                // 记录本地对应Region
                localRegion = getRegionByName(localRegionName);
                regionMap = ImmutableMap.copyOf(_regionMap);
                // 清空所有连接实例内部的region引用
                clearRegion();
                // 标志区域路由策略完成初始化,能正常使用
                isEnabled = true;
                logger.info("Region route policy switch on! Local region is: " + regionArray.get(0));

            } else {
                logger.error("Error! Regions prefer counts not match regions config!");
            }
        } else {
            logger.error("Error! Can't init local region: " + configManager.getLocalIp());
        }
    } catch (Throwable t) {
        logger.error("Error! Init region policy failed!", t);
    }
}

在整个初始化过程中,最终初始化的几个配置:

  1. isEnabled=true:标志区域路由策略正式生效
  2. patternRegionMappings:pigeon.regions中配置的所有ip段和对应Region(name,priority,weight)的映射关系
  3. regionArray:pigeon.regions中配置的所有Region实例
  4. localRegion:根据本地ip,从patternRegionMappings获取的本地Region
  5. regionMap:regionName->Region实例映射关系

路由策略应用流程

在路由策略生效后,再回到DefaultRouteManager#getAvailableClients方法,第一行通过以下函数判断路由策略是否启用:

public boolean isEnableRegionPolicy() {
    return configManager.getBooleanValue(KEY_ENABLEREGIONPOLICY, DEFAULT_ENABLEREGIONPOLICY) && isEnabled;
}

经过前面的配置初始化,isEnabled已被置为true,于是会通过调用regionPolicyManager#getPreferRegionClients来应用具体的路由策略,来看看相关实现:

public List<Client> getPreferRegionClients(List<Client> clientList, InvokerConfig<?> invokerConfig, InvocationRequest request) {
    // 先根据配置获取具体的路由策略
    RegionPolicy regionPolicy = getRegionPolicy(invokerConfig);

    // 如果获取仍是空,使用AutoSwitchRegionPolicy
    if (regionPolicy == null) {
        regionPolicy = AutoSwitchRegionPolicy.INSTANCE;
    }

    // 应用相应的路由策略过滤出满足路由要求的连接
    clientList = regionPolicy.getPreferRegionClients(clientList, request);
    checkClientsNotNull(clientList, invokerConfig);
    // 采样打点
    Region regionSample = clientList.get(0).getRegion();
    if (regionSample != null) {
        monitor.logEvent("PigeonCall.region", request.getServiceName() + "#" + regionSample.getName(), "");
    }

    return clientList;
}

public RegionPolicy getRegionPolicy(InvokerConfig<?> invokerConfig) {
    // 先通过serviceId加载
    String serviceId = ServiceUtils.getServiceId(invokerConfig.getUrl(), invokerConfig.getSuffix());
    RegionPolicy regionPolicy = regionPolicyMap.get(serviceId);
    if (regionPolicy != null) {
        return regionPolicy;
    }
    // 加载失败再用配置的regionPolicy字符串加载
    regionPolicy = regionPolicyMap.get(invokerConfig.getRegionPolicy());
    if (regionPolicy != null) {
        return regionPolicy;
    }
    // 加载失败再应用默认配置的路由策略AutoSwitchRegionPolicy
    if (DEFAULT_REGIONPOLICY != null) {
        regionPolicy = regionPolicyMap.get(DEFAULT_REGIONPOLICY);
        if (regionPolicy != null) {
            regionPolicyMap.put(invokerConfig.getRegionPolicy(), regionPolicy);
            return regionPolicy;
        } else {
            logger.warn("the regionPolicy[" + DEFAULT_REGIONPOLICY + "] is invalid, only support "
                    + regionPolicyMap.keySet() + ".");
        }
    }
    return null;
}

从上面可以看到,在根据配置获取到具体的路由策略后,是通过调用RegionPolicy#getPreferRegionClients方法来应用具体的路由策略。下面详细看看三种路由策略的原理:

AutoSwitchRegionPolicy 自动切换区域路由

AutoSwitchRegionPolicy是默认的路由策略。

路由规则

  1. 按照优先级选择region中的可用client连接,当region可用率低于设置的切换阈值时,依次选择下一个优先级的region。切换阈值regionSwitchRatio默认为0.5f,即可用的client连接低于50%为region不可用。
  2. 在autoSwitch策略下,通过修改开关值isIdcFilterEnable,可以开启本region中的idc过滤,当同时满足以下3个条件会优先路由本地idc连接:
    1. 同一ip段活跃的连接数idcActive > 同一ip段总连接数idcTotal * 配置比例idcFilterThresHoldRatio
    2. idcActive > 最小阈值idcFilterThresholdLeast
    3. 同一ip段活跃的连接数idcActive > region可用连接数active * 配置比例idcFilterThresHoldRatio

源码实现

基于上面路由规则,看看具体的源码实现:

public List<Client> getPreferRegionClients(List<Client> clientList, InvocationRequest request) {
    return getRegionActiveClients(clientList, request);
}

private List<Client> getRegionActiveClients(List<Client> clientList, InvocationRequest request) {
    int sizeBefore = clientList.size();

    Map<Region, InnerRegionStat> regionStats = new HashMap<Region, InnerRegionStat>();
    List<Region> regionArrays = Lists.newArrayList(regionPolicyManager.getRegionArray());
    // 缓存每个region的统计信息
    for (Region region : regionArrays) {
        regionStats.put(region, new InnerRegionStat());
    }
    // 分发client的region统计信息,遍历连接实例,建立到regionStat的映射,统计总数、活跃连接数等信息,用于后续判断区域是否可用
    for (Client client : clientList) {
        try {
            InnerRegionStat regionStat = regionStats.get(client.getRegion());
            if (regionStat != null) {
                regionStat.addTotal();
                if (client.isActive() && registryManager.getServiceWeightFromCache(client.getAddress()) > 0) {
                    regionStat.addActive();
                    regionStat.addClient(client);
                }
            }
        } catch (Throwable t) {
            logger.error(t);
        }
    }
    // 优先级大小按数组大小排列,遍历所有region
    for (int i = 0; i < regionArrays.size(); ++i) {
        Region region = regionArrays.get(i);
        try {
            InnerRegionStat regionStat = regionStats.get(region);
            // 本地idc,即同一ip段相关信息
            int total = regionStat.getTotal();
            int active = regionStat.getActive();
            List<Client> regionClientList = regionStat.getClientList();

            if (i == 0 && isIdcFilterEnable && regionClientList.size() > 0) { // 开启本地idc(region(0))优先
                int idcTotal = 0;
                int idcActive = 0;
                List<Client> idcClientList = new ArrayList<Client>();
                for (Client client : regionClientList) {
                    // 判断是否为本地idc
                    if (RegionUtils.isInLocalIdc(client.getHost())) {
                        // 统计本地idc的总连接实例数和活跃连接实例数
                        idcClientList.add(client);
                        ++idcTotal;
                        if (client.isActive() && registryManager.getServiceWeightFromCache(client.getAddress()) > 0) {
                            ++idcActive;
                        }
                    }
                }

                // 计算本地idc有效最小连接数阈值
                float idcLeast = idcFilterThresHoldRatio * idcTotal;

                // idc可用client比例合格
                if (idcTotal > 0 && idcActive > 0 && idcActive >= idcLeast) {
                    if (idcActive > idcFilterThresholdLeast || idcActive > idcFilterThresHoldRatio * active) {
                        // idc可用client数量合格 或 idc可用client数量占region可用client数量的比例合格
                        monitor.logEvent("PigeonCall.idc",
                                request.getServiceName() + "#" + RegionUtils.getLocalIdc(), "");
                        return idcClientList;
                    }
                }
            }

            float least = regionSwitchRatio * total;
            // 判断当前活跃的连接实例是否大于阈值,大于则返回当前区域对应的连接实例,否则进入下一个区域的连接判断
            if (total > 0 && active > 0 && active >= least) {
                if (logger.isDebugEnabled()) {
                    logger.debug("b: " + sizeBefore + ", a:" + regionClientList.size());
                }
                return regionClientList;
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug(request.getServiceName() + " skipped region " + region.getName()
                            + ", available clients less than " + least);
                }
                monitor.logEvent("PigeonCall.regionUnavailable",
                        request.getServiceName() + "#" + region.getName(), "");
            }
        } catch (Throwable t) {
            logger.error(t);
        } finally {
            //todo if force region, maybe here
        }
    }

    return clientList;
}

WeightBasedRegionPolicy 基于权重区域路由

路由规则

按照region权重,随机选择特定region中的可用client连接。前面通过pigeon.regions.prefer.xxx配置了客户端所在区域的优先规则,假设客户端机器所在区域为region1,则依据配置:pigeon.regions.prefer.region1=region1:5,region2:3,region3:1,会先对所有的客户端连接实例归类到3个区域。计算总权重为9,先初始化n=0到9的随机数,在regionSet中遍历region,判断n所属region权重区域,如region1=[0,5],region2=[6,8],region3=9,根据n的值,判断所属region,返回相应region的连接,以此实现基于权重的区域路由。

源码实现

基于上面的路由规则,看看具体的代码实现:

public List<Client> getPreferRegionClients(List<Client> clientList, InvocationRequest request) {
    return getRegionActiveClients(clientList, request);
}

// 实际调用下面函数
private List<Client> getRegionActiveClients(List<Client> clientList, InvocationRequest request) {
    // 分region存储clients
    Map<Region, List<Client>> regionClientsMap = new HashMap<Region, List<Client>>();
    List<Region> regionArrays = Lists.newArrayList(regionPolicyManager.getRegionArray());

    for (Region region : regionArrays) {
        regionClientsMap.put(region, new ArrayList<Client>());
    }
    // 建立Region->连接实例List的映射关系
    for (Client client : clientList) {
        List<Client> regionClients = regionClientsMap.get(client.getRegion());
        if (regionClients != null) {
            regionClients.add(client);
        }
    }

    // 初始化region中存在可用client的权重和
    Integer weightSum = 0;
    Set<Region> regionSet = new HashSet<Region>();

    for (Region region : regionClientsMap.keySet()) {
        if (regionClientsMap.get(region).size() > 0) {
            weightSum += region.getWeight();
            regionSet.add(region);
        }
    }

    if (weightSum <= 0) {
        throw new RouteException("Error: weightSum=" + weightSum.toString());
    }

    // 权重随机算法
    Integer n = random.nextInt(weightSum); // n in [0, weightSum)
    Integer m = 0;
    // 判断n属于regionSet中哪一段region
    for (Region region : regionSet) {
        int weight = region.getWeight();
        List<Client> regionClientList = regionClientsMap.get(region);

        if (m <= n && n < m + weight) {

            return regionClientList;

        }

        m += weight;
    }

    return clientList;
}

ForceRegionPolicy 强制区域路由

路由规则

ForceRegionPolicy可以当作是AutoSwitchRegionPolicy的简化版,ForceRegionPolicy按照配置的region优先级,根据forceRegionConf切换规则判断是否使用优先region。切换规则参见AutoSwitchRegionPolicy。两个策略的区别是ForceRegionPolicy使用了同一路由优先级配置(pigeon.regions.force.config),不针对不同的区域有不同的优先级策略(pigeon.regions.prefer.xxx),且没有idc的细化路由策略。

源码实现

忽略部分代码,核心部分实现代码如下:

public class ForceRegionPolicy implements RegionPolicy {
    private ForceRegionPolicy() {
        // 读取配置
        String forceRegionConf = configManager.getStringValue(KEY_REGION_FORCE_CONFIG, "shanghai,beijing");
        // 解析配置,得到路由优先级配置
        initForceRegionConfig(forceRegionConf);
        // 注册配置动态变更监听
        configManager.registerConfigChangeListener(new InnerConfigChangeListener());
    }

    private void initForceRegionConfig(String forceRegionConfig) {
        // 按逗号切分
        if (StringUtils.isNotBlank(forceRegionConfig)) {
            forceRegPrefer = forceRegionConfig.split(",");
        }
    }

    @Override
    public List<Client> getPreferRegionClients(List<Client> clientList, InvocationRequest request) {
        List<Region> forceRegions = getForceRegionList();
        if (forceRegions != null) {
            return getRegionActiveClients(clientList, request, forceRegions);
        }

        return clientList;
    }

    private List<Region> getForceRegionList() {
        // 要求强制路由的区域和pigeon.regions配置的区域有唯一映射
        Map<String, Region> regionMap = regionPolicyManager.getRegionMap();
        if (forceRegPrefer.length != regionMap.size()) {
            logger.debug("Force region config size not match regions config, please check!");
            return null;
        }

        List<Region> _regionArr = new ArrayList<>();
        for (String reg : forceRegPrefer) {
            Region _region = regionMap.get(reg);
            if (_region == null) {
                logger.debug("Force region config not match regions config: " + reg);
                return null;
            }
            _regionArr.add(_region);
        }

        return _regionArr;
    }

    private List<Client> getRegionActiveClients(List<Client> clientList, InvocationRequest request, List<Region> regionArrays) {
        int sizeBefore = clientList.size();

        Map<Region, InnerRegionStat> regionStats = new HashMap<Region, InnerRegionStat>();

        for (Region region : regionArrays) {
            // 缓存每个region的统计信息
            regionStats.put(region, new InnerRegionStat());
        }

        for (Client client : clientList) {
            // 分发client的region统计信息,遍历连接实例,建立到regionStat的映射,统计总数、活跃连接数等信息,用于后续判断区域是否可用
            try {
                InnerRegionStat regionStat = regionStats.get(client.getRegion());
                if (regionStat != null) {
                    regionStat.addTotal();
                    if (client.isActive() && registryManager.getServiceWeightFromCache(client.getAddress()) > 0) {
                        regionStat.addActive();
                        regionStat.addClient(client);
                    }
                }
            } catch (Throwable t) {
                logger.error(t);
            }
        }

        for (int i = 0; i < regionArrays.size(); ++i) {
            // 优先级大小按数组大小排列,遍历所有region
            Region region = regionArrays.get(i);
            try {
                InnerRegionStat regionStat = regionStats.get(region);
                int total = regionStat.getTotal();
                int active = regionStat.getActive();
                List<Client> regionClientList = regionStat.getClientList();

                float least = regionSwitchRatio * total;
                // 判断当前活跃的连接实例是否大于阈值,大于则返回当前区域对应的连接实例,否则进入下一个区域的连接判断
                if (total > 0 && active > 0 && active >= least) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("b: " + sizeBefore + ", a:" + regionClientList.size());
                    }
                    return regionClientList;
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug(request.getServiceName() + " skipped region " + region.getName()
                                + ", available clients less than " + least);
                    }
                    monitor.logEvent("PigeonCall.forceRegionUnavailable",
                            request.getServiceName() + "#" + region.getName(), "");
                }
            } catch (Throwable t) {
                logger.error(t);
            } finally {
                // maybe release work
            }
        }

        return clientList;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值