线索系统性能优化实践

引言

在京东家居事业部,线索CRM系统扮演着至关重要的角色,它作为构建家居场景核心解决方案集的首要环节,肩负着获客和拓展业务的重要使命。然而,随着业务的不断扩张和市场需求的日益增长,系统原有的架构开始显露出诸多不适应之处,如架构设计不再清晰,代码存在过量冗余,核心的读写接口响应时间长等问题,这些问题严重制约了业务的敏捷性和快速发展。鉴于这一状况,系统的性能优化和调整势在必行,以确保其能够更好地支撑业务的快速发展需求。

系统优化概述

一. 线索提交接口的统一与性能优化

系统优化前存在的问题

1. 新渠道接入周期长,代码冗余,

系统中存在五个主要的线索创建渠道,它们的处理流程高度相似,但是代码却是分散冗余的。每当有新渠道需要接入时,之前的做法都是从已有代码中复制粘贴并做小幅调整,缺乏抽象和封装,导致了代码的高度重复,增加了维护的难度和出错的风险。

比如当时我们为了支持多供应商这个需求,需要对线索分派商家的逻辑进行更改,由于这段逻辑分散在多处,同时由于测试对底层实现的不了解,可能会误认为只需要测试一个渠道就能覆盖基本场景,就有可能导致非必要的线上问题的产生。

2. 性能瓶颈

在线索创建过程中,由于业务的复杂性需要执行10来个子流程以及开发过程中的不规范导致的对线索主数据的不必要的重复更新、重复同步ES等问题,接口性能较慢,tp99将近3000ms。

3. 数据一致性问题

线索创建主数据,分配商家以及匹配到重复规则时需要新增运营回访记录,这些流程都涉及到对数据库的写操作,但是这些写入没有放在同一事务中,导致了某个子流程写入失败时存在数据一致性问题。

优化目标

我们的优化目标是对线索提交流程统一封装和抽象,提高系统的可维护性和扩展性。同时降低新渠道接入的时间成本,提高接口性能和响应,保证接口在复杂情况下的正确性。

优化策略与实施

流程梳理与抽象

我们首先对当前所有渠道的线索创建流程进行了全面的梳理,将线索的创建流程抽象化,并定义出一套标准化的流程模板。具体来说一个线索的创建包括以下流程:

(1) 入参校验

(2) 查询三级渠道

(3) 验证三级渠道开关

(4) 根据入参封装要创建的线索实体

(5) 线索是否重复的规则校验

(6) 数据库保存线索和异构到ES

(7) 读取配置规则以及后续的同步京音系统

(8) 将新线索分配到对应的商家

(9) 短信消息和京麦消息通知商家

(10) 根据线索和分配的商家信息为用户创建装修档案

创建流程拆分

通过分析发现,对于不同渠道的线索创建过程来说,最大的差异点在于流程(1) 和 (2), 对于流程(3)-(10)基本相似。

这些流程虽然在逻辑上紧密相连,但是对于线索创建这一业务来说最核心的流程是流程(6)及之前的流程,至于流程(7)-(10)则是线索创建后的附属操作,这些附属操作涉及到和外部门系统间复杂的交互,占用了大量资源并影响到核心流程的响应速度。

因此我们聚焦于线索创建这一核心流程,和从职责单一的角度考虑,我们将整个线索的常见进行拆分:

第一 核心流程-线索的创建。

第二 线索分配商家以及之后的通知操作

第三 为分配商家后为用户创建对应的装修档案

这三个创建流程通过京东自研消息JMQ进行串联,解耦了线索创建和附属操作的执行。通过异步处理附属操作,附属操作的耗时不会阻塞核心流程的执行,减少了对核心流程的干扰,从而大大提升了系统的响应性和吞吐量。

模板方法设计模式的应用

定义: 模板方法设计模式是一种行为设计模式,它在一个方法中定义了一个算法的骨架,将一些步骤的执行延迟到子类中。这样,子类可以在不改变算法结构的情况下重新定义算法的某些特定步骤。

通用类图:

示例代码:

public abstract class Game {
    // 模板方法,定义算法骨架
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }

    // 需要子类实现的方法
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();
}

public class Cricket extends Game {
    @Override
    void initialize() {
        System.out.println("Cricket Game Initialized! Start playing.");
    }

    @Override
    void startPlay() {
        System.out.println("Cricket Game Started. Enjoy the game!");
    }

    @Override
    void endPlay() {
        System.out.println("Cricket Game Finished!");
    }
}

public class Football extends Game {
    @Override
    void initialize() {
        System.out.println("Football Game Initialized! Start playing.");
    }

    @Override
    void startPlay() {
        System.out.println("Football Game Started. Enjoy the game!");
    }

    @Override
    void endPlay() {
        System.out.println("Football Game Finished!");
    }
}

public class TemplateMethodPatternDemo {
    public static void main(String[] args) {
        Game game = new Cricket();
        game.play();

        System.out.println();

        game = new Football();
        game.play();
    }
}


在这个例子中,Game是一个抽象类,定义了游戏的模板方法play()CricketFootball是具体的游戏,它们实现了Game类的抽象方法,以提供各自的游戏初始化、开始和结束的具体实现。

具体到我们系统, 流程1到10是创建线索的骨架抽象和定义。对于骨架中的子流程,我们识别出易变部分(步骤1和2)和 不易变的部分(步骤3至6)。易变部分需要交给子类去实现,不易变部分则需要统一实现。

易变部分抽象

对于入参校验和查询三级渠道这两个流程来说,每个渠道都存在独有的逻辑,比如,心愿单渠道需要校验心愿单类型和来源ID必传,而投放助手渠道则需校验投放单号必传;多阶段订单渠道是通过SKU来查询三级渠道,而市场部渠道则是通过媒体账号ID来查询。

因此我们对于这两个流程定义了抽象方法,并将实现细节交个具体渠道的负责。

不变部分统一处理

对于线索创建流程中的不易变部分,我们实现了统一的处理逻辑,如三级渠道开关验证、线索归集信息封装、重复规则校验、数据库保存以及异构到ES等流程。

同时对于所有需要数据库变更的操作放到一个事务中,保证了写入的同时成功或失败。

工程实践

通过上文介绍, 编码大体实现如下:

//获取三级渠道
protected abstract ChannelThreeDto getChannel(ClueDTO clueDTO);

//前置状态校验
protected abstract boolean preConditionCheck(ClueDTO clueDTO);

public ResultDto<Boolean> submit(ClueDTO clueDTO) {

        //1.前置状态校验
        if (!preConditionCheck(clueDTO)) {
            return ResultDto.getFailedResult(ResultCodeEnum.SERVICE_ERROR.getMsg(), ResultCodeEnum.SERVICE_ERROR.getCode());
        }

        //2.获取三级渠道
        ChannelThreeDto channelThreeDto = getChannel(clueDTO);

        //3.确认渠道开关是否开启
        if (!checkChannel(channelThreeDto)) {
            return ResultDto.getFailedResult(ResultCodeEnum.SERVICE_ERROR.getMsg(), ResultCodeEnum.SERVICE_ERROR.getCode());
        }

        //4.线索重复校验
        Boolean isRepeat = checkClueRepeat(clueDTO, channelThreeDto);
        if (isRepeat) {
            return ResultDto.getFailedResult(ResultCodeEnum.SERVICE_ERROR.getMsg(), ResultCodeEnum.SERVICE_ERROR.getCode());
        }

        //5.封装线索实体对象
        ClueManageDto clueManageDto = buildClueManage(clueDTO, channelThreeDto);

        //6.数据清洗规则检查
        ClueVisitDto clueVisitDto = clueDataWash(clueManageDto, channelThreeDto);
        if (!ObjectUtils.isEmpty(clueVisitDto)) {
            clueManageDto.setClueStatus(ClueStatusEnum.INVALID.getCode());
        }

        //7.数据库保存
        boolean result = saveClueManage(clueManageDto, clueVisitDto);

        //8.发送线索创建通知,执行之后的线索分配商家等操作
        sendClueMessage(clueDistributionDTO);

        return ResultDto.getSuccessResult(result, ResultCodeEnum.SUCCESS.getCode());
    }


优化成果

通过引入模板方法的设计模式、异步拆分以及优化事务管理策略,创建线索的系统架构得到了根本性的改进。 我们不仅提高了代码的复用率,降低了新渠道接入的成本,也极大地提升了系统的可维护性和扩展性。

现在,新渠道的接入变得更加快捷和灵活,从之前新渠道接入耗时6人/天降低到2人/天左右;同时线索创建的响应时间也从之前的3000ms降到现在的250ms左右。

二. 线索核心写接口性能优化实践

背景

在竞争激烈的市场环境中,CRM系统不仅需要准确无误地收集用户的客资信息,更重要的是要实现对这些宝贵信息的快速响应和有效跟进。用户留下联系方式的瞬间,往往是他们对产品或服务兴趣最浓厚的时刻,我们需要快速响应,抢占先机,才有可能增加用户转化为客户的可能性,因此对于核心接口的性能有较高的要求。

但是当前系统在处理线索创建、分配商家,状态变更以及商家反馈等核心流程上存在接口性能不理想的问题,比如商家反馈线索tp99耗时2000ms, 分配商家耗时1500ms。

问题分析

在每个核心流程中,系统会进行两项重要的操作:

1.更新数据库:将业务操作的结果持久化到数据库中。

2.数据同步到ES:将变更的数据同步到两个ES集群中(一个供运营端查询,另一个供商家端查询适用)

传统同步机制是在业务逻辑操作完成后立即进行数据同步。这种同步方式虽然简单直接,但存在几个缺点:

性能瓶颈:同步操作耗时,导致接口响应时间增长,影响用户体验。

复杂度增加:业务逻辑与数据同步逻辑耦合,增加了代码的复杂度和维护难度。

扩展性受限:随着业务增长,同步操作成为系统扩展的瓶颈。

优化方案

针对上述问题,我们采取了一系列措施来优化系统性能,核心策略是将数据同步到ES的过程异步化。

1. 订阅Binlake变更

我们将业务逻辑操作和数据同步到ES的过程分离。业务接口只负责业务逻辑的变更和数据库的更新,而数据同步到ES的操作,通过订阅Binlake变更事件来异步执行。

2. 处理变更消息

通过订阅线索主数据和线索分配商家数据的变更消息,封装接口将线索主数据和分配商家信息同步到ES。值得注意的是,为了避免数据库变更在JMQ中的乱序性以及可能带来的数据被错误覆盖的问题,我们只关注消息中的哪个线索单号发生了变化,而不关注具体的变更细节,通过线索单号反查数据库的方式,将最新的数据同步到ES。

3. 合并更新和统一事务

在原来的线索分配商家以及商家反馈线索接口中,存在对同一个表反复更新并且多次同步ES的问题,通过底层重构,我们把所有的DML操作合并到一个事务中,减少更新次数的同时保证了数据的正确性。

4. 非核心流程异步化

把原来线索反馈商家接口中的非核心流程异步化。在商家反馈线索状态后需要触发回流操作,回流操作本身就是一个非常耗时的操作,经常导致用户反馈接口超时,但是回流本身是用户不关注的,用户只关注他反馈的动作是否完成。因此我们对回流进行异步化,反馈线索接口现在只处理线索状态的更新,回流则是通过发送JMQ消息的方式异步处理来减少用户等待时间。

优化成效

经过优化,线索系统的性能得到了显著提升:

1. 接口响应时间明显缩短

(1) 线索提交 (投放助手渠道):

优化前:(2000-4000ms)

优化后:(100-300ms

(2) 线索分配商家接口:

优化前:(1000-2000ms)

优化后:(100-400ms

(3) 商家反馈线索接口:

优化前:(1000-3000ms)

优化后:(30-60ms

2. 用户体验改善:商家在反馈线索状态时不再遇到超时问题。

3. 架构清晰:业务逻辑与数据同步逻辑解耦,代码更加清晰和容易维护。

4. 扩展性提升:异步化后的数据同步流程为未来的系统扩展提供了更大的空间。

三. 线索列表读接口性能优化实践

优化前的挑战

在运营管理CRM系统的实践中,线索列表的查询功能是不可或缺的一环,它支持基于复杂组合条件对线索数据进行精细筛选。然而,在当前的系统实现中,线索列表页面需要展示每页50条或100条线索数据时,接口性能表现并不理想:响应时间普遍超过2000毫秒,有时甚至延迟至6000毫秒。这一性能瓶颈已经引起了用户的广泛关注和较为严重的负面反馈。

优化策略

通过对接口的分析,接口性能瓶颈主要来源于以下几个方面:

1.多次ES查询: 先根据搜索条件查询一次ES获取基础数据后,再循环遍历列表,对每个线索再查询两次ES来获取线索的手动及自动分配商家数量。

2.频繁的RPC调用:循环遍历线索列表为每个用户进行RPC调用以获取用户昵称。

3.过多的远程调用:ES查询和获取用户昵称都是调用服务端服务。

针对这些拖慢接口性能的瓶颈点,我们采取下列优化措施:

1.减少远程调用: 我们将线索运营端多次请求服务端的过程调整成单次调用。查询逻辑都下沉到服务端,由服务端查询所有字段,运营端只需要调用一次,从而显著减少了网络延迟。

2.聚合查询优化:我们利用ES的Aggregation聚合API,一次查询获取当前分页内所有线索的手动分配和自动分配商家数量,减少了多次查询的性能损耗。

代码部分实现:

BoolQueryBuilder query = QueryBuilders.boolQuery();
//线索单号列表过滤
query.filter(QueryBuilders.termsQuery("clueNo", clueIds));

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.trackTotalHits(true);
searchSourceBuilder.query(query);

IncludeExclude includeExclude = new IncludeExclude(new String[]{"1"}, null);
//按照分配类型聚合1
AggregationBuilder aggregation2 = AggregationBuilders.terms("distributionType").field("distributionType").includeExclude(includeExclude);
//按照线索单号聚合2
AggregationBuilder aggregation1 = AggregationBuilders.terms("clueNo").size(100).field("clueNo").subAggregation(aggregation2);
searchSourceBuilder.aggregation(aggregation1);

SearchRequest searchRequest = new SearchRequest();
searchRequest.indices(vendorClueESIndexName);
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);


1.合理使用缓存:针对用户昵称变动频率低的特点,我们引入了缓存机制, 首次RPC查询用户的昵称成功后对结果进行缓存,再次请求时直接从缓存获取昵称,减少RPC次数。

2.并行: 在处理线索列表填充手动分配和自动分配商家数量以及用户昵称的过程中,我们使用parallelStream()并行流技术,从而加快数据处理速度。

通过以上优化方案, 对于查询100条线索需要的查询次数:

优化前: 1次ES查询列表 + 200次ES查询分配商家数量 + 100次RPC

优化后: 1次ES查询列表 + 1次ES查询商家分配数量 + 100次RPC(有缓存下会减少次数)

优化成果

响应时间缩短

优化前:

优化后(250ms以下):

总结与展望

通过对线索系统的深度优化,我们不仅解决了线索系统在核心流程中的性能瓶颈,也为系统的长期健康发展奠定了基础。这一实践表明,适时地对系统架构进行优化,能够有效提升系统的性能和可维护性,进而支持业务的快速增长和变化。在未来,我们将继续追踪新的技术趋势和业务需求,不断优化我们的系统,确保它们能够支撑起日益增长的业务挑战。

作者:京东零售 贾攀

来源:京东云开发者社区 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/312161.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Github搭建图床 github搭建静态资源库 免费CDN加速 github搭建图床使用 jsdelivr CDN免费加速访问

Github搭建图床 github搭建静态资源库 免费CDN加速 github搭建图床使用 jsdelivr CDN免费加速访问 前言1、创建仓库2、开启 gh-pages页面功能3、访问测试 前言 写博客文章时&#xff0c;图片的上传和存放是一个问题&#xff0c;使用小众第三方图床&#xff0c;怕不稳定和倒闭&…

RTL编码(1)——概述

一、RTL级描述 RTL&#xff08;Register Transfer Level&#xff09;级&#xff1a;寄存器&#xff0b;组合逻辑&#xff0c;其功能与时序用Verilog HDL&#xff08;以下简称Verilog&#xff09;或VHDL代码描述。 RTL描述包含了同步数字电路最重要的三个特征&#xff1a;组合逻…

24-1-9 bilibilic++音视频

下午两点面试&#xff0c;面试官迟到了一会&#xff0c;面试官人很好&#xff0c;整体面试经历很不错&#xff0c;但是我人太紧张了&#xff0c;基础知识掌握的深度不够&#xff0c;没有深挖&#xff0c; 是做音视频的底层相关的&#xff0c; 实习要求只要每天打卡够九个小时就…

FineBI实战项目一(15):订单销售总额分析开发

点击新建组件&#xff0c;创建订单销售总额组件。 选择自定义图表&#xff0c;选择文本&#xff0c;拖拽要分析的字段到文本中。 进入仪表板&#xff0c;拖拽刚刚的组件进入仪表板&#xff0c;然后在再编辑标题。 效果如下

MySQL-外键等信息

38. 基础-多表查询-概述_哔哩哔哩_bilibili 1、流程函数 2、约束字段 删除外键 &#xff1a; alter table emp2 drop foreign key 外键名 //外键可以保持数据的一致性和完整性&#xff0c;外键的话&#xff0c;就是类似一个主表&#xff0c;一个从表&#xff0c;从表的其中一…

uniapp微信小程序投票系统实战 (SpringBoot2+vue3.2+element plus ) -投票帖子详情实现

锋哥原创的uniapp微信小程序投票系统实战&#xff1a; uniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )_哔哩哔哩_bilibiliuniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )共计21条视频…

js中的class类

目录 class构造函数方法原型方法访问器方法静态方法 继承super minxin关于多态 class 在ES6中之前如果我们想实现类只能通过原型链和构造函数的形式&#xff0c;不仅难以理解步骤也十分繁琐 在ES6中推出了class关键字&#xff0c;它可以在js中定一个类&#xff0c;通过new来实…

Flink异步IO

本文讲解 Flink 用于访问外部数据存储的异步 I/O API。对于不熟悉异步或者事件驱动编程的用户,建议先储备一些关于 Future 和事件驱动编程的知识。 本文代码gitee地址: https://gitee.com/ddxygq/BigDataTechnical/blob/main/Flink/src/main/java/operator/AsyncIODemo.java …

ceph、gluster、longhorn选型对比

Ceph Ceph是一个分布式的存储系统&#xff0c;可以在统一的系统中提供唯一的对象、块和文件存储。 名词解释&#xff1a; RADOS&#xff1a; 由自我修复、自我管理、智能存储节点组成的可靠、自主、分布式对象存储LIBRADOS&#xff1a; 一个允许应用程序直接访问 RADO 的库&…

虚幻UE 材质-进阶边界混合之运行时虚拟纹理

之前在学习空山新雨后时对于边缘虚化过渡处理有很多技术 今天又要介绍一个边缘过渡的方法&#xff1a;运行时虚拟纹理 文章目录 前言一、运行时虚拟纹理二、使用步骤总结 前言 边缘过渡柔和的方式我们之前介绍了很多&#xff0c;但是效果也不是最好的。 像素偏移PDO和我们今天…

查准率与查全率在自然语言处理中的核心概念与联系、核心概念和实践应用,如何使用朴素贝叶斯、SVM 和深度学习实现查准率和查全率的计算?

查准率与查全率在自然语言处理中的核心概念与联系、核心概念和实践应用,如何使用朴素贝叶斯、SVM 和深度学习实现查准率和查全率的计算? 人工智能核心技术有:1. 深度学习;2.计算机视觉;3.自然语言处理;4.数据挖掘。其中,深度学习就是使用算法分析数据,从中学习并自动归…

jsonvue-mobile 联动方式说明。

目录 jsonvue-mobile的联动类型分为两种 一种是命令式的&#xff1a; 另一种是响应式的&#xff1a; 联动场景 场景一&#xff1a;某一个字段的值变化时&#xff0c;同步修改另一个字段的值 命令式&#xff1a; 响应式&#xff1a; 场景一演示效果GIF 场景二&#xff1…

对外贸易数据平台解析_外贸三大支撑力_箱讯科技

添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 三大支撑力支撑我国外贸持续回暖 海关总署近日发布数据显示&#xff0c;今年前11个月&#xff0c;我国进出口总值37.96万亿元&#xff0c;与去年同期持平。进入四季度&#xff0c;我国外贸发展的积极因素…

k8s--动态pvc和pv

前情回顾 存储卷&#xff1a; emptyDir 容器内部&#xff0c;随着pod销毁&#xff0c;emptyDir也会消失 不能做数据持久化 hostPath&#xff1a;持久化存储数据 可以和节点上目录做挂载。pod被销毁了数据还在 NFS&#xff1a;一台机器&#xff0c;提供pod内容器所有的挂载点…

css设置内嵌样式阴影

.box{box-shadow: inset 0 0 10px #f1e227, inset 0 0 10px #b6b70f, inset 0 0 10px #879017, inset 0 0 10px #485b22, inset 0 0 10px #0a272e;/* 这是一个CSS的box-shadow属性的值&#xff0c;用于创建一个元素的内部阴影效果。具体的意思是在元素的边界内部添加五个不同颜…

Java项目:119SpringBoot废品回收系统

博主主页&#xff1a;Java旅途 简介&#xff1a;分享计算机知识、学习路线、系统源码及教程 文末获取源码 一、项目介绍 废品回收系统是由SpringBootMybatis开发的&#xff0c;分为前台和后台&#xff0c;前台进行下单&#xff0c;后台处理。 后台功能如下&#xff1a; 类型…

源码搭建教学:连锁餐饮APP开发实战

连锁餐饮APP&#xff0c;对于很多从事餐饮行业的人来说不会陌生&#xff0c;同样这个项目本身就有着很高的热度。今天&#xff0c;小编将深入为大家讲述一下此系统的前后端开发、数据库设计、用户界面设计等方面&#xff0c;让您深入了解全栈开发的方方面面。 一、项目准备与规…

1.6计算机网络的性能指标

1.6计算机网络的性能指标 常用的计算机网络的性能指标有7个&#xff1a;速率、带宽、吞吐量、时延、往返时间、利用率、丢包率 1.6.1速率 计算机发送的信号是以二进制数字形式的。一个二进制数字就是一个比特(bit&#xff0c;binary digit)字节:Byte&#xff0c;1Byte8bit(1…

android APP修改为鸿蒙APP

将一个Android应用&#xff08;Android APP&#xff09;修改为鸿蒙应用&#xff08;HarmonyOS APP&#xff09;需要进行一些适配和重构工作&#xff0c;因为两者的底层架构和开发模型存在一些差异。下面是一些通用的步骤和考虑事项&#xff0c;希望对大家有所帮助。北京木奇移动…

C++进阶(三)多态

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、多态的概念1、概念 二、多态的定义及实现1、多态的构成条件2、虚函数3、虚函数的重写4、C…