一次编程小练习:根据流量计数动态选择不同的策略

持续优化一个程序的过程,也是编程技艺提升和编程的乐趣所在。


引子

在实际应用中, 往往会承载两种不同的流量。一种是日常流量,比较平稳且持续;一种是极端流量,比较尖锐且短暂。应对这种情况,往往需要不同的策略。比如日常流量下,走普通的逻辑,而在极端流量下,则需要多级缓存等。

如何根据不同的流量来选择不同的策略呢?


基本实现

先定义一个流量策略接口 FlowStrategy:


public interface FlowStrategy {

    /**
     * 计算整数的一半
     */
    int half(int a);
}

然后实现两种策略:


public class PlainStrategy implements FlowStrategy {
    @Override
    public int half(int a) {
        System.out.println("PlainStrategy");
        return a / 2 ;
    }
}

public class ExtremeStrategy implements FlowStrategy {

    @Override
    public int half(int a) {
        System.out.println("ExtremeStrategy");
        return a >> 1;
    }
}

再实现一个策略选择器:


public class FlowStrategySelector {

    private AtomicBoolean isExtremeFlow = new AtomicBoolean(false);

    private PlainStrategy plainStrategy = new PlainStrategy();
    private ExtremeStrategy extremeStrategy = new ExtremeStrategy();

    public FlowStrategy select() {
        return isExtremeFlow.get() ? extremeStrategy : plainStrategy;
    }

    public void notifyExtreme() {
        isExtremeFlow.compareAndSet(false, true);
    }

    public void normal() {
        isExtremeFlow.compareAndSet(true, false);
    }
}

接着写一个简易版的流量计数器:


public class FlowCount implements Runnable {

    private FlowStrategySelector flowStrategySelector;
    private int rate;
    private int count;
    private long lastStartTimestamp = System.currentTimeMillis();

    public FlowCount(FlowStrategySelector flowStrategySelector) {
        this.flowStrategySelector = flowStrategySelector;
    }

    @Override
    public void run() {
        while (true) {
            if (rate > 40) {
                flowStrategySelector.notifyExtreme();
            } else {
                flowStrategySelector.normal();
            }
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                //
            }
        }
    }

    public void rate() {
        count++;
        if (System.currentTimeMillis() - lastStartTimestamp >= 1000) {
            rate = count;
            lastStartTimestamp = System.currentTimeMillis();
            count = 0;
        }
    }
}

最后写一个用例:


public class FlowStrategyTester {

    static Random random = new Random(System.currentTimeMillis());

    public static void main(String[] args) throws InterruptedException {

        FlowStrategySelector flowStrategySelector = new FlowStrategySelector();
        FlowCount flowCount = new FlowCount(flowStrategySelector);
        new Thread(flowCount).start();

        for (int i=0; i < 2000000; i++) {
            int sleepTime = random.nextInt(50);
            TimeUnit.MILLISECONDS.sleep(sleepTime);
            flowStrategySelector.select().half(Integer.MAX_VALUE);
            flowCount.rate(); // 可以通过消息队列来推送消息和计数
        }

    }

}



加点难度

假设不止有一个方法需要流量策略接口,比如 A 业务也需要, B 业务也需要,怎么办呢 ? 难道是在 FlowStrategy 里新增另一个完全不同业务的方法 ? 这样会导致 FlowStrategy 的实现子类 PlainStrategy, ExtremeStrategy 不断膨胀。

显然,合适的方法是,给每个业务创建子接口,如下所示。 把 FlowStrategy 的方法拿出去,成为空接口:

A业务:


public interface ABizFlowStrategy extends FlowStrategy {

    /**
     * 计算整数的一半
     */
    int half(int a);
}

public class APlainStrategy implements ABizFlowStrategy {
    @Override
    public int half(int a) {
        System.out.println("APlainStrategy");
        return a / 2 ;
    }
}

public class AExtremeStrategy implements ABizFlowStrategy {

    @Override
    public int half(int a) {
        System.out.println("AExtremeStrategy");
        return a >> 1;
    }
}


B 业务:


public interface BBizFlowStrategy extends FlowStrategy {

    /**
     * 计算整数的两倍
     */
    int multi(int a);
}

public class BPlainStrategy implements BBizFlowStrategy {
    @Override
    public int multi(int a) {
        System.out.println("BPlainStrategy");
        return a * 2 ;
    }
}

public class BExtremeStrategy implements BBizFlowStrategy {

    @Override
    public int multi(int a) {
        System.out.println("BExtremeStrategy");
        return a << 1;
    }
}


那么流量策略选择器就修改为:


public class FlowStrategySelector {

    private AtomicBoolean isExtremeFlow = new AtomicBoolean(false);

    private APlainStrategy aPlainStrategy = new APlainStrategy();
    private AExtremeStrategy aExtremeStrategy = new AExtremeStrategy();

    private BPlainStrategy bPlainStrategy = new BPlainStrategy();
    private BExtremeStrategy bExtremeStrategy = new BExtremeStrategy();

    public ABizFlowStrategy selectA() {
        return isExtremeFlow.get() ? aExtremeStrategy : aPlainStrategy;
    }

    public BBizFlowStrategy selectB() {
        return isExtremeFlow.get() ? bExtremeStrategy : bPlainStrategy;
    }

    public void notifyExtreme() {
        isExtremeFlow.compareAndSet(false, true);
    }

    public void normal() {
        isExtremeFlow.compareAndSet(true, false);
    }
}


显然,FlowStrategySelector 又有膨胀的趋势。 需要把 FlowStrategySelector 也拆分出来。

建立 FlowStrategySelector 接口。这里之所以要有 notifyExtreme 和 normal, 是因为 selector 必须与 flowCount 交互。


public interface FlowStrategySelector<FS extends FlowStrategy> {

    FS select();

    void notifyExtreme();

    void normal();

}

然后可实现不同业务的 FlowStrategySelector:


public class AFlowStrategySelector implements FlowStrategySelector<ABizFlowStrategy> {

    private AtomicBoolean isExtremeFlow = new AtomicBoolean(false);

    private APlainStrategy aPlainStrategy = new APlainStrategy();
    private AExtremeStrategy aExtremeStrategy = new AExtremeStrategy();

    @Override
    public ABizFlowStrategy select() {
        return isExtremeFlow.get() ? aExtremeStrategy : aPlainStrategy;
    }

    @Override
    public void notifyExtreme() {
        isExtremeFlow.compareAndSet(false, true);
    }

    @Override
    public void normal() {
        isExtremeFlow.compareAndSet(true, false);
    }
}


FlowStrategySelector 的工厂:


public class FlowStrategySelectorFactory {

    private AFlowStrategySelector aFlowStrategySelector = new AFlowStrategySelector();
    private BFlowStrategySelector bFlowStrategySelector = new BFlowStrategySelector();

    Map<Class, FlowStrategySelector> strategyMap = new HashMap() {
        {
            put(ABizFlowStrategy.class, aFlowStrategySelector);
            put(BBizFlowStrategy.class, bFlowStrategySelector);
        }
    };

    public FlowStrategySelector getSelector(Class cls) {
        return strategyMap.get(cls);
    }

    public <FS extends FlowStrategy> FS select(Class<FS> fsClass) {
        return (FS)getSelector(fsClass).select();
    }

}

流量计数器改动:


public class FlowCount implements Runnable {

    private FlowStrategySelector flowStrategySelector;
    private int rate;
    private int count;
    private long lastStartTimestamp = System.currentTimeMillis();

    public FlowCount(FlowStrategySelector flowStrategySelector) {
        this.flowStrategySelector = flowStrategySelector;
    }

    @Override
    public void run() {
        while (true) {
            if (rate > 40) {
                flowStrategySelector.notifyExtreme();
            } else {
                flowStrategySelector.normal();
            }
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                //
            }
        }
    }

    public void rate() {
        count++;
        if (System.currentTimeMillis() - lastStartTimestamp >= 1000) {
            rate = count;
            lastStartTimestamp = System.currentTimeMillis();
            count = 0;
        }
    }
}


用例:


public class FlowStrategyTester {

    static Random random = new Random(System.currentTimeMillis());

    public static void main(String[] args) throws InterruptedException {

        FlowStrategySelectorFactory flowStrategySelectorFactory = new FlowStrategySelectorFactory();
        FlowStrategySelector aflowStrategySelector = flowStrategySelectorFactory.getSelector(ABizFlowStrategy.class);
        FlowCount aflowCount = new FlowCount(aflowStrategySelector);
        new Thread(aflowCount).start();

        for (int i=0; i < 2000; i++) {
            int sleepTime = random.nextInt(50);
            TimeUnit.MILLISECONDS.sleep(sleepTime);
            flowStrategySelectorFactory.select(ABizFlowStrategy.class).half(Integer.MAX_VALUE);
            aflowCount.rate(); // 可以通过消息队列来推送消息和计数
        }

        FlowStrategySelector bflowStrategySelector = flowStrategySelectorFactory.getSelector(BBizFlowStrategy.class);
        FlowCount bflowCount = new FlowCount(bflowStrategySelector);
        new Thread(bflowCount).start();

        for (int i=0; i < 2000; i++) {
            int sleepTime = random.nextInt(50);
            TimeUnit.MILLISECONDS.sleep(sleepTime);
            flowStrategySelectorFactory.select(BBizFlowStrategy.class).multi(Integer.MAX_VALUE);
            bflowCount.rate(); // 可以通过消息队列来推送消息和计数
        }

    }

}

改进点

这里其实还有不少改进点。

  • flowStrategySelectorFactory.getSelector(BBizFlowStrategy.class) 这个调用就有点奇怪,按道理应该是 flowStrategySelectorFactory.getSelector(AFlowStrategySelector.class) 更自然一点。 但如果拿到 selector 再调用 select 方法,就始终得到的是 FlowStrategy 接口,无法调用后面的 half 或 multi 方法。这是因为引用的是基类,而不是子类。必须用 <FS extends FlowStrategy> FS select(Class<FS> fsClass) 通过参数的方式指明获取的是哪个子类实例。

  • FlowStrategySelectorFactory 里 bizFlowStrategy 与 FlowStrategySelector 的映射关系,可以做成自动化的,而不必手动配置。在 Spring 应用里,只要将这些标识为 Component 组件,然后在应用启动的时候根据 getBeansOfType 获取到实例,然后构建映射关系即可。

  • FlowStrategySelector 的实现基本相似,且会膨胀,能否做得更简单一些呢?

  • 各个类的职责是否划分明晰?

  • 如果不同的业务要采用不同的流量计数怎么办呢?


FlowStrategySelector 通用化

这里,我们发现 FlowStrategySelector 的实现基本相似。可以做一个通用的实现。FlowStrategySelector 实际上跟业务本身没关系,只跟流量策略有关系。因此,可以定义普通流量策略接口 PlainFlowStrategy 和 极限流量策略接口 ExtremeFlowStrategy, 然后让实现 implements 这些接口:


/**
 * 普通流量策略
 */
public interface PlainFlowStrategy extends FlowStrategy {
}

/**
 * @Description 极限流量策略
 */
public interface ExtremeFlowStrategy extends FlowStrategy {
}

public class APlainStrategy implements ABizFlowStrategy, PlainFlowStrategy { }

public class AExtremeStrategy implements ABizFlowStrategy, ExtremeFlowStrategy { }

public class BPlainStrategy implements BBizFlowStrategy, PlainFlowStrategy { }

public class BExtremeStrategy implements BBizFlowStrategy, ExtremeFlowStrategy { }

然后可以实现一个通用的 FlowStrategySelector (CommonFlowStrategySelector 的构造器可以通过 Spring bean 管理实现更加自动化,而不是手动 new):


public class CommonFlowStrategySelector implements FlowStrategySelector {

    private AtomicBoolean isExtremeFlow = new AtomicBoolean(false);

    private PlainFlowStrategy plainFlowStrategy;
    private ExtremeFlowStrategy extremeFlowStrategy;

    public CommonFlowStrategySelector(Class cls) {
        if (cls.getName().equals(ABizFlowStrategy.class.getName())) {
            if (plainFlowStrategy == null) {
                plainFlowStrategy = new APlainStrategy();
            }
            if (extremeFlowStrategy == null) {
                extremeFlowStrategy = new AExtremeStrategy();
            }
        }
        else if (cls.getName().equals(BBizFlowStrategy.class.getName())) {
            if (plainFlowStrategy == null) {
                plainFlowStrategy = new BPlainStrategy();
            }
            if (extremeFlowStrategy == null) {
                extremeFlowStrategy = new BExtremeStrategy();
            }
        }
    }

    @Override
    public FlowStrategy select() {
        return isExtremeFlow.get() ? extremeFlowStrategy : plainFlowStrategy;
    }

    @Override
    public void notifyExtreme() {
        isExtremeFlow.compareAndSet(false, true);
    }

    @Override
    public void normal() {
        isExtremeFlow.compareAndSet(true, false);
    }
}


FlowStrategySelectorFactory 就更简单了:


public class FlowStrategySelectorFactory {

    public FlowStrategySelector getSelector(Class cls) {
        return new CommonFlowStrategySelector(cls);
    }

    public <FS extends FlowStrategy> FS select(FlowStrategySelector selector, Class<FS> fsClass) {
        return (FS)selector.select();
    }

}


流量计数与策略选择

如果我要根据不同的业务选择不同流量技术策略,怎么办呢 ? 这里就不能直接使用 FlowCount 实现类了。而是要做点改造。

定义一个流量计数策略接口:


public interface FlowCountStrategy extends Runnable {

    /**
     * 流量速率计算
     */
    void rate();

    /**
     * 是否极端流量
     */
    boolean isExtreme();
}

流量计数策略实现(注意这里 FlowCount 在并发情形下是有点问题的,聪明的你能看出来么?):


public class FlowCount implements FlowCountStrategy, Runnable {

    private FlowStrategySelector flowStrategySelector;
    private AtomicInteger rate = new AtomicInteger(0);
    private int count;
    private long lastStartTimestamp = System.currentTimeMillis();

    public FlowCount(FlowStrategySelector flowStrategySelector) {
        this.flowStrategySelector = flowStrategySelector;
    }

    @Override
    public void run() {
        while (true) {
            if (isExtreme()) {
                flowStrategySelector.notifyExtreme();
            } else {
                flowStrategySelector.normal();
            }
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                //
            }
        }
    }

    @Override
    public void rate() {
        count++;
        if (System.currentTimeMillis() - lastStartTimestamp >= 1000) {
            rate.getAndSet(count);
            lastStartTimestamp = System.currentTimeMillis();
            count = 0;
        }
    }

    @Override
    public boolean isExtreme() {
        return rate.get() > 40;
    }
}

注意到,这里把 rate > 40 抽离成一个方法 isExtreme(), 是为了更好滴把极限流量和普通流量的判断抽离出来。

再写一个简易的流量计数策略选择器:


public class FlowCountStrategySelector {

    private FlowCount flowCount;

    public FlowCountStrategy select(FlowStrategySelector flowStrategySelector) {
        if (flowCount == null) {
            flowCount = new FlowCount(flowStrategySelector);
        }
        return flowCount;
    }
}

那么,用例就可以写成:


FlowStrategySelectorFactory flowStrategySelectorFactory = new FlowStrategySelectorFactory();
FlowStrategySelector aflowStrategySelector = flowStrategySelectorFactory.getSelector(ABizFlowStrategy.class);
FlowCountStrategySelector flowCountStrategySelector = new FlowCountStrategySelector();
FlowCountStrategy aflowCount = flowCountStrategySelector.select(aflowStrategySelector);
new Thread(aflowCount).start();

for (int i=0; i < 2000; i++) {
    int sleepTime = random.nextInt(50);
    TimeUnit.MILLISECONDS.sleep(sleepTime);
    flowStrategySelectorFactory.select(aflowStrategySelector, ABizFlowStrategy.class).half(Integer.MAX_VALUE);
    aflowCount.rate(); // 可以通过消息队列来推送消息和计数
}        

客户端就跟具体的 FlowCount 实现无关了。


参数优化

注意到,这里所有的参数都是传 XFlowStrategy.class ,这个参数感觉比较别扭。 实际上需要突出一个业务名的概念,通过业务名把这些都串起来。

定义业务枚举及业务名接口和实现:


public enum BizName {
    A,
    B;
}

public interface FlowStrategy {

    String bizName();
}

public interface ABizFlowStrategy extends FlowStrategy {

    /**
     * 计算整数的一半
     */
    int half(int a);

    @Override
    default String bizName() {
        return BizName.A.name();
    }
}

public interface BBizFlowStrategy extends FlowStrategy {

    /**
     * 计算整数的两倍
     */
    int multi(int a);

    @Override
    default String bizName() {
        return BizName.B.name();
    }
}

flowStrategySelectorFactory.getSelector(ABizFlowStrategy.class) 参数可以用 BizName 代替:


public class FlowStrategySelectorFactory {

    public FlowStrategySelector getSelector(BizName biz) {
        return new CommonFlowStrategySelector(biz);
    }

}

CommonFlowStrategySelector 的构造器就可以用 BizName 参数来代替。


public interface FlowStrategySelector {

    <FS extends FlowStrategy> FS select(Class<FS> cls);

    void notifyExtreme();

    void normal();

}

public class CommonFlowStrategySelector implements FlowStrategySelector {

    private AtomicBoolean isExtremeFlow = new AtomicBoolean(false);

    private BizName biz;
    private PlainFlowStrategy plainFlowStrategy;
    private ExtremeFlowStrategy extremeFlowStrategy;

    public CommonFlowStrategySelector(BizName biz) {
        this.biz = biz;
        if (biz == BizName.A) {
            if (plainFlowStrategy == null) {
                plainFlowStrategy = new APlainStrategy();
            }
            if (extremeFlowStrategy == null) {
                extremeFlowStrategy = new AExtremeStrategy();
            }
        }
        else if (biz == BizName.B) {
            if (plainFlowStrategy == null) {
                plainFlowStrategy = new BPlainStrategy();
            }
            if (extremeFlowStrategy == null) {
                extremeFlowStrategy = new BExtremeStrategy();
            }
        }
    }

    public FlowStrategy selectInner() {
        return isExtremeFlow.get() ? extremeFlowStrategy : plainFlowStrategy;
    }

    @Override
    public <FS extends FlowStrategy> FS select(Class<FS> cls) {
        return (FS)selectInner();
    }

    @Override
    public void notifyExtreme() {
        isExtremeFlow.compareAndSet(false, true);
    }

    @Override
    public void normal() {
        isExtremeFlow.compareAndSet(true, false);
    }
}

用例可以写成:


FlowStrategySelectorFactory flowStrategySelectorFactory = new FlowStrategySelectorFactory();
FlowStrategySelector aflowStrategySelector = flowStrategySelectorFactory.getSelector(BizName.A);
FlowCountStrategySelector flowCountStrategySelector = new FlowCountStrategySelector();
FlowCountStrategy aflowCount = flowCountStrategySelector.select(aflowStrategySelector);
new Thread(aflowCount).start();

for (int i=0; i < 1000; i++) {
    int sleepTime = random.nextInt(50);
    TimeUnit.MILLISECONDS.sleep(sleepTime);
    aflowStrategySelector.select(ABizFlowStrategy.class).half(Integer.MAX_VALUE);
    aflowCount.rate(); // 可以通过消息队列来推送消息和计数
}

这里还有一点不足的是: aflowStrategySelector.select(ABizFlowStrategy.class).half(Integer.MAX_VALUE); 还是得用 ABizFlowStrategy.class 作为参数,不然就没法调用 half 接口,因为 select 返回的是 PlainFlowStrategy 或 ExtremeFlowStrategy 并不含业务信息,无法调用业务接口的方法。 目前暂时没有找到如何能够自动转成 ABizFlowStrategy 或 BBizFlowStrategy 的方法。也想过把 half 或 multi 放到 CommonFlowStrategySelector 的方法执行,但这样也会涉及强制类型转换。


职责与命名

再推敲下, ABizFlowStrategy, BBizFlowStrategy 的命名是不够准确的。这里只是两种业务的表示,与流量策略并没有关系,是一种平行的关系,而不是包含的关系。因此,这两个类应该命名为 BizStrategy.


/**
* @Description 业务方法定义
*/
public interface BizStrategy {

   String bizName();
}

public interface ABizStrategy extends BizStrategy {

   /**
    * 计算整数的一半
    */
   int half(int a);

   @Override
   default String bizName() {
       return BizName.A.name();
   }
}

public interface BBizStrategy extends BizStrategy {

   /**
    * 计算整数的两倍
    */
   int multi(int a);

   @Override
   default String bizName() {
       return BizName.B.name();
   }
}

public class APlainStrategy implements ABizStrategy, PlainFlowStrategy { // code as before }
public class AExtremeStrategy implements ABizStrategy, ExtremeFlowStrategy { // code as before }
public class BPlainStrategy implements BBizStrategy, PlainFlowStrategy { // code as before }
public class BExtremeStrategy implements BBizStrategy, ExtremeFlowStrategy { // code as before }

CommonFlowStrategySelector 做了点微调:


public interface FlowStrategySelector {

    // code changed
    <FS extends BizStrategy> FS select(Class<FS> cls);

}

public class CommonFlowStrategySelector implements FlowStrategySelector {

    // code changed
    @Override
    public <FS extends BizStrategy> FS select(Class<FS> cls) {
        return (FS)selectInner();
    }

}


好的程序

好的程序是怎样的?

  • 职责与命名清晰,能够准确表达其意图;
  • 方法和交互定义清晰;
  • 可复用和可扩展性良好,考虑到应有的变化;
  • 方法签名的参数比较自然,不别扭;
  • 保证并发情形的准确性。

要反复斟酌推敲,写出好的程序,才能对编程技艺和设计思维有真正的提升作用。天天重复堆砌相似水平的业务代码是很难有所提升的。


小结

根据流量计数来选择不同的策略处理,是建立了一个反馈回路:当接收到请求时,会做一个请求计数处理,这个请求计数处理又会反馈到请求处理的前置环节,选择请求处理的策略。系统思考值得再好好学学。

此外,流量计数和预测是一个值得去优化和探讨的话题。据说,现在有用 AI 算法来做流量预测的。

持续优化一个程序的过程,也是编程技艺提升和编程的乐趣所在。



posted @ 2022-06-05 20:39  琴水玉  阅读(111)  评论(0编辑  收藏  举报