Dubbo负载均衡策略实现[通俗易懂]

Dubbo负载均衡策略实现[通俗易懂]一、dubbo版本说明基于dubbo版本2.6.2讲解二、负载均衡的接口关系@SPI(RandomLoadBalance.NAME)publicinterfaceLoadBalance{@Adaptive(“loadbalance”)<T>Invoker<T>select(List<Invoker<T>>invokers,URLurl,Invocationinvocation)throwsRp…

大家好,又见面了,我是你们的朋友全栈君。

一、dubbo版本说明

基于dubbo版本2.6.2讲解

二、负载均衡的接口关系

Dubbo负载均衡策略实现[通俗易懂]

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
 
}

1.根据@SPI(RandomLoadBalance.NAME)注解可以知道dubbo的默认负载均衡策略是RandomLoadBalance。

2.根据@Adaptive(“loadbalance”)注解我们可以在URL中通过loadbalance=xxx配置来动态指定select方法的负载均衡算法。

名称解释:

@SPI:扩展点注解

@Adaptive: 扩展点自适应注解

URL(元数据) 格式 :dubbo://ip:port/service.DemoService?anyhost=true&application=srcAnalysisClient&check=false&dubbo=2.8.4&generic=false&interface=service.DemoService&loadbalance=consistenthash&methods=sayHello,retMap&pid=14648&sayHello.timeout=20000&side=consumer&timestamp=1493522325563

public abstract class AbstractLoadBalance implements LoadBalance {
    
    // 计算权重
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        // 随着服务的启动时间越来越长,慢慢提升权重,直到weight
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 为空校验
        if (invokers == null || invokers.isEmpty())
            return null;
        // 如果只有一个可用服务直接返回该服务
        if (invokers.size() == 1)
            return invokers.get(0);
        // 调用具体的负载均衡算法
        return doSelect(invokers, url, invocation);
    }
    
    // 需要子类实现  (设计模式:模板模式)
    protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
    
    // 获取当前服务的权重
    protected int getWeight(Invoker<?> invoker, Invocation invocation) {
        // 获取服务配置的权重值,默认值为100
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
        if (weight > 0) {
            // 获取服务的启动时间
            long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
            if (timestamp > 0L) {
                // 求差值,得到预热时间(运行了多久)
                int uptime = (int) (System.currentTimeMillis() - timestamp);
                // 获取配置的总预热时间,默认值10分钟
                int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
                // 服务运行的时间少于预热时间,那么需要重新计算权重weight(即需要降权)
                if (uptime > 0 && uptime < warmup) {
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }
        return weight;
    }

}

三、dubbo内置的负载均衡策略

image.png

 

(1)RandomLoadBalance:加权随机负载均衡(dubbo默认的策略)

(2) RoundRobinLoadBalance:加权轮询负载均衡

  (3) LeastActiveLoadBalance:最少活跃调用数负载均衡

  (4) ConsistentHashLoadBalance:一致性哈希负载均衡

1.加权随机负载均衡(RandomLoadBalance)

Dubbo的加权随机负载均衡是按照权重设置为Provider分配随机概率。比如,有10个 Provider,并不是说,每个 Provider 的概率都是一样的,而是要结合这10个 Provider 的权重来分配概率。

Provider的权重可以在Dubbo Admin平台中设置,默认权重值是100。

调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

源码:

public class RandomLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "random";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // 可用服务数量
        int totalWeight = 0; // 所有服务权重总和
        boolean sameWeight = true; // 所有服务的权重是否都一样
        
        // 遍历所有服务
        for (int i = 0; i < length; i++) {
            // 计算权重
            int weight = getWeight(invokers.get(i), invocation);
            // 权重累加
            totalWeight += weight;
            // 比较每个服务的权重值是否一样
            if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) {
                sameWeight = false;
            }
        }
        
        // 如果不是所有的服务权重都相同,那么基于权重来随机选择。权重越大的,被选中的概率越大。
        if (totalWeight > 0 && !sameWeight) {
            // 从[0,totalWeight)范围中随机获取一个位移
            int offset = random.nextInt(totalWeight);
            // 遍历所有服务
            for (int i = 0; i < length; i++) {
                // 累减权重
                offset -= getWeight(invokers.get(i), invocation);
                // 如果位移小于0,则选中服务
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        
         // 如果所有服务的权重都相同或者权重总和小于等于零,直接random.nextInt随机从invokers中选择一个
        return invokers.get(random.nextInt(length));
    }

}

举例:

Provider

Weight

i

A

10

0

B

20

1

C

30

2

D

40

3

totalWeight = 100

|        |          |                   |                          |
+-------------------------------50---------------------------------+  totalWeight
1       10         30                   60                        100
    
|---A----|----B-----|--------c----------|-------------D------------|  概率区间


假如 offset = random.nextInt(100)为50

第一次遍历i=0, offset = 50-10 = 40
第二次遍历i=1, offset = 40-20 = 20
第三次遍历i=2, offset = 20-30 = -10,小于零则选中服务C

2.加权轮询负载均衡(RoundRobinLoadBalance)

根据设置的权重判断轮询的比例。

存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。解决办法 :结合权重,把第二台机(性能低的)的权重设置低一点。

源码:

public class RoundRobinLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "roundrobin";
    
    // key = interface + methodname,value = 轮询序号
    private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // key = interface + methodname
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        int length = invokers.size(); // 可用服务数量
        int maxWeight = 0; // 最终保存了服务中最大的权重值
        int minWeight = Integer.MAX_VALUE; // 最终保存了服务中最小的权重值
        // key为服务,value为服务的权重计数器
        final LinkedHashMap<Invoker<T>, IntegerWrapper> invokerToWeightMap = new LinkedHashMap<Invoker<T>, IntegerWrapper>();
        int weightSum = 0;// 所有服务权重总和
        // 遍历所有服务
        for (int i = 0; i < length; i++) {
            // 计算权重
            int weight = getWeight(invokers.get(i), invocation);
            // 选则最大的权重值并赋值给maxWeight
            maxWeight = Math.max(maxWeight, weight);
            // 选择最小的权重值并赋值给minWeight
            minWeight = Math.min(minWeight, weight);
            if (weight > 0) {
                // 存储服务及其权重值
                invokerToWeightMap.put(invokers.get(i), new IntegerWrapper(weight));
                // 权重累加
                weightSum += weight;
            }
        }
        // 获取当前的轮询序号
        AtomicPositiveInteger sequence = sequences.get(key);
        if (sequence == null) {
            sequences.putIfAbsent(key, new AtomicPositiveInteger());
            sequence = sequences.get(key);
        }
        // 获取当前的轮询序号,用于取模。轮询号自增。
        int currentSequence = sequence.getAndIncrement();
        
        // 服务提供者之间的权重有差别,需要按权重轮询
        if (maxWeight > 0 && minWeight < maxWeight) {
            // 当前轮询序号 与 服务提供者权重总和 取模
            int mod = currentSequence % weightSum;
            // 从0循环直到最大权重
            for (int i = 0; i < maxWeight; i++) {
                // 遍历服务
                for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {
                    // 服务提供者
                    final Invoker<T> k = each.getKey();
                    // 服务的权重值
                    final IntegerWrapper v = each.getValue();
                    // mod为0并且对应的服务提供者的权重计算器大于0
                    if (mod == 0 && v.getValue() > 0) {
                        // 返回服务提供者
                        return k;
                    }
                    // mode不等于0,服务提供者的权重计数器大于0
                    if (v.getValue() > 0) {
                        // 服务提供者的权重计数器减1
                        v.decrement();
                        // mod减1
                        mod--;
                    }
                }
            }
        }
        // 如果各服务提供者权重都相同,则直接对服务提供者取模。
        return invokers.get(currentSequence % length);
    }
    
    // Integer包装类,主要包含了一个自减方法,用于服务提供者的权重计数
    private static final class IntegerWrapper {
        private int value;

        public IntegerWrapper(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public void decrement() {
            this.value--;
        }
    }

}

问题:以上版本代码在mod == 0 && v.getValue() > 0 条件成立时才会被返回相应的 Invoker。假如 mod 很大,比如 10000,50000,甚至更大时,doSelect 方法需要进行很多次计算才能将 mod 减为0。由此可知,doSelect 的效率与 mod 有关,时间复杂度为 O(mod)。mod 又受最大权重 maxWeight 的影响,因此当某个服务提供者配置了非常大的权重,此时 RoundRobinLoadBalance 会产生比较严重的性能问题。该问题最初是在issue #2578中被反馈出来。

参考:【dubbo】负载均衡 RoundRobinLoadBalance Dubbo-2.6.5 提供的最新算法!线程安全性有什么影响?_qfzhangwei的专栏-CSDN博客_dubbo 线程安全

3.最少活跃调用数负载均衡(LeastActiveLoadBalance)

最少活跃调用数,如果活跃数相同则随机调用。

活跃数指调用前后计数差。使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差会越大。举个例子:每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A,B的活跃数分别是1,0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使慢的机器A收到少的请求。

Dubbo中使用ActiveLimitFilter过滤器来计算每个接口方法的活跃数。

源码:

public class LeastActiveLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "leastactive";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // 可用服务数量
        int leastActive = -1; // 最少活跃数的初始值是-1
        int leastCount = 0; // 用来记录具有相同最少活跃数(leastActive)的服务提供者的数量
        int[] leastIndexs = new int[length]; // 用来记录具有相同最少活跃数(leastActive)的服务提供者的索引
        int totalWeight = 0; // 所有服务权重总和
        int firstWeight = 0; //初始值 用于比较
        boolean sameWeight = true; // 所有服务提供者的权重是否都相同
        // 遍历所有服务提供者
        for (int i = 0; i < length; i++) {
            // 服务提供者
            Invoker<T> invoker = invokers.get(i);
            // 获取当前这个提供者的活跃数
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); 
             // 获取当前服务配置的权重值 (后面版本已改为用getWeight获取权限)
            int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
            // 第一个服务 或者 当前提供者活跃数小于最少活跃数
            if (leastActive == -1 || active < leastActive) {
                // 记录当前最少活跃数
                leastActive = active;
                // 重置计数
                leastCount = 1; 
                // 在leastIndexs的0位置记录当前提供者(最少活跃数的提供者)的索引
                leastIndexs[0] = i; 
                // 总权重就是当前服务提供者的权重
                totalWeight = weight;
                // 将当前提供者的权重赋值给firstWeight
                firstWeight = weight;
                // 重置权重是否相同标识为true
                sameWeight = true;
            } else if (active == leastActive) { // 当前提供者的活跃数等于最少活跃数
                // 记录当前提供者的索引
                leastIndexs[leastCount++] = i; 
                // 权重累加
                totalWeight += weight;
                // 所有提供者的权重是否相同
                if (sameWeight && i > 0 && weight != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // 如果恰好有一个提供者具有最少活跃数,那么直接返回这个提供者
        if (leastCount == 1) {
            // 从leastIndexs的0位置获取提供者在invokers中的索引
            return invokers.get(leastIndexs[0]);
        }
        // 活跃数相同
        // 如果每个提供者有不同的权重 并且 总权重大于0
        if (!sameWeight && totalWeight > 0) {
            // 从[0,totalWeight)范围中随机获取一个位移
            int offsetWeight = random.nextInt(totalWeight);
            // 遍历具有相同活跃数的提供者
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexs[i];
                 // 累减权重
                offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
                  // 如果位移小于等于0,则选中服务
                if (offsetWeight <= 0)
                    return invokers.get(leastIndex);
            }
        }
        // 所有提供者权重相同或者总权重等于0,直接random.nextInt随机从invokers中选择一个
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}

4.一致性哈希负载均衡(ConsistentHashLoadBalance)

一致性哈希负载均衡可以让参数相同的请求每次都路由到相同的服务提供者上。这种负载均衡的方式可以让请求相对平均。当某一服务提供者“挂”了,原本发往该提供者的请求,基于虚拟节点,会平摊到其他提供者,不会引起剧烈的变动。

Dubbo框架使用了优化过的Ketama一致性Hash.这种算法为每个真实的节点再创建多个虚拟节点,让节点在环形(通过TreeMap实现)上的分布更加均匀,后续的调用也会随之更加均匀。

源码:

public class ConsistentHashLoadBalance extends AbstractLoadBalance {

    private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();

    @SuppressWarnings("unchecked")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // key = 接口名 + 方法名
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        // 把所有可以调用的提供者列表进行Hash
        int identityHashCode = System.identityHashCode(invokers);
        
        // 查询一致性hash选择器
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        
        // 若不存在选择器 或者 当前服务提供者列表的hashCode和之前的不相等,说明服务提供者列表发生了变化,则重新创建选择器
        if (selector == null || selector.identityHashCode != identityHashCode) {
            // 创建一致性hash选择器
            selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
            // 重新查询一致性hash选择器
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        // 从选择器中返回服务提供者
        return selector.select(invocation);
    }
    
    // 一致性hash选择器
    private static final class ConsistentHashSelector<T> {
       
        // 存储虚拟节点
        private final TreeMap<Long, Invoker<T>> virtualInvokers;
        
        // 虚拟节点个数
        private final int replicaNumber;
        
        // 服务提供者列表hashCode
        private final int identityHashCode;
        
        // 参数位置数组
        private final int[] argumentIndex;

        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            // 创建TreeMap来保存虚拟提供者节点。TreeMap是按照key的值从小到大排序的。
            this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
            
            this.identityHashCode = identityHashCode;
            
            // 获取服务提供者的Url元数据
            URL url = invokers.get(0).getUrl();
            
            // 获取所配置的虚拟结点数,缺省值160个。如果要修改,请配置<dubbo:parameter key="hash.nodes" value="320" />
            this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
            
            // 获取需要hash的参数位置,缺省是0,0指的是方法里的第一个参数。如果要修改,请配置<dubbo:parameter key="hash.arguments" value="0,1" />
            String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
            
            // 创建参数位置数组
            argumentIndex = new int[index.length];
            for (int i = 0; i < index.length; i++) {
                argumentIndex[i] = Integer.parseInt(index[i]);
            }
           
            // 对每个提供者生成replicaNumber个虚拟结点,并存放于TreeMap中
            for (Invoker<T> invoker : invokers) {
                // 服务提供者的 address = ip:port
                String address = invoker.getUrl().getAddress();
                
                for (int i = 0; i < replicaNumber / 4; i++) {
                    // address+i 做摘要
                    byte[] digest = md5(address + i);
                    
                    for (int h = 0; h < 4; h++) {
                        // 计算位置
                        long m = hash(digest, h);
                        // 将提供者放入TreeMap
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }
        
        // 选择服务提供者
        public Invoker<T> select(Invocation invocation) {
            // 根据请求参数来生成Key
            String key = toKey(invocation.getArguments());
            // 根据这个参数生成消息摘要
            byte[] digest = md5(key);
            // 调用hash(digest, 0),将消息摘要转换为hashCode,这里仅取0-31位来生成HashCode
            // 调用sekectForKey方法选择结点
            return selectForKey(hash(digest, 0));
        }

        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            // 根据需要hash的参数的位置从args数组中取得参数并进行拼接
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }
            return buf.toString();
        }

        private Invoker<T> selectForKey(long hash) {
            // 以下部分形成了环形查询
            // tailMap方法其实就是是返回键值大于或等于key的那部分,再使用firstEntry方法获取这部分的第一个
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
            // 如果entry为null说明这个hash值就是最大的了,要想找对应的服务提供者,就要找TreeMap的第一个元素
        	if (entry == null) {
        		entry = virtualInvokers.firstEntry();
        	}
            // 返回服务提供者
        	return entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                    | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                    | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                    | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }

        private byte[] md5(String value) {
            MessageDigest md5;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.reset();
            byte[] bytes;
            try {
                bytes = value.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.update(bytes);
            return md5.digest();
        }

    }

}

hash.jpg

参考资料:

Dubbo的负载均衡 – twoheads – 博客园

源码分析Dubbo负载算法_中间件兴趣圈-CSDN博客

【dubbo】负载均衡 RoundRobinLoadBalance Dubbo-2.6.5 提供的最新算法!线程安全性有什么影响?_qfzhangwei的专栏-CSDN博客_dubbo 线程安全

深度解析dubbo负载均衡之LeastActiveLoadBalance_猿上生活-CSDN博客

Dubbo源码学习–ConsistentHashLoadBalance负载均衡(五)_井底之蛙-CSDN博客

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/157325.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • IP地址范围怎么算_ip地址数目怎么算

    IP地址范围怎么算_ip地址数目怎么算1、如果掩码、IP等信息如下:2、我们可以看到,子网掩码为255.255.255.240,因为0-255有256个数字,所以256-240=16。也就是这个网段有16个IP地址。3、我们现在使用的IP地址是什么,或者是网关,最后的一个数字就好。IP是203,网关是193。4、找到IP段就能判断可用IP是多少。这时因为每个IP段都是由四部分组成,分别是网络号、网关、可用IP、广播号。…

    2022年10月19日
  • 动态规划算法求最长回文子串

    动态规划算法求最长回文子串给出了动态规划方法求最长回文子串的程序及分析。

  • navicat 15 最新 激活码_在线激活

    (navicat 15 最新 激活码)JetBrains旗下有多款编译器工具(如:IntelliJ、WebStorm、PyCharm等)在各编程领域几乎都占据了垄断地位。建立在开源IntelliJ平台之上,过去15年以来,JetBrains一直在不断发展和完善这个平台。这个平台可以针对您的开发工作流进行微调并且能够提供…

  • Unity Odin从入门到精通(三):静态检查器详解

    Unity Odin从入门到精通(三):静态检查器详解前言:开发者可以使用静态检查器来选择项目工程、Unity引擎、.Net框架中所有的类型,并查看其所有的静态成员。打开静态检查器:在Unity的菜单栏中选择【Tools->OdinInspector->StaticInspector】菜单项来打开静态检查器。如下图所示:查看静态检查器的源码:首先在Rider当中切换到Assemblies视图。接着在该视图的列表当中选择Sirenix.OdinInspector.Editor程序集。然后从该程序集的列表当中选择Sirenix.Odi

  • 删除链表倒数第n个节点_求链表的倒数第m个元素

    删除链表倒数第n个节点_求链表的倒数第m个元素原题链接给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。进阶:你能尝试使用一趟扫描实现吗?示例 1:输入:head = [1,2,3,4,5], n = 2输出:[1,2,3,5]示例 2:输入:head = [1], n = 1输出:[]示例 3:输入:head = [1,2], n = 1输出:[1]提示:链表中结点的数目为 sz1 <= sz <= 300 <= Node.val <= 1001 <= n <= s

  • 2021pycharm最新激活码【2021免费激活】「建议收藏」

    (2021pycharm最新激活码)好多小伙伴总是说激活码老是失效,太麻烦,关注/收藏全栈君太难教程,2021永久激活的方法等着你。https://javaforall.cn/100143.htmlIntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,上面是详细链接哦~00OE5RWT28-eyJsaWNlbnNlSWQiOi…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号