跟我一起学开源设计第3节: 开源的服务端用户埋点SDK源码设计与实现分析

一、背景

在之前的分享中,我们通过引入用户行为分析Growing IO的客户端SDK,介绍了Spring Boot Starter的开发方法,同时也介绍了Spring Boot Starter中的常见的几项高级配置的玩法:

跟我一起学开源设计第1节:封装埋点GrowingIO Spring Boot Starter组件

跟我一起学开源设计第2节:SpringBoot Starter组件中的4项高级设计

本文来介绍该客户端SDK埋点的Java源码设计,希望可以借助这个源码分析和设计,可以让大家在日后的工作中,对于业务性的服务端的埋点上报之类的功能设计思想有所了解,能够自主的学会如何开发一个服务端的埋点上报的SDK组件,正如之前所说,这个东西其实不需要我们依赖服务端的运行,我们直接在客户端上面进行模拟数据上报这个操作即可。也可以自行登录到growingio网站上注册个试用账号。

二、基本概念

源码分析前,我们有必要了解下这种的业务知识与概念,有助于帮助我们统一语言与概念,通过如下文档,可以知道该SDK是如何使用的,建议先自己花费5分钟过一遍,同时思考下自己不明白和不理解的地方,同时也给自己提出几个问题。

docs.growingio.com/v3/develope...

在该网页文档中的代码使用案例可以知道,对于一个客户端SDK埋点的使用,需要提供一个服务器端的URL、项目ID、传输的队列大小和间隔、网络超时时间、日志等,同时需要再客户端构造出如下的变量数据:

(1)、事件时间:即一个行为操作上报或触发的时间点,需要告诉服务端,什么时候发生的。

(2)、事件标识:即一个业务场景的标识,比如创建订单事件、支付事件、用户注册事件、取消订单事件,可以为每种场景定义一个唯一标识:

(3)、用户ID:即触发的用户是谁,既然是用户行为分析,肯定是要把用户ID上报给服务端

(4)、事件级别变量:即当前业务场景的核心参数、核心数据,服务端可以根据该数据进行分组统计分析

SDK使用的示例代码如下:

java 复制代码
//事件行为消息体
GIOEventMessage eventMessage = new GIOEventMessage.Builder()
    .eventTime(System.currentTimeMillis())            // 事件时间,默认为系统时间(选填)
    .eventKey("BuyProduct")                           // 事件标识 (必填)
    .loginUserId("417abcabcabcbac")                   // 登录用户ID (必填)
    .addEventVariable("product_name", "苹果")          // 事件级变量 (选填)
    .addEventVariable("product_classify", "水果")      // 事件级变量 (选填)
    .addEventVariable("product_price", 14)            // 事件级变量 (选填)
    .build();
​
//上传事件行为消息到服务器
GrowingAPI.send(eventMessage);

请大家牢记如下4个内容,这是一个基本的用户行为分析埋点SDK的基本元素,如果然你去设计一个埋点SDK,这四个必不可少,对于学习其他家的埋点SDK源码也是如此。

梳理的一个图如下所示:

此时,我们最好思考几个问题,比如:

(1)、SDK是如何构造事件消息的

(2)、SDK是同步上报数据还是异步上报数据

(3)、SDK是如何使用高性能上报数据

(4)、SDK上报的数据格式是什么样的

(5)、SDK这个埋点上报过程中都做了哪些事

为什么要梳理问题呢,其实,这也是一种源码学习方法,在源码学习方法中,其中一个方法就是带着问题/目标去学习,在持续的梳理和跟进中,一步步的来解决自己的问题。

三、源码分析

3.1、源码下载与编译

1、首先在Github上面,找到该SDK的仓库,下载源码:

github.com/growingio/g...

2、然后再IDEA中,导入该项目,选择Maven的编译:

3、导入工程后,可以发现,其实该SDK的源码设计并不多,但是也是值得我们学习和思考的:

4、然后写个测试类,运行一下简单的结果,可以看到控制台输出了一个日志消息,同时对消息结构封装为了数组结构。

看到这里不知道大家是什么感觉,我这里个人感觉是,个人完全可以模拟一个服务端的接口,来接收这个数据,从而体验一下客户端数据埋点上报。

3.2、源码分析

源码分析,我们先从程序入口开始。

3.2.1、GIOEventMessage源码分析

1、事件消息的构造GIOEventMessage类,该类本质就是一消息数据结构,从源码中可以看到几个关键点:

(1)、定义了非常精简的HTTP事件请求参数名称:

(2)、定义了2个map存储基本事件参数和变量级别的事件参数

(3)、存储和更新变量数据:

3.2.2、GrowingAPI源码分析

接下来就是核心的消息时间发送方法: GrowingAPI.send(msg),先来看下GrowingAPI这个类都做了那些内容:

(1)、服务端初始化参数配置,并校验服务端的URL是否可用:

(2)、封装对外提供的发送方法,将消息传递给消息存储策略处理器中。

这样growingapi的分析就结束了。然后它在这里为我们引入了一个新的组件:StoreStrategyClient,也就是说当我们调用send方法发送的时候,SDK将一个个的消息,传递给了StoreStrategyClient类。

3.2.3、StoreStrategyClient源码分析

通过刚才的类可以知道当发送了消息数据,通过 StoreStrategyClient.getStoreInstance().push(msg);代码将消息传递给了消息存储策略处理器,我们接下来分析这个:

(1)、通过单例,提供了一个默认的存储策略

(2)、push消息的时候,父类的抽象逻辑,定义了一个数量的线程池,具体如何push交给子类实现:

(3)、当前工程中只有一个默认存储实现DefaultStoreStrategy,我们来分析一下他,这个也是整个SDK中最核心的设计,非常值得每个人学习,也是一种典型的生产消费模式的代码设计:

(4)、消息加入内存队列,在存储其中维护了这几个发送相关的参数:阻塞队列、阻塞队列的大小、发送间隔、发送器、批量发送数量等

接下来的方法是非常重要的,多多理解:

scss 复制代码
static {
        sendMsgSchedule.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                while (!queue.isEmpty()) {//队列不为空则执行
                    if (currentBatchMsgSize() < sendMsgBatchSize) {// 当前小于批量发送数量
                        GIOMessage gioMessage = queue.poll();// 从队列中取出来
                        if (gioMessage != null) {
                            String projectId = gioMessage.getProjectId();
                            if (batchMsgMap.containsKey(projectId)) {//通过项目ID区分批量消息
                                List<GIOMessage> list = batchMsgMap.get(projectId);//将消息放到某个项目标识下
                                list.add(gioMessage);
                            } else {
                                List<GIOMessage> list = new ArrayList<GIOMessage>();
                                list.add(gioMessage);//消息不存在滴1次增加一个
                                batchMsgMap.put(projectId, list);
                            }
                        }
                    } else {break;}// 大于批量阈值就终止循环,进入发送流程
                }
                // 队列为空时,取出批量消息数据结构中的数据,开始调用发送器批量发送
                for (Map.Entry<String, List<GIOMessage>> entry : batchMsgMap.entrySet()) {
                    if (entry.getValue() != null && !entry.getValue().isEmpty()) {
                        sender.sendMsg(entry.getKey(), entry.getValue());
                    }
                }
                batchMsgMap.clear();// 发送完清除数据
            }
        }, sendInterval, sendInterval, TimeUnit.MILLISECONDS);
    }

通过以上逻辑可以看到,这里采用了一个异步的定时任务线程池,每隔100ms检测一次,当前内存队列中是否存在消息,如果消息小于批量发送的数量就取出来,大于批量发送数量,就开始批量发送。如果说100ms产生了10条消息,则10条消息会整体发送。

3.2.4、消息发送器

通过上一步知道了,消息是每隔100ms异步传递给消息发送器的,然后我们接着分析,看到了发送器的实现,它又是一个线程池异步的调用网络进行发送。

看到它调用了网络发送器,网络发送的源码如下,这里就是纯HTTP发送了,然后也可以看到它发送了网络IO错误后也会进行重试处理。

这样一个消息从构建到发送,我们就已经分析完了,我们回顾一下消息的流程,如下图所示:

通过这8个步骤,可以非常清晰的看到它是如何将一个消息发送到服务端的。

四、可以学习到的知识点

通过本文我们可以学习到的知识点如下:

建造者设计模式、策略模式、模板方法模式、单例模式、生产者消费者模式、固定线程池/调度线程池、高性能埋点数据上报、批量数据异步发送上报、扩展点设计等。

大家是否清楚对于上面的知识点都是如何在源码中体现的吗?

可以多花费一点时间来品味一下哦。

本文这个SDK其实源码设计的非常不错了了,经常听到很多人说多线程这项技术无法再实际工作中使用,其实业务功能可能确实很少使用,但是提到高性能和中间件类型的工具、项目、SDK一般都会大量的使用,来完成异步、批量这种处理。

希望本篇文章可以帮助到大家,学习到一种客户端SDK的埋点的设计。

我相信看完了这个SDK设计,大家应该也可以自己手写一个客户端埋点的SDK工具包了,我相信这个对自己的设计、技术也有一定的提升的。如果未来公司的业务,是XXX上报类的,我相信这个SDK的精髓一定会帮助到你。本文没有讲述源码中关于压缩数据上报的内容,感兴趣的可以自己研究下。

五、总结

本文也是开源设计专题的第三篇文章,感兴趣的小伙伴,在阅读后,可以尝试基于该SDK进行下源码的深入理解和学习。

同时,本文也分享了一种源码学习方法,即:带着问题/目标去学习,针对XX问题,进行跟踪和分析。通过最近3篇的文章,希望大家可以具备了:

Spring Boot Starter的设计能力

埋点类的SDK设计能力

一种基于队列的高性能处理能力

异步/批量思想的能力。

六、练习

感兴趣的小伙伴,既然学习了,也不能白学习,可以尝试实现和完成如下内容:

(1)、抽取这个项目中的精髓,封装出自己的埋点SDK Client和埋点Server端,然后进行一些基本的数据展示和分析。

(2)、使用typora文档中提供的mermaid这个东西来梳理当前SDK的业务时序交互流程图

七、最后

本文也是作者首发于个人知识星球【觉醒的新世界程序员】中的一篇文章,也是在星球内正在分享的《开源设计系列专题》中的中的内容,如果想了解相关内容,可以来了解下,与我一同学习交流,星球内每周更新一篇总结文档。

非常期待与大家一起学习、进步,喜欢的老铁可以收藏、点赞、关注、分享哦。

欢迎在评论区一起交流哦。

相关推荐
.生产的驴4 分钟前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
huaqianzkh18 分钟前
了解华为云容器引擎(Cloud Container Engine)
云原生·架构·华为云
Code哈哈笑22 分钟前
【C++ 学习】多态的基础和原理(10)
java·c++·学习
chushiyunen27 分钟前
redisController工具类
java
A_cot33 分钟前
Redis 的三个并发问题及解决方案(面试题)
java·开发语言·数据库·redis·mybatis
Kika写代码37 分钟前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
刘某某.39 分钟前
使用OpenFeign在不同微服务之间传递用户信息时失败
java·微服务·架构
alden_ygq39 分钟前
GCP容器镜像仓库使用
java·开发语言
迪捷软件40 分钟前
知识|智能网联汽车多域电子电气架构会如何发展?
架构·汽车
zyhJhon41 分钟前
软考架构-层次架构风格
架构