Netflix中的负载均衡策略
Spring Cloud的负载均衡策略可以通过配置Ribbon搞定,也就是注入实现com.netflix.loadbalancer.IRule的类,当前包含的策略包括
1.RandomRule 随机策略 在while循环内,如果服务地址不为空会不停的循环直到随机出一个可用的服务。
@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        } else {
            Server server = null;
            while(server == null) {
                if (Thread.interrupted()) {
                    return null;
                }
                List<Server> upList = lb.getReachableServers();
                List<Server> allList = lb.getAllServers();
                int serverCount = allList.size();
                if (serverCount == ) {
                    return null;
                }
                int index = this.rand.nextInt(serverCount);
                server = (Server)upList.get(index);
                if (server == null) {
                    Thread.yield();
                } else {
                    if (server.isAlive()) {
                        return server;
                    }
                    server = null;
                    Thread.yield();
                }
            }
            return server;
        }
    }
不过感觉怎么第一个就有坑呢。。upList表示当前可用的服务实例集合,这个集合可以由客户端开启定时任务定期对调用服务进行ping来更新,allList表示当前所有服务实例的集合。
也就是说当存在。通过com.netflix.loadbalancer.BaseLoadBalancer中可见:
     public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];
            BaseLoadBalancer.logger.debug("LoadBalancer:  PingTask executing [{}] servers configured", numCandidates);
            for(int i = ; i < numCandidates; ++i) {
                results[i] = false;
                try {
                    if (ping != null) {
                        results[i] = ping.isAlive(servers[i]);
                    }
                } catch (Exception var7) {
                    BaseLoadBalancer.logger.error("Exception while pinging Server: '{}'", servers[i], var7);
                }
            }
            return results;
        }
     public void runPinger() throws Exception {
            if (BaseLoadBalancer.this.pingInProgress.compareAndSet(false, true)) {
                Server[] allServers = null;
                boolean[] results = null;
                Lock allLock = null;
                Lock upLock = null;
                try {
                    allLock = BaseLoadBalancer.this.allServerLock.readLock();
                    allLock.lock();
                    allServers = (Server[])BaseLoadBalancer.this.allServerList.toArray(new Server[BaseLoadBalancer.this.allServerList.size()]);
                    allLock.unlock();
                    int numCandidates = allServers.length;
                    boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
                    List<Server> newUpList = new ArrayList();
                    List<Server> changedServers = new ArrayList();
                    for(int i = ; i < numCandidates; ++i) {
                        boolean isAlive = resultsx[i];
                        Server svr = allServers[i];
                        boolean oldIsAlive = svr.isAlive();
                        svr.setAlive(isAlive);
                        if (oldIsAlive != isAlive) {
                            changedServers.add(svr);
                            BaseLoadBalancer.logger.debug("LoadBalancer [{}]:  Server [{}] status changed to {}", new Object[]{BaseLoadBalancer.this.name, svr.getId(), isAlive ? "ALIVE" : "DEAD"});
                        }
                        if (isAlive) {
                            newUpList.add(svr);
                        }
                    }
                    upLock = BaseLoadBalancer.this.upServerLock.writeLock();
                    upLock.lock();
                    BaseLoadBalancer.this.upServerList = newUpList;
                    upLock.unlock();
                    BaseLoadBalancer.this.notifyServerStatusChangeListener(changedServers);
                } finally {
                    BaseLoadBalancer.this.pingInProgress.set(false);
                }
            }
        }
如此看来,当upList数量不等于allList数量时,这个server = (Server)upList.get(index);就出问题了!?当然,默认情况下ping的方法是不进行真实健康监测的,即所有服务都是健康的,保证allList.size()=upList.size();不过感觉很怪异。
2.RoundRobinRule 轮询策略,但是有个查找次数的限制,也就是说查了10次都是不可用的服务的话就会警告没有可用服务并返回null了,选择的方式是很简单,取余运算。
  public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        } else {
            Server server = null;
            int count = ;
            while(true) {
                if (server == null && count++ < ) {
                    List<Server> reachableServers = lb.getReachableServers();
                    List<Server> allServers = lb.getAllServers();
                    int upCount = reachableServers.size();
                    int serverCount = allServers.size();
                    if (upCount !=  && serverCount != ) {
                        int nextServerIndex = this.incrementAndGetModulo(serverCount);
                        server = (Server)allServers.get(nextServerIndex);
                        if (server == null) {
                            Thread.yield();
                        } else {
                            if (server.isAlive() && server.isReadyToServe()) {
                                return server;
                            }
                            server = null;
                        }
                        continue;
                    }
                    log.warn("No up servers available from load balancer: " + lb);
                    return null;
                }
                if (count >= ) {
                    log.warn("No available alive servers after 10 tries from load balancer: " + lb);
                }
                return server;
            }
        }
    }
    private int incrementAndGetModulo(int modulo) {
        int current;
        int next;
        do {
            current = this.nextServerCyclicCounter.get();
            next = (current + ) % modulo;
        } while(!this.nextServerCyclicCounter.compareAndSet(current, next));
        return next;
    }
此处的upCount依然是个摆设。。。
3.ClientConfigEnabledRoundRobinRule 默认使用RoundRobinRule 策略 不过字面意思,客户端可配置的,所以可以作为父类扩展
 public void initWithNiwsConfig(IClientConfig clientConfig) {
        this.roundRobinRule = new RoundRobinRule();
    }
    public Server choose(Object key) {
        if (this.roundRobinRule != null) {
            return this.roundRobinRule.choose(key);
        } else {
            throw new IllegalArgumentException("This class has not been initialized with the RoundRobinRule class");
        }
    }
4.WeightedResponseTimeRule 实例初始化的时候会开启一个定时任务,通过定时任务来获取服务响应时间定期维护每个服务的权重
 public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        } else {
            Server server = null;
            while(server == null) {
                List<Double> currentWeights = this.accumulatedWeights;
                if (Thread.interrupted()) {
                    return null;
                }
                List<Server> allList = lb.getAllServers();
                int serverCount = allList.size();
                if (serverCount == ) {
                    return null;
                }
                int serverIndex = ;
                double maxTotalWeight = currentWeights.size() ==  ? 0.0D : ((Double)currentWeights.get(currentWeights.size() - )).doubleValue();
                if (maxTotalWeight < 0.001D) {
                    server = super.choose(this.getLoadBalancer(), key);
                    if (server == null) {
                        return server;
                    }
                } else {
                    double randomWeight = this.random.nextDouble() * maxTotalWeight;
                    int n = ;
                    for(Iterator var13 = currentWeights.iterator(); var13.hasNext(); ++n) {
                        Double d = (Double)var13.next();
                        if (d.doubleValue() >= randomWeight) {
                            serverIndex = n;
                            break;
                        }
                    }
                    server = (Server)allList.get(serverIndex);
                }
                if (server == null) {
                    Thread.yield();
                } else {
                    if (server.isAlive()) {
                        return server;
                    }
                    server = null;
                }
            }
            return server;
        }
    }
    public void maintainWeights() {
            ILoadBalancer lb = WeightedResponseTimeRule.this.getLoadBalancer();
            if (lb != null) {
                if (WeightedResponseTimeRule.this.serverWeightAssignmentInProgress.compareAndSet(false, true)) {
                    try {
                        WeightedResponseTimeRule.logger.info("Weight adjusting job started");
                        AbstractLoadBalancer nlb = (AbstractLoadBalancer)lb;
                        LoadBalancerStats stats = nlb.getLoadBalancerStats();
                        if (stats != null) {
                            double totalResponseTime = 0.0D;//所有实例的响应时间总和
                            ServerStats ss;
                            for(Iterator var6 = nlb.getAllServers().iterator(); var6.hasNext(); totalResponseTime += ss.getResponseTimeAvg()) {
  //通过ss.getResponseTimeAvg()获取每个服务的平均响应时间 然后累加到totalResponseTime中
                                Server server = (Server)var6.next();
                                ss = stats.getSingleServerStat(server);
                            }
                            Double weightSoFar = 0.0D;
                            List<Double> finalWeights = new ArrayList();
                            Iterator var20 = nlb.getAllServers().iterator();
                            while(var20.hasNext()) {
                                Server serverx = (Server)var20.next();
                                ServerStats ssx = stats.getSingleServerStat(serverx);
                                double weight = totalResponseTime - ssx.getResponseTimeAvg();//所有服务的平均响应时间的和-该服务的平均响应时间=该服务的权重
                                weightSoFar = weightSoFar.doubleValue() + weight;
//由于通过集合存储 所以此处采取区间的模式 也就是从0到n,n到...的模式
//比如 三个服务 响应时间分别为10,20,30 则权重分别为(0-50)(50-90)(90-120)
                                finalWeights.add(weightSoFar);
                            }
                            WeightedResponseTimeRule.this.setWeights(finalWeights);
                            return;
                        }
                    } catch (Exception var16) {
                        WeightedResponseTimeRule.logger.error("Error calculating server weights", var16);
                        return;
                    } finally {
                        WeightedResponseTimeRule.this.serverWeightAssignmentInProgress.set(false);
                    }
                }
            }
        }
不过当权重的集合中没有数据的时候,这个类继承了RoundRobinRule 类,就使用轮询的方式选择了。如果存在权重信息则使用this.random.nextDouble() * maxTotalWeight的方式也就是1以内小数*最大权重值区间内的随机数来选取服务索引的方式。跟RandomRule 的模式一样,当选取的服务状态异常的时候会While循环走下去。直到。。。死循环。
5.BestAvailableRule 对所有实例进行迭代,首先过滤掉不可用的服务,然后选出连接数最少的服务返回,继承了ClientConfigEnabledRoundRobinRule类也就是使用了RoundRobinRule策略,也就是loadBalancerStats进行统计服务连接信息为空的时候先采用轮询策略过渡。
 public Server choose(Object key) {
        if (this.loadBalancerStats == null) {
            return super.choose(key);
        } else {
            List<Server> serverList = this.getLoadBalancer().getAllServers();
            int minimalConcurrentConnections = ;
            long currentTime = System.currentTimeMillis();
            Server chosen = null;
            Iterator var7 = serverList.iterator();
            while(var7.hasNext()) {
                Server server = (Server)var7.next();
                ServerStats serverStats = this.loadBalancerStats.getSingleServerStat(server);
                if (!serverStats.isCircuitBreakerTripped(currentTime)) {
                    int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
                    if (concurrentConnections < minimalConcurrentConnections) {
                        minimalConcurrentConnections = concurrentConnections;
                        chosen = server;
                    }
                }
            }
            if (chosen == null) {
                return super.choose(key);
            } else {
                return chosen;
            }
        }
    }
6.RetryRule 采用了轮询策略(内部直接实例化RoundRobinRule使用)的重试策略来获取可用的服务实例。这里有个maxRetryMillis属性用来限定重试的时间,如果首次获取服务实例为空,则开启一个定指定关闭时间的定时线程,在该指定时间内如果没有找到可用的实例就返回null了。默认为500毫秒。(轮询策略内不是10次以内不管找到可用实例与否都返回结果,所以此处可以看成一个次数微微可控的加强版)
  public Server choose(ILoadBalancer lb, Object key) {
        long requestTime = System.currentTimeMillis();
        long deadline = requestTime + this.maxRetryMillis;
        Server answer = null;
        answer = this.subRule.choose(key);
        if ((answer == null || !answer.isAlive()) && System.currentTimeMillis() < deadline) {
            InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis());
            while(!Thread.interrupted()) {
                answer = this.subRule.choose(key);
                if (answer != null && answer.isAlive() || System.currentTimeMillis() >= deadline) {
                    break;
                }
                Thread.yield();
            }
            task.cancel();
        }
        return answer != null && answer.isAlive() ? answer : null;
    }
7.PredicateBasedRule 继承自ClientConfigEnabledRoundRobinRule的一个抽象类。
   public abstract AbstractServerPredicate getPredicate();
    public Server choose(Object key) {
        ILoadBalancer lb = this.getLoadBalancer();
        Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        return server.isPresent() ? (Server)server.get() : null;
    }
使用的时候需要重写getPredicate方法,目测是先过滤一部分服务然后在选择一个服务。
//上边方法this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);调用到这
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
return eligible.size() == ? Optional.absent() :
Optional.of(eligible.get(this.nextIndex.getAndIncrement() % eligible.size()));
//此处可见过滤后的集合为空则返回 Optional.absent()表示不存在对象集合(通过isPresent()方法默认就是false),集合不为空则还是如同轮询算法般取余
}
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));//loadBalancerKey 如果为null的话 则返回当前即可(这个filter的过滤条件是不过滤。。。)
} else {
List<Server> results = Lists.newArrayList();
Iterator var4 = servers.iterator(); while(var4.hasNext()) {
Server server = (Server)var4.next();
//此处进行条件判断 将满足条件的集合返回
if (this.apply(new PredicateKey(loadBalancerKey, server))) {
results.add(server);
}
} return results;
}
}
当然这个抽象类需要我们实现getPredicate()返回AbstractServerPredicate过滤条件(默认全部返回为true,也就是等价于采用轮询的模式了)。
8.AvailabilityFilteringRule实现PredicateBasedRule类,如代码所示,组合条件是一个new AvailabilityPredicate().
private AbstractServerPredicate predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, (IClientConfig)null)).addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build();
public void initWithNiwsConfig(IClientConfig clientConfig) {
this.predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, clientConfig)).addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build();
}
查看过滤条件
    public boolean apply(@Nullable PredicateKey input) {
        LoadBalancerStats stats = this.getLBStats();
        if (stats == null) {
            return true;
        } else {
            return !this.shouldSkipServer(stats.getSingleServerStat(input.getServer()));//下边返回true则这块会把该服务实例过滤掉 返回为!true
        }
    }
   //也就是这块 可以看出 如果断路器当前是开启状态或者当前服务实例的请求连接数大于配置的连接数阈值则进行过滤(默认是2147483647,可以通过clientConfig进行配置 Spring Cloud中也就是<clientName>.<nameSpace>.ActiveConnectionsLimit进行配置)
    private boolean shouldSkipServer(ServerStats stats) {//满足其一条件则会返回true
        return CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped() || stats.getActiveRequestsCount() >= ((Integer)this.activeConnectionsLimit.get()).intValue();
    }
    public Server choose(Object key) {
        int count = ;
        for(Server server = this.roundRobinRule.choose(key); count++ <= ; server = this.roundRobinRule.choose(key)) {
            if (this.predicate.apply(new PredicateKey(server))) {
                return server;
            }
        }
        return super.choose(key);
    }
筛选的条件可以发现是先使用轮询的方式挑选出一个服务实例,然后再进行过滤查看是否满足可以的条件,不满足再轮询下一条。
8.ZoneAvoidanceRule实现PredicateBasedRule类,此处的过滤条件通过构造函数可以看出,字面意思,第一个是根据区域进行筛选,第二个是根据可用性进行筛选
   public void initWithNiwsConfig(IClientConfig clientConfig) {
        ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, clientConfig);
        AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, clientConfig);
        this.compositePredicate = this.createCompositePredicate(zonePredicate, availabilityPredicate);
    }
ZoneAvoidancePredicate的过滤条件如下:
public boolean apply(@Nullable PredicateKey input) {
        if (!ENABLED.get()) {//查看niws.loadbalancer.zoneAvoidanceRule.enabled配置的熟悉是否为true(默认为true)如果为false没有开启分片过滤 则不进行过滤
            return true;
        } else {
            String serverZone = input.getServer().getZone();//获取配置的分片字符串 默认为UNKNOWN
            if (serverZone == null) {
                return true;
            } else {
                LoadBalancerStats lbStats = this.getLBStats();
                if (lbStats == null) {//无负载均衡的要求
                    return true;
                } else if (lbStats.getAvailableZones().size() <= ) {
                    return true;//可用的分片(处于Up状态)<=1 当然就没必要再过滤了
                } else {
                    Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);//key为服务实例配置的Zone
                    if (!zoneSnapshot.keySet().contains(serverZone)) {
                        return true;//如果所有分片的配置都不符合规则 那就没必要继续筛选了 不进行过滤 也就表示当前的分片设置没啥意义了
                    } else {
                        logger.debug("Zone snapshots: {}", zoneSnapshot);
                        Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get());//此处开始挑选可用的区域
                        logger.debug("Available zones: {}", availableZones);
                        return availableZones != null ? availableZones.contains(input.getServer().getZone()) : false;
                    }
                }
            }
        }
    }
对两个过滤条件进行实例化后会通过this.compositePredicate = this.createCompositePredicate(zonePredicate, availabilityPredicate);将过滤条件合并。
private List<AbstractServerPredicate> fallbacks = Lists.newArrayList();//也就是所有过滤条件都存到这个fallback里了
public static CompositePredicate.Builder withPredicate(AbstractServerPredicate primaryPredicate) {
return new CompositePredicate.Builder(primaryPredicate);
}
public CompositePredicate.Builder addFallbackPredicate(AbstractServerPredicate fallback) {
this.toBuild.fallbacks.add(fallback);
return this;
}
ZoneAvoidanceRule实现PredicateBasedRule类所以还是会通过父类的choose方法进行选择。
  public Server choose(Object key) {
        ILoadBalancer lb = this.getLoadBalancer();
        Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        return server.isPresent() ? (Server)server.get() : null;
    }
    public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
        List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
        return eligible.size() ==  ? Optional.absent() : Optional.of(eligible.get(this.nextIndex.getAndIncrement() % eligible.size()));
    }
getEligibleServers方法在AbstractServerPredicate的子类CompositePredicate中进行了重写。
public class CompositePredicate extends AbstractServerPredicate {
    private List<AbstractServerPredicate> fallbacks = Lists.newArrayList();
    private int minimalFilteredServers = ;
    private float minimalFilteredPercentage = 0.0F;
   public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
        List<Server> result = super.getEligibleServers(servers, loadBalancerKey);
        AbstractServerPredicate predicate;
        for(Iterator i = this.fallbacks.iterator(); (result.size() < this.minimalFilteredServers || result.size() <= (int)((float)servers.size() * this.minimalFilteredPercentage)) && i.hasNext(); result = predicate.getEligibleServers(servers, loadBalancerKey)) {
            predicate = (AbstractServerPredicate)i.next();
        }
        return result;
    }
}
先使用父类的getEligibleServers进行过滤一遍( 默认情况下也就是没过滤)
然后按照fallbacks中存储的过滤器顺序进行过滤(此处就行先ZoneAvoidancePredicate然后AvailabilityPredicate)
当然进行下一条过滤是存在条件的 也就是:
(result.size() < this.minimalFilteredServers || result.size() <= (int)((float)servers.size() * this.minimalFilteredPercentage)) && i.hasNext()
当前过滤后的实例结果集大小小于最小过滤集合总数了(此处小于默认值1也就是0了)或者过滤后的结果集大小小于实例总数的最小过滤集合百分比了(此处比例因子是0所有相当于结果集大小还是0了)也就是当前服务示例的结果集以及不满足继续过滤的需求了 但这时候&& i.hasNext() 也就是过滤条件还没结束。。则继续进行过滤。
反复琢磨了会。。没看懂啊!!什么情况,假如父类过滤后result.size()>0的话,那循环条件中直接就(xx;false&&true;xxxx)了直接就退出了,那过滤条件是摆设么。。。换句话说,result.size()=0了 满足(xx;true&&true;xxxx)然后开始执行xxxx的过滤条件了。。问题上result都为空了。还过滤什么?!
总结:
Spring Cloud使用Feign+Ribbon可以方便的实现客户端负载均衡策略,而且提供多种负载规则,当然也可以通过实现AbstractLoadBalancerRule抽象类或者IRule进行扩展。简单方便。
Netflix中的负载均衡策略的更多相关文章
- Spring Cloud中的负载均衡策略
		
在上篇博客(Spring Cloud中负载均衡器概览)中,我们大致的了解了一下Spring Cloud中有哪些负载均衡器,但是对于负载均衡策略我们并没有去详细了解,我们只是知道在BaseLoadBal ...
 - springcloud中的负载均衡策略
		
IRule 这是所有负载均衡策略的父接口,里边的核心方法就是choose方法,用来选择一个服务实例. AbstractLoadBalancerRule AbstractLoadBalancerRule ...
 - spring cloud中通过配置文件自定义Ribbon负载均衡策略
		
一.Ribbon中的负载均衡策略 1.Ribbon中支持的负载均衡策略 AvailabilityFilteringRule:过滤掉那些因为一直连接失败的被标记为circuit tripped的后端se ...
 - 【SpringCloud】Netflix源码解析之Ribbon:负载均衡策略的定义和实现
		
Ribbon负载均衡策略定义 IRule其实就只做了一件事情Server choose(Object key),可以看到这个功能是在LB中定义(要求)的,LB把这个功能委托给IRule来实现.不同的I ...
 - Spring Cloud Ribbon 中的 7 种负载均衡策略
		
负载均衡通器常有两种实现手段,一种是服务端负载均衡器,另一种是客户端负载均衡器,而我们今天的主角 Ribbon 就属于后者--客户端负载均衡器. 服务端负载均衡器的问题是,它提供了更强的流量控制权,但 ...
 - 撸一撸Spring Cloud Ribbon的原理-负载均衡策略
		
在前两篇<撸一撸Spring Cloud Ribbon的原理>,<撸一撸Spring Cloud Ribbon的原理-负载均衡器>中,整理了Ribbon如何通过负载均衡拦截器植 ...
 - 每天学点SpringCloud(三):自定义Eureka集群负载均衡策略
		
相信看了 每天学点SpringCloud(一):简单服务提供者消费者调用,每天学点SpringCloud(二):服务注册与发现Eureka这两篇的同学都了解到了我的套路,没错,本篇博客同样是为了解决上 ...
 - SpringCloud Netflix Ribbon(负载均衡)
		
⒈Ribbon是什么? Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具. Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负 ...
 - Spring Cloud微服务开发笔记5——Ribbon负载均衡策略规则定制
		
上一篇文章单独介绍了Ribbon框架的使用,及其如何实现客户端对服务访问的负载均衡,但只是单独从Ribbon框架实现,没有涉及spring cloud.本文着力介绍Ribbon的负载均衡机制,下一篇文 ...
 
随机推荐
- 利用css3制作毛玻璃的效果
			
忙里偷闲,最近又在看许多比较酷炫的效果.现在基于jquery的插件比较多,但是很多插件的兼容性不是太好,所以原生的才是王道.在日常当中,毛玻璃已经不常见了,那是一个很久远年代的东西了.诺,下面就是毛玻 ...
 - 如何发挥ERP系统中的财务监控职能?
			
ERP系统的管理理念与特点 ERP,是整合了企业管理理念.业务流程.基础数据.人力物力.计算机硬件和软件于一体的企业资源管理系统.ERP系统运用信息技术将企业的资金流.物资流.信息流进行有效的集成,使 ...
 - android--Git上克隆项目遇到的坑
			
直接上图,首先你得有你得GitHub项目地址,如下: 然后打开android studio,选择新建项目时从Git上克隆: 点击clone等待完成,新窗口打开. 打开之后可能.或许.大概.也许会出现下 ...
 - mybatis 的动态SQL
			
在XML 中支持的几种标签: • if • choose.when.otherwise • where • set • trim • foreach OGNL 表达式 1. el or e22. el ...
 - 前端HTML空格与后台PHP utf-8空格
			
今天在处理html input输入框时,发现一个问题: 在用户名输入框中输入admin "'p(中间是一个空格),点保存后台提示数据保存成功,按理应该是未修改,通过chrome调试工具发现传 ...
 - 搜索关键字自动更正 - Oracle Endeca Server
			
做了几个Oracle Endeca 电商项目.每个项目都会有搜过关键字拼写错误更正(Spelling Correction)的需求.淘宝也有类似功能. Oracle Endeca Sever提供了关键 ...
 - layui和bootstrap对比
			
layui和bootstrap 对比 这两个都属于UI渲染框架. layui是国人开发的一套框架,2016年出来的,现在已更新到2.X版本了.比较新,轻量级,样式简单好看. bootstrap 相对来 ...
 - pandas模块安装问题笔记
			
1. # pip install pandas 引用 pandas 时,没有模块 ,进行模块安装,出现一推英文提示 结果 Collecting pandas Could not fetch URL ...
 - qmake
			
https://blog.csdn.net/m0_37876745/article/details/78537556
 - Exchange & Office 365最小混合部署
			
前言 这篇文章的主题是混合部署~ 混合使得本地组织和云环境像一个单一的.协作紧密的组织一样运作.当组织决定进行混合部署,达到本地Exchange Server和Office 365共存的状态时,就会面 ...