一次编程小练习:根据流量计数动态选择不同的策略
持续优化一个程序的过程,也是编程技艺提升和编程的乐趣所在。
引子
在实际应用中, 往往会承载两种不同的流量。一种是日常流量,比较平稳且持续;一种是极端流量,比较尖锐且短暂。应对这种情况,往往需要不同的策略。比如日常流量下,走普通的逻辑,而在极端流量下,则需要多级缓存等。
如何根据不同的流量来选择不同的策略呢?
基本实现
先定义一个流量策略接口 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 算法来做流量预测的。
持续优化一个程序的过程,也是编程技艺提升和编程的乐趣所在。