本文正在参加「金石计划」

前语

最近两个月在忙项意图一同接到了一个重构中心项意图活,又是了解的要求,又快又好,主打一个我全都要。时刻严重,按理说叠叠乐是最优解,可是我很倔强,我不想玩叠叠乐。虽然堆X山的我干了也不少,留下了不少有气味的项目,可是我想要做一些不相同的,让人眼前一亮的规划。就像标题说的,我要点使用了模板形式和单例形式来做核算服务的重构,也用到了许多的优化手法来进步核算的效率,保证从守时核算晋级到实时核算。

前情提要,X山气味很重,作者吐槽凶猛,这不是开往幼儿园的车,再重复一次,这不是开往幼儿园的车。各位没来得及下车的乘客无论坐没坐好,我只说一次,车门已锁死,发车啦!

恶龙待机中

浅说一下项目背景

订单交给是公司的中心项目,其意图是经过市场代码称号维度的资源,进行订单交期、料号、库位的主动运算,然后削减供应统筹人工保护订单信息的工时,缩短订单周期,进步交给质量。项目一期历时大半年的开发和完善,前后投入五名后端和三名前端,最总算半年前正式安稳运转。项目二期于今年二月初进行开发,除开二期的需求迭代之外,需求将本来一小时一次的守时核算改成实时核算,需求从底层替换架构来适应实时核算。

项目一期分成三个服务,后端、核算、数据。数据服务一期是主要做守时批量读取公司数仓数据做二次清洗,保存到数据库,主要是做本地表,进步查询速度。

后端服务担任与前端交互,页面上增删改查导入导出和一些简略的核算逻辑。困难点在于查询接口的优化,由于触及许多实时查询和大Sql,数据量多是百万级,动不动联查几张十几张表,必要时还需求经过多数据源从ERP的Oracle数据库补偿数据。一期我前期参加了后端服务的开发,担任页面的增删改查导入导出之类的活,没有参加核算的开发,由于其时我很快就被抽出去干另一个项目了。

核算服务一期阅历了三个开发,风格各不相同吧,从现在代码来看,能够说是叠叠乐了,谁都不敢动之前的代码,改得很憋屈。核算是整个订单交给项意图中心,本质便是用户在商城前端创立订单,传递到ERP,然后订单交给项目经过ERP等多个体系的根底数据,拼装订单的具体信息,一同核算约好交期、料号、库位等信息供给给相关事务人员,缩短订单交给周期。

项目二期是由我这个不太懂事务的小伙子作为项目担任人,带着一个厉害的小伙伴进行核算服务的重构,还有个和我相同不明白事务的小伙子担任后端服务的二期需求迭代开发。喔,对了,我还担任后端服务的接口功能优化,详见查询接口功能优化实录,讲点新手也能用的 – (),可谓步履维艰。

整理一二期架构

一期架构便是经典微服务架构,拆分了后端、核算和数据三个微服务各司其职,数据上选用守时全量数据运算,每小时运转一次,一次运转二十分钟。

二期由于实时核算的需求,引进了Flink做数据的支撑,也就变向干掉了本来的数据服务,一同将数据迁移到了分布式数据库TiDB,便利扩容。由于订单交给问题在于大数据量,项目自身对内没有什么高并发,项目需求的根底数据来历于多个体系,其间ERP的表数据量一般是千百万级,大部分仍是基表中的基表,需求做许多的联查,一联查数据量就更大了。

整个数据处理流程是,OGG搜集Oracle多个基表推送音讯到Kafka,利用Flink消费音讯,清洗掉部分无用的字段和数据,小部分能做宽表的做宽表,数据处理完毕后存放到TiDB中。限于公司Oracle数据库的数据质量太差,很惋惜没有将核算做到Flink上.。难点主要在于开发时刻短,这个我都不想吐槽了,看过我历史文章的兄弟们必定能了解我的开发速度只能说异于常人了,麻了。除开时刻的问题,主要是核算事务代码前期进行整理时发现过于杂乱,全程10个核算小模块,中心触及许多的数据库实时查询以及修正库的操作,限于我对事务的了解尚浅,不能雷厉风行地修正核算流程。因而我和跟我一同重构的小伙伴,判断该核算不太合适放到Flink直接核算,而是选用spring boot实时消费Kafka音讯来做实时。因而咱们的重心就变成,怎样将二十分钟的核算流程优化到尽量短的时刻。

怎样快速上手老项目

一般的小项目,看看页面和需求文档,读读代码跑一下程序差不多就能了解个大约。可是面临多模块上万行代码的究极恶龙,这种办法无疑是极端费时吃力的,特别是在你的领导催你干活,定的工期紧得像冬天雪地电线杆舌头相同的时分。我能了解海绵能够挤水,可是有的人想把海绵变成水,就踏马离谱。说一千道一万,阅历过太多的我,浑身都是功能优化和提效强化的姿态,见的那啥多了,天然会总结一些套路复用。

老项目特别是大点的项目都有些个大缺点,比方轮到你背锅的时分或许都是N代目了,文档和各种记载往往仍是最初那个人留下的,后边接手的大多得过且过,这时分你要问我的主张?我的主张是当然是加入他们啊,你在想啥呢,有时分项目能跑,或许刚好便是有个BUG打通了任督二脉,万一让你改了岂不坏事了。还有的便是代码质量问题了,几轮开发下来,没有统一的标准很简单变成了各写各的,后来的不敢改前任的代码,只能搁这玩叠叠乐。说实话这质量都不用想了,我回忆起了一些欠好的东西,呕。

我虽然有时分会生气,有些搭档们的代码属于典型的不可保护,甚至会出现显着的初级过错,这种肝火一般出现在合作开发以及做优化的时分。可是从我个人角度来讲,我能够了解我们,究竟工期实在是太离谱了,三天一个看板项目从零开端的后端开发,甚至没有数据库规划和需求交流,看过我之前文章规划计划-大数据量查询接口优化 – ()的读者,必定体会到这一份艰辛。在这种速度下开宣布的代码质量不高,确实情有可原,一同我也是尽或许地从我开发者的方向,为我们继续产出高质量的组件,供给相似开发结构的体验,削减我们额外的工作量。

(⊙﹏⊙),跑题了,拉回来,吐槽多了些,来劲了。上手老项意图第一要害必定是经过原型和数据库来对照了解数据流,只整理中心页面相关的要害表,其他不用关注。

要害表的重要字段一定要搞清意思和来历,需求了解从页面哪里更新或许核算的逻辑。

假如只是担任某一块,那就要点了解那一块,其他模块大致了解架构和数据流就行,以点及面。当然最好的状况是,处理线上问题,经过处理问题来加深自己的了解。说白了便是得耐住性质,多看文档,合作文档Debug,当然这一切的条件是有满足的时刻。

那么怎样速成呢?现在的领导总是全都要,不给时刻又要慢工出细活,我说给我一天48小时,他说我全都要,杰出一个你说你的我说我的。当然,这活还得做,有个人安慰我的时分说,钱难挣x难吃,emmm,换人我就开怼了,但对他不可,哈哈。作为菜恐龙必定也是有速成的法子,最极速的办法便是做无情IO机器,我不需求关怀自始至终的逻辑,我只需求了解最终的成果是什么样。然后顺着中心字段往上溯源,了解每个字段的来历,做好这个预备之后,看需求是怎样样的,假如是新的需求,那么就彻底阻隔,数据库表和代码独立出来,不要与之前的代码耦合。假如是在曾经的根底上修正,那就叠叠乐,只增不减,不要乱动,就像署理形式相同,尽量不要动之前的。

说的比较笼统嗷,接下来仍是实践出真知吧,留意了,朋友们,看我操作!

勇者行进中

我在重构时遇到最大的问题便是事务和数据都不了解,属所以空降菜恐龙。和我一同重构核算服务的小伙伴是项意图接盘开发和运维,所以他会相对了解,我想也是,否则两人都不明白,这不废了,还重构啥,我选叠叠乐!限于一个月的开发时刻,我要在极短的时刻内从头了解,还要深刻了解一个现已继续开发运维超越一年半的中心体系,我挑选裂开。

……莫西莫西,我们好,这儿是菜鲤鱼,菜恐龙去歇息补偿裂开的心了,我将代替他来讲讲重构中的大事件,敲黑板了嗷,我们留意听,listen to me please!

规划形式

规划形式我们必定是常用啊,即便你没有故意使用,也会无意间用到一些,比方建造者形式、装饰器形式、署理形式、外观形式。我自己关于规划形式的看法是,假如没有明晰的思路就别用,由于大部分需求规划的代码必定会添加代码的杂乱度,相比带来的扩展性之类的优点,了解门槛的进步反倒是会先给人一榔头。当然你要是做架构的话,必定仍是玩一下规划形式比较好,做事务体系的话,怎样舒畅怎样来吧,不要故意。

我在项目中要点使用了模板形式和单例形式两种规划形式,模板形式用于整理代码逻辑一同为后续的优化供给了代码根底,单例形式则是基于现状的一个功能优化。

模板形式

前面也说到了,我重构的是核算服务,不过这个核算服务也算是比较单纯,主流程是为订单行拼装数据,具体能够拆分为十个子核算。每个子核算也能大致拆分为三种代码,数据预备、数据核算、数据处理及收回。数据预备是从数据库或许接口之类的数据源获取核算需求的数据。数据核算则是拿着预备好的数据,依照事务逻辑进行核算。数据处理及收回,数据处理无非便是入库或许传递到下一个子核算做预备,收回是将无用大目标主动置为null,由于核算流程总耗时会有50秒左右,所以要经过null优先GC(不一定有用,后续出对比文章)。基于这样的代码结构,得到了一个适用于大部分核算模块代码的模板笼统类

import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class AbstractCalculation<T> {
    /**
     * 使命数据预备
     */
    public abstract <T> T prepare(CommonDTO commonDTO);
    /**
     * 使命数据核算
     */
    public abstract boolean calculation(T t, CommonDTO commonDTO);
    /**
     * 使命成果处理
     */
    public abstract boolean handle(T t, CommonDTO commonDTO);
    /**
     * 使命执行办法,用于调用上面三个笼统办法
     */
    public Boolean process(CommonDTO commonDTO) {
        String taskName = this.getClass().getSimpleName();
        long startTime = System.currentTimeMillis();
        T prepare = prepare(commonDTO);
        long endTime = System.currentTimeMillis();
        log.info("使命数据预备--耗费时刻:{}秒,使命称号:{}", (endTime - startTime) / 1000, taskName);
        startTime = System.currentTimeMillis();
        calculation(prepare, commonDTO);
        endTime = System.currentTimeMillis();
        log.info("使命核算--耗费时刻:{}秒,使命称号:{}", (endTime - startTime) / 1000, taskName);
        startTime = System.currentTimeMillis();
        handle(prepare, commonDTO);
        endTime = System.currentTimeMillis();
        log.info("使命成果处理--耗费时刻:{}秒,使命称号:{}", (endTime - startTime) / 1000, taskName);
        return Boolean.TRUE;
    }
}

一期的核算服务说实话,上万行并且有两三轮开发的叠叠乐,现已成了依托答辩,代码注释呢,有但不彻底有,这就不细说了,信赖我们也有相似的遭遇。但刚好是由于叠叠乐,所以主核算能根据类拆分组合后能得到具体的子核算代码,因而我和另一个小伙伴首要确认做的事便是将一期的代码整理分类,搬过来完成模板笼统类,大致如下。

@Service
@Slf4j
public class MaretNameMatchTemplate extends AbstractCalculation<MarketCodeNameMatchParam> {
    private final XXX xxx;
    public MaretNameMatchTemplate(XXX xxx) {
        this.xxx = xxx;
    }
    @Override
    public MarketCodeNameMatchParam prepare(CommonDTO commonDTO) {
        //TODO 调用各类接口或办法预备数据a和b
        return new MarketCodeNameMatchParam(a, b);
    }
    @Override
    public boolean calculation(MarketCodeNameMatchParam param, CommonDTO commonDTO) {
        //TODO 数据核算
         return true;
    }    
    @Override
    public boolean handle(MarketCodeNameMatchParam param, CommonDTO commonDTO) {
        param.setA(null);
        param.setB(null);
        return true;
    }
}

说是搬也不是彻底搬,除了最基本的分类代码,还要一同做优化,将循环里能拆出来的IO操作,提出来放到数据预备办法中做批量或许全量MAP。留意,要点来了,既然子核算能这样拆分,主核算流程天然也能这样拆分。

@Service
@Slf4j
public class ConcreteCalculation extends AbstractCalculation<DelculationData> {
    @Autowired
    private PlatformTransactionManager transactionManager;
    @Autowired
    @Qualifier("ioDenseExecutor")
    private ThreadPoolTaskExecutor ioDense;
    private final ManualFilingOtdTimeTemplate manualFilingOtdTimeCulation;
    private final ErpOrderUpInsertTemplate erpOrderUpInsert;
    private final DelCulationOtdTimeTemplate otdTimeCulation;
    private final MaretNameMatchTemplate maretNameMatchTemplate;
    private final ResourceAllocationTemplate resourceAllocationTemplate;
    private final RemoveDataTemplate removeDataTemplate;
    private final ReleaseOccTemplate releaseOccTemplate;
    private final SuggestDeliveryCalculationTemplate suggestDeliveryCalculationTemplate;
    private final CalculationAppointDeliveryTemplate calculationAppointDeliveryTemplate;
    private final CommittedDeliveryService committedDeliveryService;
    public ConcreteCalculation(ManualFilingOtdTimeTemplate manualFilingOtdTimeCulation,
                               ErpOrderUpInsertTemplate erpOrderUpInsert, DelCulationOtdTimeTemplate otdTimeCulation,
                               MaretNameMatchTemplate maretNameMatchTemplate,
                               ResourceAllocationTemplate resourceAllocationTemplate,
                               RemoveDataTemplate removeDataTemplate, ReleaseOccTemplate releaseOccTemplate,
                               SuggestDeliveryCalculationTemplate suggestDeliveryCalculationTemplate,
                               CalculationAppointDeliveryTemplate calculationAppointDeliveryTemplate,
                               CommittedDeliveryService committedDeliveryService) {
        this.manualFilingOtdTimeCulation = manualFilingOtdTimeCulation;
        this.erpOrderUpInsert = erpOrderUpInsert;
        this.otdTimeCulation = otdTimeCulation;
        this.maretNameMatchTemplate = maretNameMatchTemplate;
        this.resourceAllocationTemplate = resourceAllocationTemplate;
        this.removeDataTemplate = removeDataTemplate;
        this.releaseOccTemplate = releaseOccTemplate;
        this.suggestDeliveryCalculationTemplate = suggestDeliveryCalculationTemplate;
        this.calculationAppointDeliveryTemplate = calculationAppointDeliveryTemplate;
        this.committedDeliveryService = committedDeliveryService;
    }
    @Override
    public DelculationData prepare(CommonDTO commonDTO) {
        DelculationDataBuilder builder = new DelculationDataBuilder();
        DelculationDataDirector director = new DelculationDataDirector(builder);
        DelculationData delculationData = director.create();
        log.info("**人工报备核算开端**");
        manualFilingOtdTimeCulation.process(commonDTO);
        log.info("**人工报备核算完毕**");
        log.info("**erp订单新增或修正开端**");
        erpOrderUpInsert.process(commonDTO);
        log.info("**erp订单新增或修正完毕**");
        log.info("**出货数据开释库存开端**");
        releaseOccTemplate.process(commonDTO);
        log.info("**出货数据开释库存完毕**");
        return delculationData;
    }
    @Override
    public boolean calculation(DelculationData concreteDelculation, CommonDTO commonDTO) {
        //OTD时效核算,核算后的数据进交期核算表
        log.info("**交期数据otd核算开端**");
        otdTimeCulation.process(commonDTO);
        log.info("**交期数据otd核算完毕**");
        log.info("**市场代码称号匹配开端**");
        maretNameMatchTemplate.process(commonDTO);
        log.info("**市场代码称号匹配完毕**");
        log.info("**约好交期核算开端**");
        calculationAppointDeliveryTemplate.process(commonDTO);
        log.info("**约好交期核算完毕**");
        log.info("**主张交期核算开端**");
        suggestDeliveryCalculationTemplate.process(commonDTO);
        log.info("**主张交期核算完毕**");
        log.info("**资源分配核算开端**");
        resourceAllocationTemplate.process(commonDTO);
        log.info("**资源分配核算完毕**");
        return true;
    }
    @Override
    public boolean handle(DelculationData concreteDelculation, CommonDTO commonDTO) {
        List<OdDeliveryCalculation> updateList = commonDTO.getUpdateList();
        if (!updateList.isEmpty()) {
            Instant end1 = Instant.now();
            batchSchedule(updateList);
            log.info("============更新耗时:{}s===================", ChronoUnit.SECONDS.between(end1, Instant.now()));
        }
        //删除过期数据
        log.info("**删除过期数据开端**");
        removeDataTemplate.process(commonDTO);
        log.info("**删除过期数据完毕**");
        commonDTO.setDeliveryList(Collections.emptyList());
        commonDTO.setUpdateList(Collections.emptyList());
        commonDTO.setMarketStockNum(Collections.emptyMap());
        commonDTO.setMarketOccupyNum(Collections.emptyMap());
        return true;
    }
    private void batchSchedule(List<OdDeliveryCalculation> addList) {
        if (!CollectionUtils.isEmpty(addList)) {
            AtomicBoolean isSuccess = new AtomicBoolean(true);
            AtomicInteger cur = new AtomicInteger(1);
            List<Thread> unfinishedList = new ArrayList<>();
            //切分新增集合
            List<List<OdDeliveryCalculation>> partition = Lists.partition(addList, 100);
            int totalSize = partition.size();
            //多线程处理开端
            CompletableFuture<Void> future =
                    CompletableFuture.allOf(partition.stream().map(addPartitionList -> CompletableFuture.runAsync(() -> {
                        DefaultTransactionDefinition defGo = new DefaultTransactionDefinition();
                        defGo.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
                        TransactionStatus statusGo = transactionManager.getTransaction(defGo);
                        int curInt = cur.getAndIncrement();
                        try {
                            committedDeliveryService.updateBatchById(addPartitionList);
                            synchronized (unfinishedList) {
                                unfinishedList.add(Thread.currentThread());
                            }
                            notifyAllThread(unfinishedList, totalSize, false);
                            LockSupport.park();
                            if (isSuccess.get()) {
                                transactionManager.commit(statusGo);
                            } else {
                                transactionManager.rollback(statusGo);
                            }
                        } catch (Exception e) {
                            transactionManager.rollback(statusGo);
                            isSuccess.set(false);
                            notifyAllThread(unfinishedList, totalSize, true);
                        }
                    }, ioDense)).toArray(CompletableFuture[]::new));
            future.join();
        }
    }
    private void notifyAllThread(List<Thread> unfinishedList, int totalSize, boolean isForce) {
        if (isForce || unfinishedList.size() >= totalSize) {
            for (Thread thread : unfinishedList) {
                log.info("其时线程={}被唤醒", thread.getName());
                LockSupport.unpark(thread);
            }
        }
    }
}

CommonDTO是后加的,原因是其时在填充模板的时分,我想到了一个问题,为什么不进一步合并数据预备模块呢。虽然各个子核算相对独立,但总是会查一些差不多的数据,那么要主动树立一个衔接,削减重复数据的查询,因而加入了CommonDTO这个数据传递参数。下面数据处理也是一点小技巧,由于部分数据最终会放到同一张表,所曾经面的子核算就不要去更新数据库,经过CommonDTO传递,直到最终统一入库,削减数据库操作。这儿用到了多线程事务,一开端是没打算用的,成果实测时发现更新一千条数据需求40秒,或许是由于单表九十多个字段太多了吧,没办法就换了。多线程事务敞开后进步到了10秒内,相关细节见功能优化-怎样爽玩多线程来开发 – 114保藏。

最终总结一下,引进模板规划形式带来的最显着的优点便是代码结构明晰,每个模板完成类是什么功能一望而知。除此之外数据预备、数据核算、数据处理代码块分类明晰,便于后续的优化和迭代运维。

单例形式

OGG搜集的是Oracle订单表的变化,为了保证顺序性,推送Kafka的Topic分区设置为1。由于分区设置为1,所以同一个消费者组里只能有一个消费者进行消费,核算的并行度为1。为了保证消费速度满足快,在进步核算速度的一同将整个核算流程改造为了支撑批量,因而设置Kafka消费单次抓取1000条。为了保证音讯在核算报错的时分不丢失,挑选手动提交消费偏移量,一同根据核算时刻拉长了Kafka消费间隔时刻,参数装备能够看音讯积压问题难?思路代码优化细节全揭露

@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> onceFactory() {
    ConcurrentKafkaListenerContainerFactory<String, String> container = new ConcurrentKafkaListenerContainerFactory();
    Map<String, Object> props = new HashMap(16);
    props.put("bootstrap.servers", this.prop.getKafkaServers());
    props.put("key.deserializer", StringDeserializer.class);
    props.put("value.deserializer", StringDeserializer.class);
    props.put("group.id", StringUtils.isEmpty(this.prop.getConsumerGroupId()) ? "onceConsumerGroup" : this.prop.getConsumerGroupId());
    props.put("fetch.min.bytes", 1048576);
    props.put("max.poll.records", 1000);
    props.put("max.poll.interval.ms", 300000);
    props.put("enable.auto.commit", "false");
    container.setConsumerFactory(new DefaultKafkaConsumerFactory(props));
    container.setConcurrency(1);
    container.setBatchListener(true);
    container.getContainerProperties().setAckMode(AckMode.MANUAL);
    return container;
}
@KafkaListener(topics = {"ERP_YC_CUX"}, containerFactory = "onceFactory")
public void oggListener(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
    //TODO 消费音讯并核算    
    ack.acknowledge();
}

这儿单例形式的使用其实便是一个本地缓存,由于单线程的约束,所以没有必要用分布式缓存Redis,用了反而添加IO开支。使用办法很简略,定义一个单例,内部特点便是咱们需求的部分数据,采取旁路缓存形式。

/**
 * @author WangZY
 * @classname CommonData
 * @date 2023/3/15 17:45
 * @description 共享变量
 */
@Data
public class CommonData {
    private volatile static CommonData instance;
    private CommonData() {
    }
    public static CommonData getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new CommonData();
                }
            }
        }
        return instance;
    }
    /**
     * 物料ID--实体
     */
    private Map<Long, MtlSystemItemsB> itemIdMap;
    /**
     * 物料编码--实体
     */
    private Map<String, Long> itemCodeMap;
}
/**
 * @author WangZY
 * @classname InitDataTask
 * @date 2023/3/15 18:15
 * @description 初始化数据
 */
@Component
public class InitDataTask {
    private final IMtlSystemItemsBService itemService;
    private final IOdDeliveryCenterService centerService;
    private final IOdsErpStockAgeDService stockAgeService;
    public InitDataTask(IMtlSystemItemsBService itemService, IOdDeliveryCenterService centerService,
                        IOdsErpStockAgeDService stockAgeService) {
        this.itemService = itemService;
        this.centerService = centerService;
        this.stockAgeService = stockAgeService;
    }
    @PostConstruct
    public void initData() {
        CommonData instance = CommonData.getInstance();
        List<MtlSystemItemsB> list = itemService.lambdaQuery().select(MtlSystemItemsB::getInventoryItemId,
                MtlSystemItemsB::getSegment1).list();
        Map<Long, MtlSystemItemsB> itemIdMap = new HashMap<>();
        Map<String, Long> itemCodeMap = new HashMap<>();
        for (MtlSystemItemsB item : list) {
            Long itemId = item.getInventoryItemId();
            String itemCode = item.getSegment1();
            itemCodeMap.put(itemCode, itemId);
            itemIdMap.put(itemId, item);
        }
        instance.setItemCodeMap(itemCodeMap);
        instance.setItemIdMap(itemIdMap);
   }
}

CommonData是一个经典的两层检查锁单例写法,在InitDataTask办法使用@PostConstruct在spring boot发动时执行一次,做了一下缓存预热。

拒绝叠叠乐!我用设计模式重构核心项目
这儿也是有个小细节,CommonData内部有Map itemIdMap和Map itemCodeMap两个特点,这儿都是从物料表获取数据。假如直入直出的写,会像上图相同依照物料ID和物料编码为key,实体类为value各做一个map。可是该表的物料ID和物料编码其实是一对一的对应联络,因而经过物料编码–>物料ID–>实体类的桥接,也能到达相同的效果,可是用桥接的物料ID做value比实体类更省内存。

代码优化

优化IDE提示问题的代码

拒绝叠叠乐!我用设计模式重构核心项目

随意拉个搭档的代码上来抽打,其时我说我改完了之后让你看看我怎样改的,但我也不知道截至发文这天你有没有看,横竖是感谢(咬牙切齿)友谊供给的资料。此类问题许多,由于我用的IDEA,会有高亮提示,所以一般看到了就会清掉,我们也要养成良好的代码习气,点一点就完事了。

老生常谈的合并查询

详见查询接口功能优化实录,讲点新手也能用的的优化手法一栏,里边有说到List转Map和批量查询两种手法。合并查询的意图便是一次性查出所有需求的数据,经过代码树立联络,而不是循环一个个查询。打个比方,我现在是一千条批量消费,需求循环一千次填充订单的某些字段,这时分就不或许在循环里进行数据库查询,由于哪怕是几百毫秒乘以一千也是十分恐惧的时刻耗费。

削减重复查询

这是许多老项意图通病了,几轮开发下来,或许不自觉间就积累了许多重复查询,所以做代码整理时就尤其要留意此类问题,及时处理。削减重复查询不仅仅要删,还要改,加缓存往往是好的手法。

拒绝叠叠乐!我用设计模式重构核心项目

削减数据量

由于订单交给项意图数据源繁杂多样,咱们不能操控他们怎样传过来,可是咱们能做的是在接收端尽量精简字段,然后削减数据量。比方订单交给经常会从ERP中经过多数据源结构直接读取数据,我没见过其他ERP体系,横竖咱们这个是比较离谱,像是这种几百个字段千百万甚至过亿数据量的表比比皆是。

拒绝叠叠乐!我用设计模式重构核心项目

这种状况下必定就不能几百个字段一块拿了,很简单触发OOM问题,几个G必定不够你使的,抽取要害字段就尤为重要。

拒绝叠叠乐!我用设计模式重构核心项目
不要偷懒,mybatis-plus之类的结构固然好用,可是该优化的时分,就老老实实写Sql生成转换类。

数据库表规划优化

口语化解说数据库优化 – ()之前在这篇优化的文章中说到过三大数据库优化方向,削减数据量、空间换时刻、替换架构。这儿削减数据量,我还用到了添加冗余字段以及生成成果表两种优化手法。

冗余字段,由于主表的字段接近100个字段,所以不能随意加字段,避免单表功能下降。因而字段加不加,我的判断根据有两个,是否核算过程中能用到和是否能验证核算成果。核算中能用到可是不会最终展示的字段,比方物料ID,订单ID这种用于相关其他表的字段,我认为是有必要存一份,不是所有人都有查日志和记日志的好习气,适当地给搭档一点暖心的记载是一件好事。再说一般ID都是数字类型,说实话不占太多空间,何须介意那一点丢失呢。还有一些字段能验证核算成果,比方核算的中心值,有时分与其费力巴拉地刨日志,不如直接就在表里记载上就好了,简略明了。

成果表大多是咱们经过Flink处理后的一些中心表数据,或许数仓那里直接取过来的部分数据,算是省了许多的Sql联查。我做这个项目一开端真的是两眼一黑,ERP组的搭档老是动不动甩过来一些几百行带函数和视图的大Sql,说实话,我感觉看到这Sql,一点主意都没有了,只想摆烂。一张表最少几十个字段,就一小半有注释,有的字段便是那什么好听点叫扩展性字段,比方segement1、segement2…..14,我也不知道为啥这些123456的为啥就扩展了。我司买的用友的ERP,话说有没有用友的老哥来讲讲你们表真的这么规划吗,太笼统了吧!

拒绝叠叠乐!我用设计模式重构核心项目
这是核算过程中用到的一个ERP供给的订单根底信息的Oracel Sql,红框圈起来的是程序包,里边一般是几十行的一个子查询。Sql大约都这个姿态,我也不清楚为啥怎样ERP那儿怎样搞着搞着就成这个姿态了,仿佛不叠大Sql就没法开发了相同。(⊙﹏⊙),看到这个的ERP搭档,下回还求你们帮我改Sql,我是真不会啊,这Sql我看了一眼黑。

多线程多线程

作为我最熟练也是最喜欢的优化手法,我自己写了一些代码片拿来即用,十分便利也是十分有效的操作,专治大循环以及大批量数据库操作。功能优化-怎样爽玩多线程来开发 – 114保藏关于多线程的瑰宝我都放在这篇文章里了,大航海时代,开端了(笑)。

核算可靠性

核算服务、核算服务,那天然不能只说功能,可靠性也是必须保证的重要一环。有测试过全量核算一次是在5分钟左右,因而核算服务选用实时增量加守时全量两套计划并行。实时天然是保证时效性,每半小时会有一次核算成果的最终更新时刻查询,假如这半小时没有更新数据,那么发动一次全量核算并告诉开发人员进行人工处理。守时作为兜底,保证即便守时出现问题,也能保证半小时的时效,不至于彻底不能用。

实时部分为了搜集反常数据,添加了反常信息表,使命重试3次依然失利,则发送音讯告诉人工处理。由于要保证音讯顺序性,所以要手动提交消费偏移量,即便音讯积压也要回绝顺序导致的数据处理反常。前面说到的全量是从ERP的原核算成果表中取数据,因而不存在顺序问题,这个在考虑之中不需求忧虑。嘻嘻,欠好意思,再贴一篇我写的处理音讯积压的文章,音讯积压问题难?思路代码优化细节全揭露 – ()。哎呀,怎样什么倒霉问题都让我遇上了,便利我王婆卖瓜来着。忽然有个主意,假如我去带货会不会是一把能手,真是一把子无语住了,家人们,谁懂啊!(你干嘛,哎呦)

最终整了几个接口作为核算进口,便利人工介入处理,作为开发留一个小小的后门不过分吧。

少男祈求中(复盘)

这会儿写文的时分,听到这首歌,跟着摇起来了,很有感觉くびったけ-yama,忽然感觉轻快了起来。

从年前两周被告诉要重构到年后忽然成为这个被重构中心项意图担任人再到今日,发生了许多许多事,可是一看时刻,也便是不到两个月的时刻。原计划是一个月弄完,可是中心夹杂着其他项意图活,真实开端做重构到现在也便是一个月的开发时刻吧。现在首要是阐明一下我这个担任人的状况,在努力承受项意图事务逻辑中,现在是能对整个项目有个根底的了解,核算服务相对了解些,可是说白了仍是需求别人的协助才干找问题。或许有各式各样的托言,比方我一同开发着两三个项目,有着各式各样的运维和问题处理,日子中也有些不如意的当地,导致我最近心情不佳,进犯性很强。对被进犯过的搭档说一声抱愧,我的问题,过后我也会主动下个话,究竟我们都是来上班的,不是来受气的。

核算服务重构从一开端便是我和另一个小伙伴的二重奏,说真的,做的过程中太难了,没人能帮忙。也没有人手抽过来协助我俩,无他,项目事务杂乱度上来了,短时刻上不了手,包含我也是,即便到现在我也没法说一句我懂这个项目,查问题依旧需求别人的协助。由于核算这个和页面逻辑不相同,他没有一个明确的进口,有时分便是无从查起,想用力都没招,特别是小伙伴立刻换岗跑路了,剩我一个独苗。我踏马真的是,心累了,上头了。

说回复盘,我的感觉便是混乱,从上到下没有一个完整的项目管理流程。一切的起源来自于其时被告诉我要参加主导重构,然后…emmm,就没有然后了。让另一个小伙伴给我讲了下大致的核算流程就完事了,也没说怎样做,便是主打一个自由发挥,我只能说感谢领导信赖。不过仍是有一些预备的,比方整理了下核算需求的字段来历,对部分Sql做了解释,问题是这远远不够啊。实时的依托是什么,就只有一个Flink吗?从我的角度来看,为什么要用Flink,是需求理由的,不是说什么东西往Flink上一套就成了实时XX,这玩意是东西不是互联网+创业大赛,哪有那么好用?

最终经过Kafka来做实时是结合前期数据预备和需求阐明耗时三天敲定的计划,说实话,这就离谱。咱们重构是在项目开端半个多月时才敲定的最终完整计划,要知道给的开发时刻总共也就一个月时刻,因而在我看来,时刻的急迫、极端匮乏的预备还有开发一同背着多个项目并行,导致了重构开局的极限地狱。假如我要做一个项意图重构,首要保证中心开发人员必须专心于这个项目,不会三心二意导致思路中断,一同该给的数据预备和事务逻辑一定要明晰,数据预备就不说了,至少保证每个给出来的Sql取值和逻辑正确吧,别老是开端用了才知道哪哪都不可,孩子没了才知道奶了,这能行嘛。还有事务逻辑也是,换了新的PM接手,后端开发总共三人就一个人懂事务,差点凑两队卧龙凤雏了。不过也得亏PM漏需求了,否则后边这个多出来的一个月开发时刻还不知道咋争取到。

核算服务确认好计划后,仍是度过了比较顺畅的开发周期,便是搬老代码一同做优化。优化比较费工夫,由于老代码部分现已优化过了,要想深度优化的话,必定是进行适当程度的代码改动,水磨工夫不可少。我和小伙伴是采取平和分工,整个音讯消费和核算主线由我来构建,再各自担任一半核算模块,由相对了解的小伙伴担任跨核算模块的数据抽取优化,我来补偿整体的可靠性开发。

核算主流程的重构虽然磕磕绊绊,但也算是告一段落了,从我个人的角度来看,我是不满足的,由于还有大把功能上的问题没有处理。内存溢出过、CPU百分百过、GC过于频频以及连物理机的网络带宽都爆了,真的是问题多多,这些问题,后续会出文章单独解说。站在公司的角度,我认为相同也是难以满足的,虽然从成果上看仅靠两人在一个多月的时刻就完成了一个中心项意图重构,并且由一般的守时更新服务晋级到了实时核算,这是一个适当性价比的成果。可是这就导致一个问题,我俩是彻底没有任何文档留痕的,包含整个计划的确认都是口头定下来的,除了我俩再来个人他都不知道写了啥。现在小伙伴换岗(恭喜恭喜),也给我留下了巨大的难题,我该怎样去接手他担任的模块,怎样才干快速响使用户的问题并处理,单单靠一份简略的交接文档,我觉得是很难做到的。哎,我现在也没啥好的计划,假如读者有遇到相似的问题,能够私信或许回复我,我最近真的很头大,不知道该怎样处理这些难题。

话说回来,这是不是又是一个新的文章资料呢。我上星期还和一个搭档玩笑说,别老是给我制作功能缺口了,多亏了我们,我的资料源源不断。本次重构的过程中也处理了许多的问题,从低到高各种过错缺点都有,可惜我忘了记一下了,下次一定好好截图,记载下我们的高光时刻。不过我自己也是有许多犯错的时分,详见鲨逼操作记载有时分需求翻开思路-开膛手参上(含过错思路具体记载) – (),过错是很正常的,没人不犯错,及时改正就OK。

写在最终

首要跟我们说一声抱愧,上星期五写的时分,感觉压力太大了,忽然写不下去了,思路就断了。今日2023.03.26下午想要快速进入状况,可是一直卡着没进去,代码优化这块算是拉了,用了一些之前的文章做内容填充,导致整篇文章有点为德不卒了,没有到达我自己预期的效果。最近一两月吧,工作十分多,一下子全压到我头上,有点蚌埠住了,我知道这也是个托言吧,自己骗自己时间短歇息下。我也不求我们同情,过一阵子自己就好了,年轻人哪有那么矫情,这年头我们都很难,仅有抱愧的是影响了写作状况,没有把这次难得的阅历百分百呈现出来,有点对不起我们,也不太能对得起自己的付出,哎。

文章会有一些割裂感,由于我是这一周多断断续续下班后写的,思路不连贯,有时分我也没法很好地衔接起来。状况有点欠好,可是也不能一直停滞不前,所以本文仍是尽量以我其时最好的状况展示出来了,希望能对我们有一点小小的协助。其实在写文章对我来说算是一种解压的重要手法,不知道我们能不能看到我文章的保藏数,我今日看了看,757点赞719保藏。保藏数这么高,我是很高兴的,证明我写的文章确实是很好的协助了,并且我有两篇我觉得写的十分好的文章都有超越一百的保藏数,对我来说有着极强的正反馈。真的谢谢我们的助威,我也一定会为我们继续产出高质量有深度的文章。

最终的最终,再次着重一次我的人生信条,做一个高兴的人,在这苦楚压抑的国际绽放幸福高兴之花!向美好的国际献上祝愿,诸君共勉,下一篇文章不见不散!!!