线索系统性能优化实践

news2024/11/18 15:21:50

引言

在京东家居事业部,线索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以下):

总结与展望

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

作者:京东零售 贾攀

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DDMS修改默认端口8700

当前操作系统为 Mac&#xff0c;编辑器 Intellij IDE 准备工作&#xff1a;在IDE中的终端下&#xff0c;输入monitor指令 > 回车&#xff0c;打开DDMS 1、第一步&#xff0c; 2、第二步&#xff1a;修改静态端口 3、第三步 4、第四步&#xff1a;修复配置端口 5、第五步 重…

高级路由学习试题

文章目录 高级路由学习试题一.高级路由题目答案 二.OSPF 相关答案 三.基础知识答案 高级路由学习试题 一.高级路由题目 1.以下属于ITOIP特性的有&#xff08;&#xff09; A、智能 B、开放 C、融合 D、标准 2.层级化网络模型将网络划分为&#xff08;&#xff09; A、汇…

[Kubernetes]9. K8s ingress讲解借助ingress配置http,https访问k8s集群应用

前面讲解了使用Helm部署mysql集群,这里来看看使用Ingress搭建负载均衡功能 1.介绍 功能类似 Nginx ,可以根据域名、路径把请求转发到不同的 Service , Ingress 为外部访问集群提供了一个 统一 入口, 避免 了 对外暴露集群端口 ,可以配置 https,http访问集群应用,接下来看看如…

实现STM32烧写程序-(1)获取Bootloader版本信息

简介 如何像ST Flash Loader等工具一样写自己的烧写程序呢?文档 AN3155: USART protocol used in the STM32 bootloader 步骤 Boot模式 将 开发板例如STM32F103C8T6 Boot0->1 & Boo1->0 重启或复位进入系统存储模式 物理连接 将USART1 通过 USB转TTL线连接到…

大模型学习与实践笔记(五)

一、环境配置 1. huggingface 镜像下载 sentence-transformers 开源词向量模型 import os# 设置环境变量 os.environ[HF_ENDPOINT] https://hf-mirror.com# 下载模型 os.system(huggingface-cli download --resume-download sentence-transformers/paraphrase-multilingual-…

一卡通水控电控开发踩过的坑

最近在做一个项目&#xff0c;是对接一卡通设备的。我一开始只拿到设备和3个文档开局。不知道从哪下手。一步一步踩坑过来。踩了很多没有必要的坑&#xff0c;写出来给有用的人吧。 读卡器怎么用&#xff1f; 有个读卡器&#xff0c;一开始什么软件也不提供。我都不知道是干嘛…

Spring Boot - Application Events 的发布顺序_ApplicationContextInitializedEvent

文章目录 Pre概述Code源码分析 Pre Spring Boot - Application Events 的发布顺序_ApplicationEnvironmentPreparedEvent Spring Boot - Application Events 的发布顺序_ApplicationEnvironmentPreparedEvent 概述 Spring Boot 的广播机制是基于观察者模式实现的&#xff0c…

NLP论文阅读记录 - 05 | 2023 抽象总结与提取总结:实验回顾

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.相关工作2.1 提取方法2.2 抽象方法2.3 数据集 三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结思考 前言 Abstractive vs. Extractiv…

从头安装与使用一个docker GPU环境

GPU版docker的安装与使用 欢迎使用GPU版docker安装使用说明使用官方教程安装docker新建一个GPU版docker环境调用docker环境执行本地python文件 欢迎使用GPU版docker安装使用说明 使用官方教程安装docker 导入源仓库的GPG key curl -fsSL https://download.docker.com/linux/…

RTL编码(1)——概述

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

水产冷链物流行业零下25℃库架一体 海格里斯HEGERLS四向穿梭式冷藏冷库智能密集仓

随着国内外仓储物流整体规模和低温产品消费需求的稳步增长&#xff0c;冷链市场应用潜力不断释放。在传统“货架叉车”的方式下&#xff0c;货物、人员及机械设备不断进出&#xff0c;容易造成温度波动&#xff0c;导致冷量流失。立体冷库则以更高密度、更具成本效益的方式&…

electron+vue网页直接播放RTSP视频流?

目前大部分摄像头都支持RTSP协议&#xff0c;但是在浏览器限制&#xff0c;最新版的浏览器都不能直接播放RTSP协议&#xff0c;Electron 桌面应用是基于 Chromium 内核的&#xff0c;所以也不能直接播放RTSP&#xff0c;但是我们又有这个需求怎么办呢&#xff1f; 市场上的方案…

【动态规划】 【字典树】C++算法:472 连接词

作者推荐 【动态规划】458:可怜的小猪 涉及知识点 动态规划 字典树 LeetCode472 连接词 给你一个 不含重复 单词的字符串数组 words &#xff0c;请你找出并返回 words 中的所有 连接词 。 连接词 定义为&#xff1a;一个完全由给定数组中的至少两个较短单词&#xff08;不…

jenkins 自由风格部署vue项目,参数化构建vue项目

1. 丢弃旧的构建 2. 是否需要install 3. git 4. 配置node16: 5. 脚本&#xff1a; 脚本&#xff1a; #进入Jenkins工作空间下项目目录 cd /var/lib/jenkins/workspace/你的任务名称 node -v #检测node版本&#xff08;此条命令非必要&#xff09; npm -v #检测npm版本&#x…

【开源】基于JAVA语言的康复中心管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 普通用户模块2.2 护工模块2.3 管理员模块 三、系统展示四、核心代码4.1 查询康复护理4.2 新增康复训练4.3 查询房间4.4 查询来访4.5 新增用药 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的康复中…

Linux ----冯诺依曼体系结构与操作系统

目录 前言 一、冯诺依曼体系结构 二、为什么选择冯诺依曼体系结构&#xff1f; 三、使用冯诺依曼结构解释问题 问题1&#xff1a; 问题2: 四、操作系统 1.操作系统是什么 2.为什么需要操作系统 3.操作系统怎样管理的 4.如何给用户提供良好环境 五、我们是怎样调用系…

imgaug库指南(18):从入门到精通的【图像增强】之旅

引言 在深度学习和计算机视觉的世界里&#xff0c;数据是模型训练的基石&#xff0c;其质量与数量直接影响着模型的性能。然而&#xff0c;获取大量高质量的标注数据往往需要耗费大量的时间和资源。正因如此&#xff0c;数据增强技术应运而生&#xff0c;成为了解决这一问题的…

CodeQL基本使用

0x01 安装codeql 去github下载一个对应版本的codeql捆绑包。 https://github.com/github/codeql-action/releases 然后解压&#xff0c;这里我是解压到桌面 然后用添加到环境变量中 然后在任意位置输入codeql命令&#xff0c;如果能有以下提示就表示安装成功 然后下载vscode…

戴尔服务器有8条内存条,开机有一条内存条自检提示出错,可以不用管他吗,有影响吗?

环境 戴尔R730 问题描述 戴尔服务器有8条内存条&#xff0c;开机有一条内存条自检提示出错&#xff0c;可以不用管他吗&#xff0c;有影响吗&#xff1f; 提示B1内存有问题 解决方案 不能&#xff0c;有影响&#xff0c;安装系统时卡住在启动节目无法正常安装&#xff0c;…

mysql原理--redo日志1

1.redo日志是个啥 我们知道 InnoDB 存储引擎是以页为单位来管理存储空间的&#xff0c;我们进行的增删改查操作其实本质上都是在访问页面&#xff08;包括读页面、写页面、创建新页面等操作&#xff09;。我们前边唠叨 Buffer Pool 的时候说过&#xff0c;在真正访问页面之前&a…