简单工厂模式+适配器模式——请求分发

  在某个电商体系中,有若干个营销活动,每个营销活动的后台操作基本相同。为了便于管理,节约维护成本和未来的开发成本,于是开发了一套活动后台管理系统,统一管理所有活动的后台设置。

  三个营销活动a、b,相同的基本操作add(), remove(),importData() 等,针对每个操作,处理类的核心逻辑为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private AService aService;

private BService bService;

// 中间省略

//根据活动类型,选择具体的活动处理service及方法
switch(activityType){
case 1 :
return aService.list();
case 2 :
return bService.list();
default :
break;
}

  很显然,上述代码臃肿,若要接入新活动,必须修改代码,违背了开闭原则。下面开始重构。

  我发现每个营销活动都有唯一的活动类型,即一个 activityType 对应一个 activityService 实例,符合简单工厂模式的定义。接下来,定义模型:

  • Factory(工厂角色):根据外界指令,创建所需要的产品对象。即根据 activityType 得到 activityService
  • Product(抽象产品角色):工厂类所生产对象的父类,封装了所有产品的共有方法。即 add(), remove() 等。
  • ConcreateProduct(具体产品角色):简单工厂模式的创建目标,需要实现抽象产品角色的所有抽象方法。即 AService、BService、CService
  • 抽象产品类 AbstractActivity
1
2
3
4
5
6
7
8
public abstract class AbstractActivity {

public abstract void add();

public abstract void remove();

public abstract void importData();
}
  • 具体产品类 AService、BService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class AService extends AbstractActivity {

    @Override
    public void add() {

    }

    @Override
    public void remove() {

    }

    @Override
    public void importData() {

    }
    }

    public class BService extends AbstractActivity {

    @Override
    public void add() {

    }

    @Override
    public void remove() {

    }

    @Override
    public void importData() {

    }
    }
  • 工厂角色类 Factory

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Factory {

    public static Product getProduct(Integer activityType) {

    if (null == activity) {
    // 省略
    }

    AbstractActivity activity = null;

    if (activityType.equals(1)) {
    activity = new AService();
    } else if (activityType.equals(2)) {
    activity = new BService();
    }

    return activity;
    }
    }
  • 模拟客户端 Client

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Client {
    public static void main(String args[]) {

    Integer activityType = 1;
    //通过工厂类创建产品对象
    AbstractActivity activity = Factory.getProduct(activityType);
    activity.list();
    }
    }

  我成功地利用多态实现了请求分发功能。此时又多了一种营销活动 c,我们要在工厂类中增加 if-else if 分支,此举违背了 开闭原则。

1
2
3
4
5
6
7
if (activityType.equals(1)) {
activity = new AService();
} else if (activityType.equals(2)) {
activity = new BService();
} else if (activityType.equals(3)) {
activity = new CService();
}

我们可以通过 context.getBeansOfType 获得所有产品角色,来构建工厂。
  • 工厂类代码片段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //得到所有继承了AbstractActivity的实例 -> 得到所有具体产品角色
    Map<String, AbstractActivity> activityMap = context.getBeansOfType(AbstractActivity.class);
    //创建抽象类工厂 I<产品类型, 产品角色>
    Map<Integer, AbstractActivity> factoryMap = new HashMap<>(activityMap.size());

    for (Map.Entry<String, AbstractActivity> entry : activityMap.entrySet()) {
    AbstractActivity activity = entry.getValue();
    //产品角色中自带 类型
    factoryMap.put(activity.getType(), activity);
    }
  • 抽象产品类 AbstractActivity

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractActivity {

//产品类型
public abstract Integer getType();

public abstract void add();

public abstract void remove();

public abstract void importData();
}
  • 具体产品类 AService
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class AService extends AbstractActivity {

    @Override
    public Integer getType() {
    //每个产品对应不同的类型
    return 1;
    }

    @Override
    public void add() {

    }

    @Override
    public void remove() {

    }

    @Override
    public void importData() {

    }
    }

  开发新的产品,只需要继承抽象产品角色即可,不需要再修改其他代码片段。

  此时有了一个新问题,新产品 d 没有 importData() 方法。产品 d 不需要重写 抽象方法。抽象方法和调用者不能直接接入,需要中转适配——适配器模式。

  我引入接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了里面所有方法,而我们不和原始的接口交互,只和抽象类关联,再写一个类继承抽象类,重写我们需要的方法即可。

  • 接口类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface IActivityService {

    Integer getType();

    void add();

    void remove();

    void importData();
    }
  • 抽象产品

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public abstract class AbstractActivity implements IActivityService {

    public Integer getType() {
    return null;
    }

    public void add() {

    }

    public void remove() {

    }

    public void importData() {

    }

    }
  • 具体产品

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class AService extends AbstractActivity {

    @Override
    public Integer getType() {
    //每个产品对应不同的类型
    return 1;
    }

    @Override
    public void add() {

    }

    @Override
    public void remove() {

    }

    @Override
    public void importData() {

    }
    }

    public class DService extends AbstractActivity {

    @Override
    public Integer getType() {
    //每个产品对应不同的类型
    return 1;
    }

    @Override
    public void add() {

    }

    @Override
    public void remove() {

    }
    }

  即使此时需要为多个产品增加公共方法,只在interface 和 abstract 中增加新方法,且在需要使用新方法的产品类中重写即可,对其它产品类是透明的。

java 集合——LinkedList 和 ArrayList到底怎么用

  LinkedList 和 ArrayList 是最常用的集合,两者是 List 的实现类。我们知道LinkedList便于插入、删除,却不便于获取指定位置的元素,而ArrayList恰恰相反。基于这些特性,我发现所接触的代码中需要优化的还有很多。

nginx 须知

  Nginx(发音同engine x)是异步框架的 Web服务器,也可以用作反向代理,负载平衡器 和 HTTP缓存。一大部分Web服务器使用Nginx,通常作为负载均衡器。

数据库适应业务的架构演进

垂直分表

  垂直分表也称为“大表拆小表”,基于关系型数据库字段进行拆分,是最常见的数据库表设计。由于字段比较多,通常将低频使用或长度较大的字段拆分到“扩展表”中。

  我在中国网安参与过国家信息安全漏洞库的优化工作。其中一个优化点便是提升数据库的访问速度。有张“漏洞信息表”包含上百个字段,根据主业务场景不同,划分成了三张表——主要信息表、标签表、正文表,后两者便是上文描述的“扩展表”。

垂直分库

  垂直分库是指根据业务将相关表从一个库划分到另一个库。这种架构在“微服务”盛行的今天非常普及。

  如今在我所处的电商体系中,商品、订单、用户都是单独封库,这样有利于我们对不同业务类型的数据进行“分级”管理、维护、监控、扩展,与上层“服务拆分”、系统解耦思想不谋而合

  在IO瓶颈、硬件资源不足等环境面前,垂直分库架构通过横向扩展能够解决上述痛点。

水平分表

  水平分表也称为“横向分表”,即将一张表中根据数据行拆分到不同的表中(同一个库),以此来降低单表数据量,优化查询性能。

  纵观整个数据库,仍在一台机器上,不容易解决IO性能瓶颈。我见过的水平分表都做成了分库分表。

主从备份

  主从备份是容灾的手段,通过冗余备份,起到数据安全、服务高可用作用。

读写分离

  读写分离是建立在主从备份的基础上,在主从的架构体系下,主库负责写数据,而从库只提供查询操作。

  当单机(主库)无法支撑大量的数据访问,需要从库的分流,也可以有多个从库同时分流。由于数据同步可能会有延时,主从在极短时间内会出现数据不一致的情况。

分库分表

  分库分表是升级版的水平分表,将这些拆分出的表保存到不同的数据库中。

  在高并发和海量数据的场景下,分库分表能够有效缓解单机和单库的性能瓶颈和压力,突破 IO、连接数、硬件资源的瓶颈。

问题

  • 分库分表策略
  • 跨库事务

      后面,我将总结一下我所认知的解决方案。

参考

分库分表的几种常见形式以及可能遇到的难
数据库主从复制,读写分离,负载均衡,分库分表分别表达的什么

最终一致性——理论及实践

查询模式

  A服务调用B服务超时,A必须确认B服务是否完成,可以做查询校验。

  例如,某个营销活动需要选择5k个商品参加,我们选择导入的方式,并生成导入结果反馈给客户。由应用层读取、筛选数据,批量(多线程)请求服务层,每次保存200个商品,服务层负责将200个商品落库,建立营销-商品关联关系,甚至需要做其他耗时处理(比如打标签)。如此一来,服务层接口很可能会产生超时(默认5000ms)异常,应用层无法得知数据是否保存成功。

  问题来了:如何最大程度保证「导入结果」与「数据库」数据一致性?

  我们的解决方案是查询服务层,校验数据是否落库。查询的时机可以根据业务场景决定,由于整个「导入」是异步进行,本身对时效并不严格,我们决定在批量(多线程)保存结束后,再用超时的数据作为入参批量查询服务层作为校验手段。(超时情况如何获得入参,请参考Future-异常的处理

补偿模式

Future 异常的处理

Future 简介

Java并发专题 带返回结果的批量任务执行 CompletionService ExecutorService.invokeAll

Java程序执行超时——Future接口介绍

异常处理

任务内部异常

  当任务中具体处理方法发生异常,而恰好我们需要获取该方法或者该任务的参数,或对其特殊处理,则可以使用捕获异常的方式构造出参。例如,需要校验一批快递单号是否已经发货,当具体的校验服务抛异常,则可以将这这批待校验的单号作为“未发货”返回,或者标记成其他状态(例:未知)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyTask implements Callable<Response> {

private Request request;
private Service service;

@Override
public Response call() {
Response response = null;
try {
//调用方法
response = service.invoke(request);
} catch (Exception e) {
//异常,返回入参
response.setXXX(requese.getXXX());
}
}
}

任务中断异常

  该异常是检查型异常,目的是强制开发者处理这类异常:

1
2
3
4
5
6
7
try {
completionService.take().get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}

  由于该场景对所有类型的异常处理均一致,故只捕获 Exception

1
2
3
4
5
try {
future = completionService.take();
taskResult = future.get();
} catch (Exception e) {
}

  任务发生异常,无法返回出参,但是可以获取任务的入参——提交任务前,将任务及其入参存入 Map< Future, TaskRequest>,待发生异常后,方便获取入参,或对其特殊处理,同上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TaskRequest taskRequest = new TaskRequest();
// 封装入参 taskRequest
...
Future<Response> future = completionService.submit(taskRequest);
Map<Future<Response>, TaskRequest> futureTaskMap = new HashMap<taskNum>;
// 构建 futureTaskMap

try {
future = completionService.take();
taskResult = future.get();
} catch (Exception e) {
//异常,返回入参
taskRequest = futureTaskMap.get(future);
taskRequest.setXXX(taskRequest.getXXX());
}

  为了优化内存,还可以在任务执行结束或异常之后,将 Map< Future< Response>, TaskRequest> 中的当前元素移除。

1
2
3
4
5
6
7
try {
...
} catch (Exception e) {
...
} finnily {
futureTaskMap.remove(future);
}