从开源框架理解设计模式系列#Strategy策略模式

目录

what什么是策略模式

why为什么需要策略模式

how如何实现策略模式

开源框架经典案例 

Dubbo中策略模式的使用

Dubbo中负载均衡LoadBalance策略分析

Dubbo中集群Cluster容错策略

Dubbo中使用的LoadingStrategy的使用 

ShardingSphere中分片算法和分片策略的使用

ShardingAlgorithm分片算法

ShardingStrategy分片策略

JDK中ThreadPoolExecutor策略模式

使用场景 

优缺点对比

优点

缺点

参考资料


what什么是策略模式

        Gof定义:定义一系列的算法, 把它们一个个封装起来,  并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

        HeadFirst定义:定义了算法族,分别封装起来,让它们之间可以相互替代,此模式让算法的变化独立于使用算法的客户。

why为什么需要策略模式

        完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。

        在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。

        在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。

        除了提供专门的查找算法类之外,还可以在客户端程序中直接包含算法代码,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。

        为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。

how如何实现策略模式

策略模式的主要角色如下。

  1. 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
  2. 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
  3. 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

策略模式的结构图

开源框架经典案例 

        策略模式应该是一个尤其高频的设计模式,不管是开源框架,还是日常工作,在日常工厂中,我们一个枚举的switch,代码里面if-else的逻辑,都可以用策略模式是实现。

Dubbo中策略模式的使用

        Dubbo中大量使用策略模式进行设计,比如负载均衡策略、集群容灾策略、线程池策略等等,接下来就详细介绍一下他们的具体使用和源码。

Dubbo中负载均衡LoadBalance策略分析

        负载均衡是分布式架构的标配,以一个请求来说,从F5、nginx、rpc调用,每一层都应该使用负载均衡进行流量控制和资源隔离。为了避免大量请求到一个或者几个服务提供方,从而使得这些机器负载很高,甚至导致服务不可用,需要一个复杂均衡策略。Dubbo提供了很多的负载均衡策略,默认是random,也就是随即调用。

        先介绍一下Dobbo提供的负载均衡策略有以下几种。

 

  • RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。
  • RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。
  • LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。
  • ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。
  • ShortestResponseLoadBalance: 最短响应时间负载均衡,核心思想是基于rt时间进行选择。

源代码介绍

        在dubbo的每次调用过程中,会先去获取负载均衡策略,然后将负载均衡策略传给doInvoke方法,执行真正的远程调用。具体代码如下。


public abstract class AbstractClusterInvoker<T> implements ClusterInvoker<T> {  
    ....
    @Override
    public Result invoke(final Invocation invocation) throws RpcException {
        checkWhetherDestroyed();

        // binding attachments into invocation.
        Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
            ((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
        }
        
        List<Invoker<T>> invokers = list(invocation);
        // 获取负载均衡策略
        LoadBalance loadbalance = initLoadBalance(invokers, invocation);
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        // 使用相应策略,执行远程调用
        return doInvoke(invocation, invokers, loadbalance);
    }
    ...
 /**
     * Init LoadBalance.
     * <p>
     * if invokers is not empty, init from the first invoke's url and invocation
     * if invokes is empty, init a default LoadBalance(RandomLoadBalance)
     * </p>
     *
     * @param invokers   invokers
     * @param invocation invocation
     * @return LoadBalance instance. if not need init, return null.
     */
    protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invocation) {
        if (CollectionUtils.isNotEmpty(invokers)) {
            return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                    .getMethodParameter(RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE));
        } else {
            return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
        }
    }
}

Dubbo中集群Cluster容错策略

        不管是单体应用,还是微服务架构应用,当我们进行系统设计的使用,都应该考虑发生异常情况时如何处理,常规思维,我们会想是继续执行呢,还是重试呢,还是直接抛异常呢。

        Dubbo提供了多种集群容错策略,默认是FailOverCluster,也就是失败重试,默认的重试次数两次,完全可以自定义。

 

  • Failfast Cluster:快速失败当服务消费方调用服务提供者失败后,立即报错,也就是只调用一次。通常这种模式用于非幂等性的写操作。
  • Failsafe Cluster:失败安全当服务消费者调用服务出现异常时,直接忽略异常。这种模式通常用于写入审计日志等操作。
  • Failback Cluster:失败自动恢复当服务消费端用服务出现异常后,在后台记录失败的请求,并按照一定的策略后期再进行重试。这种模式通常用于消息通知操作。
  • Forking Cluster:并行调用当消费方调用一个接口方法后,Dubbo Client会并行调用多个服务提供者的服务,只要一个成功即返回。这种模式通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
  • Broadcast Cluster:广播调用当消费者调用一个接口方法后,Dubbo Client会逐个调用所有服务提供者,任意一台调用异常则这次调用就标志失败。这种模式通常用于通知所有提供者更新缓存或日志等本地资源信息。
  • Failover Cluster:失败重试当调用失败后,会自动切换到其他服务器进行重试,这通常用于读操作或者具有幂等的写操作。
  • AvailableCluster:可用实例遍历调用目前可用的实例(只调用一个),如果当前没有可用的实例,则抛出异常
  • Mergeable Cluster:合并结果将集群中的调用结果聚合起来返回结果,通常和group一起配合使用。通过分组对结果进行聚合并返回聚合后的结果,比如菜单服务,用group区分同一接口的多种实现,现在消费方需从每种group中调用一次并返回结果,对结果进行合并之后返回,这样就可以实现聚合菜单项。
  • ZoneAware Cluster:多zone模式多注册中心订阅的场景,注册中心集群间的负载均衡,这里面又有子策略,分别是

    • 指定优先级:preferred="true"注册中心的地址将被优先选择

    • 同中心优先:检查当前请求所属的区域,优先选择具有相同区域的注册中心
    • 权重轮询:根据每个注册中心的权重分配流量
    • 缺省值:选择一个可用的注册中心
    • ​<dubbo:registry address="zookeeper://127.0.0.1:2181" preferred="true" />
      
      <dubbo:registry address="zookeeper://127.0.0.1:2181" zone="beijing" />
      
      <dubbo:registry id="beijing" address="zookeeper://127.0.0.1:2181" weight="100" />
      
      <dubbo:registry id="shanghai" address="zookeeper://127.0.0.1:2182" weight="10" />
      

策略选择逻辑

        策略选择,反向思考一下应该是在reference的配置过程中,因此从ReferenceConfig这个类入手。看源码会发现,在初始化过程中,从init到doProxy里面有策略的选择,同时在没有注入的时候,默认会使用failover策略。

        首先看一下官网的这个图,官网是当前最新的,但是图里面的内容没更新,估计是维护人员原图找不到了就懒得改。

cluster

 

public class ReferenceConfig<T> extends ReferenceConfigBase<T> {

    ....
public synchronized void init() {
 ...
//创建代理引用
  ref = createProxy(map);
...
}

private T createProxy(Map<String, String> map) {
     // for multi-subscription scenario, use 'zone-aware' policy by default
                    String cluster = registryURL.getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME);
                    // The invoker wrap sequence would be: ZoneAwareClusterInvoker(StaticDirectory) -> 
FailoverClusterInvoker(RegistryDirectory, routing happens here) -> Invoker
//策略模式获取集群容灾策略,没有的话默认failover
                    invoker = Cluster.getCluster(cluster, false).join(new StaticDirectory(registryURL, invokers));
       
}
//默认策略在这里定义,不可修改
@SPI(Cluster.DEFAULT)
public interface Cluster {
    String DEFAULT = FailoverCluster.NAME;

    /**
     * Merge the directory invokers to a virtual invoker.
     *
     * @param <T>
     * @param directory
     * @return cluster invoker
     * @throws RpcException
     */
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;

    static Cluster getCluster(String name) {
        return getCluster(name, true);
    }

    static Cluster getCluster(String name, boolean wrap) {
        if (StringUtils.isEmpty(name)) {
            name = Cluster.DEFAULT;
        }
        return ExtensionLoader.getExtensionLoader(Cluster.class).getExtension(name, wrap);
    }
}

Dubbo中使用的LoadingStrategy的使用 

        Dubbo中有一个LoadingStrategy接口,这个接口的作用是dubbo通过遍历strategies,来读取某一个接口在所有策略下的所有实现类,并以k - v的方式缓存到extensionClasses中。其中strategies中的每一项,代表着不同的spi加载策略,每一种加载策略,都有相应的目录结构。但是到这一步,还仅仅读取了实现类的class对象,而没有进行初始化。后续使用实现类时,可以直接拿到class对象,而不需要再去读取文件了。

       加载过程是ServiceLoader.load方法,利用的是jdk自带的spi机制,加载 LoadingStrategy类的实现类,jdk的spi机制约定了,接口实现类的描述文件存放位置为:META-INFO/services/,文件名称为接口的全限定名,即 org.apache.dubbo.common.extension.LoadingStrategy,文件结构如下:

 

// 获取所有loading策略
private static LoadingStrategy[] loadLoadingStrategies() {
    return java.util.stream.StreamSupport.stream(java.util.ServiceLoader.load(LoadingStrategy.class).spliterator(), false)
            .sorted()
            .toArray(LoadingStrategy[]::new);
}
private Map<String, Class<?>> loadExtensionClasses() {
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();

        for (LoadingStrategy strategy : strategies) {
          // 扫描每个加载策略目录中的扩展点实现
            loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
          // 对alibaba的践行兼容
            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        }

        return extensionClasses;
    }

ShardingSphere中分片算法和分片策略的使用

        在ShardingSphere中,ShardingStrategy是一个分片策略,我们直接看一下官方文档的介绍。用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL 中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,Apache ShardingSphere 也支持根据多个字段进行分片。

ShardingAlgorithm分片算法

通过分片算法将数据分片,支持通过 =>=<=><BETWEEN 和 IN 分片。 分片算法需要应用方开发者自行实现,可实现的灵活度非常高。

目前提供 3 种分片算法。 由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。

  • 标准分片算法

对应 StandardShardingAlgorithm,用于处理使用单一键作为分片键的 =INBETWEEN AND><>=<= 进行分片的场景。需要配合 StandardShardingStrategy 使用。

  • 复合分片算法

对应 ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合 ComplexShardingStrategy 使用。

  • Hint分片算法

对应 HintShardingAlgorithm,用于处理使用 Hint 行分片的场景。需要配合 HintShardingStrategy 使用。

ShardingStrategy分片策略

包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供 4 种分片策略。

  • 标准分片策略

对应 StandardShardingStrategy。提供对 SQL 语句中的 =><>=<=IN 和 BETWEEN AND 的分片操作支持。 StandardShardingStrategy 只支持单分片键,提供 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。 PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片。 RangeShardingAlgorithm 是可选的,用于处理 BETWEEN AND><>=<= 分片,如果不配置 RangeShardingAlgorithm,SQL 中的 BETWEEN AND 将按照全库路由处理。

  • 复合分片策略

对应 ComplexShardingStrategy。复合分片策略。提供对 SQL 语句中的 =><>=<=IN 和 BETWEEN AND 的分片操作支持。 ComplexShardingStrategy 支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

  • Hint分片策略

对应 HintShardingStrategy。通过 Hint 指定分片值而非从 SQL 中提取分片值的方式进行分片的策略。

  • 不分片策略

对应 NoneShardingStrategy。不分片的策略。

分片算法的类图如下

分片策略的类图如下

使用分片工厂创建分片策略

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ShardingStrategyFactory {
    
    /**
     * Create sharding strategy.
     * 
     * @param shardingStrategyConfig sharding strategy configuration
     * @param shardingAlgorithm sharding algorithm
     * @return sharding strategy instance
     */
    public static ShardingStrategy newInstance(final ShardingStrategyConfiguration shardingStrategyConfig, final ShardingAlgorithm shardingAlgorithm) {
        if (shardingStrategyConfig instanceof StandardShardingStrategyConfiguration && shardingAlgorithm instanceof StandardShardingAlgorithm) {
            return new StandardShardingStrategy(((StandardShardingStrategyConfiguration) shardingStrategyConfig).getShardingColumn(), (StandardShardingAlgorithm) shardingAlgorithm);
        }
        if (shardingStrategyConfig instanceof ComplexShardingStrategyConfiguration && shardingAlgorithm instanceof ComplexKeysShardingAlgorithm) {
            return new ComplexShardingStrategy(((ComplexShardingStrategyConfiguration) shardingStrategyConfig).getShardingColumns(), (ComplexKeysShardingAlgorithm) shardingAlgorithm);
        }
        if (shardingStrategyConfig instanceof HintShardingStrategyConfiguration && shardingAlgorithm instanceof HintShardingAlgorithm) {
            return new HintShardingStrategy((HintShardingAlgorithm) shardingAlgorithm);
        }
        return new NoneShardingStrategy();
    }
}

JDK中ThreadPoolExecutor策略模式

        JDK中场景的策略模式如线程池的拒绝策略,就是个典型的策略模式,这个也是面试常问话题,比如有哪几个参数,他们的策略是啥,优缺点是啥,为什么需要,这也是本系列文章的思考,从what、why、how去梳理设计模式。

        线程池对拒绝任务的处理策略。在 ThreadPoolExecutor 里面定义了 4 种 handler 策略,分别是

  • CallerRunsPolicy :这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。
  • AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。
  • DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。
  • DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。

使用场景 

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
  • 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。

优缺点对比

优点

  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。
  • 策略模式提供了管理相关的算法族的办法。
  • 策略模式提供了可以替换继承关系的办法。 继承提供了另一种支持多种算法或行为的方法,但这会将行为硬行编制到Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类, 它们之间的唯一差别是它们所使用的算法或行为。将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
  • 使用策略模式可以避免使用多重条件转移语句。提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时,  很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语

缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时, 才需要使用Strategy模式。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
  • Strategy和Context之间的通信开销无论各个ConcreteStrategy实现的算法是简单还是复杂,它们都共享Strategy定义的接口。因此很可能某些ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题,那么将需要在Strategy和Context之间更进行紧密的耦合。

参考资料

集群容错 | Apache Dubbo

负载均衡 | Apache Dubbo

 dubbo源码,ExtensionLoader.strategies属性解析 - 简书

深度剖析Apache dubbo

Dubbo 的集群容错模式:Mergeable Cluster_LieBrother-CSDN博客

Sharding :: ShardingSphere

设计模式之禅

Gof设计模式:可复用面向对象软件的基础 典藏版

Head_First设计模式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值