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

1 多维度思维

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

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

1.1 合理性分析

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

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

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

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

1.2 结构化分析

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

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

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

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

环境维度:取消环境重力

1.3 可行性分析

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

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

1.4 多维度思考

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

2 纵向思维与横向思维

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

2.1 时间管理四象限

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

2.2 金字塔原理

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

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

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

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

我们分析一个创建订单业务场景,当前有A、B、C三种订单类型,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前线
链接:https://juejin.cn/post/7208559891605602361

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值