从横向和纵向两个维度寻求复杂问题的答案

JAVA前线

共 21610字,需浏览 44分钟

 · 2021-08-22


JAVA前线 


欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习



1 多维度思维

在知乎上看到了这个有意思的问题:一头牛重800公斤,一座桥承重700公斤,牛应该怎么过桥。初看题目我们不难得出两个方案:桥梁加固、等待牛体重降至700公斤。

这两个方案显然是正确的,但是我们不能就此止步。因为这类问题考察的是思维方法论,直接给出答案反而非最重要,对于这个问题我们可以从合理性、结构化、可行性三个维度进行分析。


1.1 合理性分析

一头800公斤的牛要通过承重700公斤的桥,这个需求本身合理吗?我们可以从必要性、紧迫性、替代性这三个维度提出三个问题:

第一个问题问必要性:牛为什么要过桥,到底什么事情非要过桥不可

第二个问题问紧迫性:如果非要过桥,那么这个过桥的需求是否紧急

第三个问题问替代性:有没有什么替代方案,是否可以坐船或者绕路走


1.2 结构化分析

如果经过讨论结果是牛非过桥不可,那么我们再思考牛怎么过桥的方案。这里可以使用结构化思维,将大问题拆分为小维度,尽量做到不遗漏和不重复。影响过桥的因素有这几个维度:桥的维度、牛的维度、资源维度、环境维度。

桥的维度:加固桥使承重大于800公斤

牛的维度:等待牛的体重小于700公斤

资源维度:使用一台吊机把牛运过去

环境维度:取消环境重力


1.3 可行性分析

我们从桥的维度、牛的维度、资源维度、环境维度给出了方案,那么选择哪个方案呢?这就需要我们进行可行性评估,因时因地在资源制约下选择当前最合适的方案。

加固桥方案经济成本较高,等待牛的体重小于700公斤时间成本较高,取消环境重力技术难度较高,所以使用一台吊机把牛运过去这个方案目前看来最合适。


1.4 多维度思考

经过我们从合理性、结构化、可行性三个维度梳理之后,虽然答案没有什么新颖之处,但是思维过程很清晰,思考方法也可以应用在其它问题。之所以思维过程清晰,是因为我们没有一上来直接给出答案,而是从多个维度对为题进行了分析,所以增加维度可以使思考过程更清晰。


2 纵向思维与横向思维

思考维度可以从多方面进行充实,其中最常见的是增加横向和纵向两个维度,本文也着重讨论两个维度。总体而言,横向扩展的是思考广度,纵向扩展的是思考深度,而应用在不同场景中细节又各有不同。


2.1 时间管理四象限

时间管理理论四象限法则根据重要和紧急两个维度,建立了一个四象限坐标,可以帮助我们解决主次不分的问题。我们分配工作时间时可以结合四象限法则,重要且紧急的任务优先级最高,而不要急于处理不重要且不紧急的任务。



2.2 金字塔原理

金字塔思维的核心思想并不复杂:一件事情可以总结出一个中心思想,这个中心思想可以由三至七个论点支持,每个论点再可以由三至七个论据支持,基本结构如下图:



金字塔原理内在结构可以从纵向和横向两个维度分析,纵向结构体现了结论先行和以上统下原则,横向结构体现了归类分组和逻辑递进原则。关于金字塔原理详细分析请参看我的文章:结构化思维如何指导技术系统优化

文章分析到这里,我们发现纵向和横向思维有助于厘清思路和增加条理性,下面我们看看纵向和横向思维怎样帮助程序员处理复杂问题。


3 架构设计如何应用纵横思维

我们分析一个创建订单业务场景,当前有ABC三种订单类型,A类型订单价格9折,物流最大重量不能超过8公斤,不支持退款。B类型订单价格8折,物流最大重量不能超过5公斤,支持退款。C类型订单价格7折,物流最大重量不能超过1公斤,支持退款。按照需求字面含义平铺直叙地写代码也并不难:

public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Override
    public void createOrder(OrderBO orderBO) {
        if (null == orderBO) {
            throw new RuntimeException("参数异常");
        }
        if (OrderTypeEnum.isNotValid(orderBO.getType())) {
            throw new RuntimeException("参数异常");
        }
        // A类型订单
        if (OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType())) {
            orderBO.setPrice(orderBO.getPrice() * 0.9);
            if (orderBO.getWeight() > 9) {
                throw new RuntimeException("超过物流最大重量");
            }
            orderBO.setRefundSupport(Boolean.FALSE);
        }
        // B类型订单
        else if (OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType())) {
            orderBO.setPrice(orderBO.getPrice() * 0.8);
            if (orderBO.getWeight() > 8) {
                throw new RuntimeException("超过物流最大重量");
            }
            orderBO.setRefundSupport(Boolean.TRUE);
        }
        // C类型订单
        else if (OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType())) {
            orderBO.setPrice(orderBO.getPrice() * 0.7);
            if (orderBO.getWeight() > 7) {
                throw new RuntimeException("超过物流最大重量");
            }
            orderBO.setRefundSupport(Boolean.TRUE);
        }
        // 保存数据
        OrderDO orderDO = new OrderDO();
        BeanUtils.copyProperties(orderBO, orderDO);
        orderMapper.insert(orderDO);
    }
}

上述代码从功能上完全可以实现业务需求,但是程序员不仅要满足功能,还需要思考代码的可维护性。如果新增一种订单类型,或者新增一个订单属性处理逻辑,那么我们就要在上述逻辑中新增代码,如果处理不慎就会影响原有逻辑。

为了避免牵一发而动全身这种情况,设计模式中的开闭原则要求我们面向新增开放,面向修改关闭,我认为这是设计模式中最重要的一条原则:

当需求变化时通过扩展而不是通过修改已有代码来实现变化,这样就保证代码稳定性。扩展也不是随意扩展,因为事先定义了算法,扩展也是根据算法扩展,用抽象构建框架,用实现扩展细节。标准意义的二十三种设计模式说到底最终都是在遵循开闭原则

如何改变平铺直叙的思考方式?这就要为问题分析加上纵向和横向两个维度,我选择使用分析矩阵方法,其中纵向表示策略,横向表示场景。



3.1 纵向做隔离

纵向维度表示策略,不同策略在逻辑上和业务上应该是隔离的,本实例包括优惠策略、物流策略和退款策略,策略作为抽象,不同订单类型去扩展这个抽象,策略模式非常适合这种场景。

3.1.1 优惠策略

// 优惠策略
public interface DiscountStrategy {
    public void discount(OrderBO orderBO);
}

// A类型订单优惠策略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {

    @Override
    public void discount(OrderBO orderBO) {
        orderBO.setPrice(orderBO.getPrice() * 0.9);
    }
}

// A类型订单优惠策略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {

    @Override
    public void discount(OrderBO orderBO) {
        orderBO.setPrice(orderBO.getPrice() * 0.8);
    }
}

// A类型订单优惠策略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {

    @Override
    public void discount(OrderBO orderBO) {
        orderBO.setPrice(orderBO.getPrice() * 0.7);
    }
}

// 优惠策略工厂
@Component
public class DiscountStrategyFactory implements InitializingBean {
    private Map<String, DiscountStrategy> strategyMap = new HashMap<>();

    @Resource
    private TypeADiscountStrategy typeADiscountStrategy;
    @Resource
    private TypeBDiscountStrategy typeBDiscountStrategy;
    @Resource
    private TypeCDiscountStrategy typeCDiscountStrategy;

    public DiscountStrategy getStrategy(String type) {
        return strategyMap.get(type);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeADiscountStrategy);
        strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBDiscountStrategy);
        strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCDiscountStrategy);
    }
}

// 优惠策略执行器
@Component
public class DiscountStrategyExecutor {
    private DiscountStrategyFactory discountStrategyFactory;

    public void discount(OrderBO orderBO) {
        DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
        if (null == discountStrategy) {
            throw new RuntimeException("无优惠策略");
        }
        discountStrategy.discount(orderBO);
    }
}

3.1.2 物流策略

// 物流策略
public interface ExpressStrategy {
    public void weighing(OrderBO orderBO);
}

// A类型订单物流策略
@Component
public class TypeAExpressStrategy implements ExpressStrategy {

    @Override
    public void weighing(OrderBO orderBO) {
        if (orderBO.getWeight() > 9) {
            throw new RuntimeException("超过物流最大重量");
        }
    }
}

// B类型订单物流策略
@Component
public class TypeBExpressStrategy implements ExpressStrategy {

    @Override
    public void weighing(OrderBO orderBO) {
        if (orderBO.getWeight() > 8) {
            throw new RuntimeException("超过物流最大重量");
        }
    }
}

// C类型订单物流策略
@Component
public class TypeCExpressStrategy implements ExpressStrategy {

    @Override
    public void weighing(OrderBO orderBO) {
        if (orderBO.getWeight() > 7) {
            throw new RuntimeException("超过物流最大重量");
        }
    }
}

// 物流策略工厂
@Component
public class ExpressStrategyFactory implements InitializingBean {
    private Map<String, ExpressStrategy> strategyMap = new HashMap<>();

    @Resource
    private TypeAExpressStrategy typeAExpressStrategy;
    @Resource
    private TypeBExpressStrategy typeBExpressStrategy;
    @Resource
    private TypeCExpressStrategy typeCExpressStrategy;

    @Override
    public void afterPropertiesSet() throws Exception {
        strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeAExpressStrategy);
        strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBExpressStrategy);
        strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCExpressStrategy);
    }

    public ExpressStrategy getStrategy(String type) {
        return strategyMap.get(type);
    }
}

// 物流策略执行器
@Component
public class ExpressStrategyExecutor {
    private ExpressStrategyFactory expressStrategyFactory;

    public void weighing(OrderBO orderBO) {
        ExpressStrategy expressStrategy = expressStrategyFactory.getStrategy(orderBO.getType());
        if (null == expressStrategy) {
            throw new RuntimeException("无物流策略");
        }
        expressStrategy.weighing(orderBO);
    }
}

3.1.3 退款策略

// 退款策略
public interface RefundStrategy {
    public void supportRefund(OrderBO orderBO);
}

// A类型订单退款策略
@Component
public class TypeARefundStrategy implements RefundStrategy {

    @Override
    public void supportRefund(OrderBO orderBO) {
        orderBO.setRefundSupport(Boolean.FALSE);
    }
}

// B类型订单退款策略
@Component
public class TypeBRefundStrategy implements RefundStrategy {

    @Override
    public void supportRefund(OrderBO orderBO) {
        orderBO.setRefundSupport(Boolean.TRUE);
    }
}

// C类型订单退款策略
@Component
public class TypeCRefundStrategy implements RefundStrategy {

    @Override
    public void supportRefund(OrderBO orderBO) {
        orderBO.setRefundSupport(Boolean.TRUE);
    }
}

// 退款策略工厂
@Component
public class RefundStrategyFactory implements InitializingBean {
    private Map<String, RefundStrategy> strategyMap = new HashMap<>();

    @Resource
    private TypeARefundStrategy typeARefundStrategy;
    @Resource
    private TypeBRefundStrategy typeBRefundStrategy;
    @Resource
    private TypeCRefundStrategy typeCRefundStrategy;

    @Override
    public void afterPropertiesSet() throws Exception {
        strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeARefundStrategy);
        strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBRefundStrategy);
        strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCRefundStrategy);
    }

    public RefundStrategy getStrategy(String type) {
        return strategyMap.get(type);
    }
}

// 退款策略执行器
@Component
public class RefundStrategyExecutor {
    private RefundStrategyFactory refundStrategyFactory;

    public void supportRefund(OrderBO orderBO) {
        RefundStrategy refundStrategy = refundStrategyFactory.getStrategy(orderBO.getType());
        if (null == refundStrategy) {
            throw new RuntimeException("无退款策略");
        }
        refundStrategy.supportRefund(orderBO);
    }
}

3.2 横向做编排

横向维度表示场景,一种订单类型在广义上可以认为是一种业务场景,在场景中将独立的策略进行串联,模板方法设计模式适用于这种场景。

模板方法模式定义一个操作中的算法骨架,一般使用抽象类定义算法骨架。抽象类同时定义一些抽象方法,这些抽象方法延迟到子类实现,这样子类不仅遵守了算法骨架约定,也实现了自己的算法。既保证了规约也兼顾灵活性。这就是用抽象构建框架,用实现扩展细节。

// 创建订单服务
public interface CreateOrderService {
    public void createOrder(OrderBO orderBO);
}

// 抽象创建订单流程
public abstract class AbstractCreateOrderFlow {

    @Resource
    private OrderMapper orderMapper;

    public void createOrder(OrderBO orderBO) {
        // 参数校验
        if (null == orderBO) {
            throw new RuntimeException("参数异常");
        }
        if (OrderTypeEnum.isNotValid(orderBO.getType())) {
            throw new RuntimeException("参数异常");
        }
        // 计算优惠
        discount(orderBO);
        // 计算重量
        weighing(orderBO);
        // 退款支持
        supportRefund(orderBO);
        // 保存数据
        OrderDO orderDO = new OrderDO();
        BeanUtils.copyProperties(orderBO, orderDO);
        orderMapper.insert(orderDO);
    }

    public abstract void discount(OrderBO orderBO);

    public abstract void weighing(OrderBO orderBO);

    public abstract void supportRefund(OrderBO orderBO);
}

// 实现创建订单流程
@Service
public class CreateOrderFlow extends AbstractCreateOrderFlow {

    @Resource
    private DiscountStrategyExecutor discountStrategyExecutor;
    @Resource
    private ExpressStrategyExecutor expressStrategyExecutor;
    @Resource
    private RefundStrategyExecutor refundStrategyExecutor;

    @Override
    public void discount(OrderBO orderBO) {
        discountStrategyExecutor.discount(orderBO);
    }

    @Override
    public void weighing(OrderBO orderBO) {
        expressStrategyExecutor.weighing(orderBO);
    }

    @Override
    public void supportRefund(OrderBO orderBO) {
        refundStrategyExecutor.supportRefund(orderBO);
    }
}

3.3 复杂架构设计

上述实例业务和代码并不复杂,其实复杂业务场景也不过是简单场景的叠加、组合和交织,无外乎也是通过纵向做隔离、横向做编排寻求答案。



纵向维度抽象出能力池这个概念,能力池中包含许多能力,不同的能力按照不同业务维度聚合,例如优惠能力池,物流能力池,退款能力池。我们可以看到两种程度的隔离性,能力池之间相互隔离,能力之间也相互隔离。

横向维度将能力从能力池选出来,按照业务需求串联在一起,形成不同业务流程。因为能力可以任意组合,所以体现了很强的灵活性。除此之外,不同能力既可以串行执行,如果不同能力之间没有依赖关系,也可以如同流程Y一样并行执行,提升执行效率。


4 数据分片如何应用纵横思维

现在有一个电商数据库存放订单、商品、支付三张业务表。随着业务量越来越大,这三张业务数据表也越来越大,查询性能显著降低,数据拆分势在必行。那么数据拆分也可以从纵向和横向两个维度进行。


4.1 纵向分表

纵向拆分就是按照业务拆分,我们将电商数据库拆分成三个库,订单库、商品库。支付库,订单表在订单库,商品表在商品库,支付表在支付库。这样每个库只需要存储本业务数据,物理隔离不会互相影响。



4.2 横向分表

按照纵向拆分方案之后我们已经有三个库了,平稳运行了一段时间。但是随着业务增长,每个单库单表的数据量也越来越大,逐渐到达瓶颈。

这时我们就要对数据表进行横向拆分,所谓横向拆分就是根据某种规则将单库单表数据分散到多库多表,从而减小单库单表的压力。

横向拆分策略有很多方案,最重要的一点是选好ShardingKey,也就是按照哪一列进行拆分,怎么分取决于我们访问数据的方式。


4.2.1 范围分片

如果我们选择的ShardingKey是订单创建时间,那么分片策略是拆分四个数据库分别存储每季度数据,每个库包含三张表分别存储每个月数据:



这个方案的优点是对范围查询比较友好,例如我们需要统计第一季度的相关数据,查询条件直接输入时间范围即可。这个方案的问题是容易产生热点数据。例如双11当天下单量特别大,就会导致11月这张表数据量特别大从而造成访问压力。


4.2.2 查表分片

查表法是根据一张路由表决定ShardingKey路由到哪一张表,每次路由时首先到路由表里查到分片信息,再到这个分片去取数据。我们分析一个查表法思想应用实际案例。

Redis官方在3.0版本之后提供了官方集群方案RedisCluster,其中引入了哈希槽(slot)这个概念。一个集群固定有16384个槽,在集群初始化时这些槽会平均分配到Redis集群节点上。每个key请求最终落到哪个槽计算公式是固定的:

SLOT = CRC16(key) mod 16384

一个key请求过来怎么知道去哪台Redis节点获取数据?这就要用到查表法思想:

(1) 客户端连接任意一台Redis节点,假设随机访问到节点A
(2) 节点A根据key计算出slot值
(3) 每个节点都维护着slot和节点映射关系表
(4) 如果节点A查表发现该slot在本节点,直接返回数据给客户端
(5) 如果节点A查表发现该slot不在本节点,返回给客户端一个重定向命令,告诉客户端应该去哪个节点请求这个key的数据
(6) 客户端向正确节点发起连接请求

查表法方案优点是可以灵活制定路由策略,如果我们发现有的分片已经成为热点则修改路由策略。缺点是多一次查询路由表操作增加耗时,而且路由表如果是单点也可能会有单点问题。


4.2.3 哈希分片

相较于范围分片,哈希分片可以较为均匀将数据分散在数据库中。我们现在将订单库拆分为4个库编号为[0,3],每个库包含3张表编号为[0,2],如下图如所示:



我们选择使用orderId作为ShardingKey,那么orderId=100这个订单会保存在哪张表?因为是分库分表,第一步确定路由到哪一个库,取模计算结果表示库表序号:

db_index = 100 % 4 = 0

第二步确定路由到哪一张表:

table_index = 100 % 3 = 1

第三步数据路由到0号库1号表:


在实际开发中路由逻辑并不需要我们手动实现,因为有许多开源框架通过配置就可以实现路由功能,例如ShardingSphere、TDDL框架等等。


5 文章总结

复杂问题不过是简单问题的叠加、组合和交织,横向和纵向两个维度拆分问题不失为一种好方法。纵向做隔离是指将不同业务形态进行隔离,能力池之间进行隔离,能力之间也进行隔离。横向做编排是指从能力池中灵活选择出能力,进行组合和编排,形成形态各异的业务流程,希望本文对大家有所帮助。



JAVA前线 


欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习


浏览 178
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报