【案例实战】业务稳定性运行之全链路混合压测

2024-01-09 20:44

本文主要是介绍【案例实战】业务稳定性运行之全链路混合压测,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1.全链路压测开展步骤

(1)什么是全链路压测

全链路压测是指基于真实业务场景,通过模拟海量的用户请求,对整个后台服务进行压力测试,从而评估整个系统的性能水平。

在这里插入图片描述

  • 对应用程序的整个技术栈进行完整的压力和性能测试,覆盖了整个应用程序的各个部分,包括前端、后端、服务、数据库等。
  • 模拟真实用户交互和**系统环境(生产环境)**的情况下,评估整个应用在高负载情况下的性能表现和持续调优的过程,包括响应时间、吞吐量、资源利用率以及系统的稳定性和可扩展性等。
  • 全链路压测可以帮助发现系统的性能瓶颈和故障点,并对整个系统的性能进行全面评估和优化。

(2)全链路压测的实施步骤

  • 确定压测场景
    • 根据实际使用情况和业务需求,确定要测试的压测场景,包括用户访问路径、操作流程和数据交互等。
    • 每个场景包括一系列用户行为和操作流程,涉及到多个应用程序的不同组件。
    • 区分核心业务和非核心业务,主要精力和压测都是对核心业务进行
  • 流量数据模型准备
    • 根据压测场景,构建模拟用户行为的测试脚本。
    • 为模拟用户行为提供必要的测试数据,可以使用真实或合成的数据进行测试,以确保测试的真实性和覆盖性。
    • 做好数据隔离:不能污染正常的业务数据,整理好每个环节的数据流转。
  • 选择工具配置并发
    • 选择合适的流量录制和回放工具,根据实际情况和性能需求,配置并发用户数、请求频率和负载模型等。
    • 可以逐步增加并发负载,以模拟系统在峰值和极限负载下的性能表现。
  • 开始运行压测
    • 运行测试脚本,模拟并发用户对系统进行访问和交互。
    • 观察和记录系统在不同负载情况下的性能指标,例如响应时间、吞吐量、CPU和内存利用率等。
  • 数据分析和优化
    • 分析压测结果,并识别可能的性能问题和瓶颈点。
    • 可以根据分析结果进行调整和优化,例如调整配置参数、进行代码优化或增加系统资源等。
  • 结果评估和报告
    • 根据压测结果进行结果评估,并生成详细的测试报告,报告应包括系统的性能指标、发现的问题和建议的优化方案。
  • 压测现场还原
    • 对相关压测数据进行清理,比如缓存,队列,数据库等,避免长期占用相关资源,定时维护好相关测试数据。

(3)全链路压测实施关键技术难点

  • 流量数据模型:压测数据不真实,达不到生产环境的流量模型,最终导致压测不准确,比如只是拍脑袋想的用户访问路径。
  • 业务探针性能:全链路压测涉及到各个方法调用链的监听,会有数据采集上报,离不开APM调用链路追逐工具,需要降低本身的损耗。
  • 流量染色:流量请求和访问链路长,涉及业务多,需要多个系统代码改造,减少压测代码侵入性。
  • 数据隔离:避免影响线上正式用户,比如数据库,MQ,缓存,外部三方服务等。
  • 日志隔离:业务日志输出到文件或者到队列,避免压测数据采集后加入到正式的报表里面,导致运营和产品分析数据不准确。
  • 压测时间:一般在业务访问低峰进行,比如国外的话就是半夜2~6点,国外业务就根据时差进行调整。

2.数据模型准备之流量回放

(1)什么是流量回放

流量回放就是通过记录线上流量,在开发或者测试环境回放,来发现系统是否能够正常运行,降低代码变动整体系统带来的风险。

(2)流量回放工具GoReplay

  • 官网:https://goreplay.org/

  • github:https://github.com/buger/goreplay

  • GoReplay是GO语言编写的http流量复制工具,使用流程简单,支持多个系统,mac、linux、win。

  • GoReplay 不是代理,而是在后台侦听网络接口上的流量,无需更改生产基础架构,只需在与服务相同的机器上运行 GoReplay 守护程序。

在这里插入图片描述

  • 流量录制重放特点
    • 捕获网络指定端口流量,输出到控制台
    • 捕获网络指定端口流量,将原始流量实时重放到其他环境中
    • 捕获网络指定端口流量,并保存到文件中
    • 捕获网络指定端口流量,请求过滤指定路径流量,并保存到文件中

(3)Linux服务器安装Go环境和GoReplay

  • Go语言包下载地址:https://studygolang.com/dl
# 下载之后解压
tar -C /usr/local -zxvf go1.5.3.linux-amd64.tar.gz
  • 配置go语言环境变量
# 打开文件
vim /etc/profile
# 添加环境变量
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
# 编译生效
source /etc/profile
# 测试
go version
# go version go1.21.5 linux/386
  • GoReplay下载二进制包地址:https://github.com/buger/gor/releases
  • 下载1.3.1版本,下载之后解压
tar xvzf gor_1.3.1_x64.tar.gz 
  • 解压完压缩包后,可以从当前目录进行Gor,也可以将Gor文件复制到的PATH文件下
  • ./gor 为执行文件

(4)使用方式

  • 参数介绍
输入
--input-raw : 用于捕获 HTTP 流量时,应指定 IP 地址或界面以及应用程序端口
--input-file :接收以前使用过的文件记录
--input-tcp :如果决定将多个转发器Gor实例转发流量到它,Gor聚合实例使用
输出
--output-http :重播HTTP流量到给定的端点
--output-file :记录传入到文件的流量
--output-tcp :将传入的数据转发到另一个Gor实例
--output-stdout :用于调试,输出所有数据。
  • 监听本地服务端口 8082 流量保存到本地的文件
./gor --input-raw :8082 --output-file=requests.gor
  • 将流量从文件回放到其他服务
./gor --input-file requests.gor --output-http="http://ip:8082"
  • 将 http 的请求打印到终端
./gor --input-raw :8082 --output-stdout
  • 将 http 的请求实时转发到其他服务
./gor --input-raw :8082 --output-http="http://ip:8082"
  • 将流量放大或减少转发给其他服务端口
./gor --input-file "requests.gor|200%" --output-http="http://ip:8082"
./gor --input-file "requests.gor|20%" --output-http="http://ip:8082"
  • 过滤指定请求
./gor --input-raw :80 --http-allow-method GET --output-http http://target_server:8080
./gor --input-raw :8080 --output-http staging.com --http-allow-url /api
  • 只收集请求头中符合 api-version 为 1.0x 的请求
./gor --input-raw :8080 --output-http staging.com --http-allow-headerapi-version:^1\.0\d
  • 对指定的header或url请求进行限流或者加速
./gor --input-tcp :28020 --output-http"http://staging.com|10"# (每秒请求数限制10个以内)
./gor --input-raw :80 --output-tcp"replay.local:28020|10%" # (每秒请求数限制10%以内)
./gor --input-raw :80 --output-tcp"replay.local:28020|10%" --http-header-limiter "X-API-KEY:10%"
./gor --input-raw :80 --output-tcp"replay.local:28020|10%" --http-param-limiter "api_key:10%"
  • 将流量转发到多个站点
./gor --input-raw :80 --output-http "http://target_server:8080"--output-http "http://target_server2:8080"
  • 将相同的流量发送到多个站点,并且平分所有流量
./gor --input-raw :80 --output-http "http://staging.com" --output-http "http://dev.com"--split-output true

3.全链路压测之流量染色

(1)什么是流量染色

流量染色就是让压测流量可以被程序代码识别,方便做好数据隔离。对压测的请求增加特色的流量标识,比如请求里面增加url参数或header增加请求头。区分压测流量和真实流量,正常用户不会访问到压测数据,压测数据不会影响正式业务。染色后的压测流量,产生的数据可以再压测结束后直接清理。

(2)流量链路改造

在这里插入图片描述

  • 数据库隔离

    • 压测产生的数据需要和真实数据库的进行隔离,一般采用数据库的影子库、影子表进行隔离。

    • 具体来说,影子库是生产环境数据库的一份完整拷贝,包含与生产环境相同的表结构和数据。

    • 影子表是在影子库中创建的与生产环境表相对应的测试表,压测的数据进入影子表。

    • 生产和压测环境的隔离,通过在压测环境中使用影子库和影子表,可以避免对生产环境数据的直接修改和干扰。

    • 完整的数据环境,通过生成影子库和影子表的完整拷贝,全链路压测的时候可以在准确、真实的数据环境中进行工作。

    • 线上问题还原,当线上出现问题时,可以使用影子库和影子表进行问题还原和分析,在相同数据环境中重现问题。

  • 消息队列隔离

    • 业务产生消息到MQ后,消费者会进行消费,压测过程产生的数据不能直接投递到MQ中。

    • 一般是采用队列隔离或者消息隔离,隔离策略也是基于消息的生产者封装方法进行投递。

    • 队列隔离:创建不同的消息队列,压测的队列和正式的队列采用不同的前缀进行区分。

    • 消息隔离:消息里面增加参数,标记消息是否是压测还是正式的数据。

  • 缓存隔离

    • 缓存里面的数据隔离,对key进行区分,根据流量标识是否是压测流量,增加相关的key前缀标识。

    • 不直接操作redis,而是封装redis工具类,在工具类里面判断是否是压测流量,里面对key的读写进行操作。

(3)流量标识透传

流量标识透传是一种将请求上下文信息从发起端(如客户端)传递到目标端(如后端服务)的方案。可以在测试过程中追踪和识别请求的来源,并对不同的请求进行分类和分析。压测流量全部带标识,结合拦截器,存储在ThreadLocal里面进行不同服务直接传递。

(4)流量标识透传方案

  • HTTP Header:将请求上下文信息添加到HTTP请求的Header中。常用的Header字段包括:
    • X-Request-ID:请求标识ID,用于唯一标识每个请求。
    • X-Trace-ID:链路追踪ID,用于追踪请求在分布式系统中的路径。
    • X-Forwarded-For:客户端真实IP地址,用于透传客户端IP。
    • 其他自定义的Header字段用于传递其他请求上下文信息。

(5)跨服务器之间流量传递

在这里插入图片描述

  • 微服务里面采用了Fegin进行RPC调用,在发送请求前可以从ThreadLocal里面的进行获取相关标识符进行
  • Dubbo/GRPC都类似,将请求上下文信息绑定到线程上下文,以实现在不同服务间的透传

4.全链路混合压测环境搭建

(1)阿里云docker部署mysql

docker run -d -p 3306:3306 --name mysql --privileged=true -v /data/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysql8test. mysql:8.0.23
  • 新建数据库,一个正式库,一个影子库(shadow)。

在这里插入图片描述

  • 订单库 order、shadow_order
CREATE TABLE `product_order` (`id` bigint NOT NULL AUTO_INCREMENT,`product_id` bigint DEFAULT NULL,`product_title` varchar(255) DEFAULT NULL,`amount` int DEFAULT NULL,`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  • 商品库 product、shadow_product
CREATE TABLE `product` (`id` bigint NOT NULL AUTO_INCREMENT,`title` varchar(255) DEFAULT NULL,`stock` int DEFAULT NULL,`amount` int DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 分别在两个库中加入数据
INSERT INTO `product` (`id`, `title`, `stock`, `amount`) VALUES (1, '名称', 200, 10);

在这里插入图片描述

(2)阿里云docker部署RabbitMQ

docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:3.8.9-management

在这里插入图片描述

(3)阿里云docker部署Redis

docker run -itd --name redis -p 6379:6379 -v /mydata/redis/data:/data redis:7.0.8 --requirepass 123456

在这里插入图片描述

(4)阿里云docker部署Nacos

docker run -d -e MODE=standalone -e JVM_XMS=128m -e JVM_XMX=128m -e JVM_XMN=128m -p 8848:8848 -p 9848:9848 --restart=always --privileged=true --name nacos nacos/nacos-server:v2.2.3

在这里插入图片描述

5.全链路混合压测项目搭建

(1)需求模块划分

  • 微服务业务划分:商品服务、订单服务

在这里插入图片描述

(2)业务逻辑

在这里插入图片描述

在这里插入图片描述

  • 我们就以蓝色的用户行为链路进行测试。

(3)流量染色和RPC透传

  • 压测流量全部带标识,结合拦截器,存储在ThreadLocal里面进行不同服务直接传递。
/*** @author lixiang* @date 2024/1/6 15:50*/
@Slf4j
public class RequestInterceptor implements HandlerInterceptor {public static TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response,@NonNull Object handler) throws Exception {//前端在header中传入test_flag标识来区分是否为正式流量或者压测流量String testFlag = request.getHeader("test_flag");if(StringUtils.isNotBlank(testFlag)){//通过threadLocal传递信息log.info("压测流量,path = {}",request.getRequestURI());threadLocal.set(1);}else{log.info("正式流量,path = {}",request.getRequestURI());threadLocal.set(0);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)  {threadLocal.remove();}
}
  • RPC调用feign透传参数
/*** @author lixiang* @date 2024/1/6 15:50*/
@Configuration
public class FeignConfig implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert attributes != null;HttpServletRequest request = attributes.getRequest();Enumeration<String> headerNames = request.getHeaderNames();if (headerNames != null) {while (headerNames.hasMoreElements()) {String name = headerNames.nextElement();String values = request.getHeader(name);requestTemplate.header(name, values);}}}
}

(4)缓存隔离

  • 缓存里面的数据隔离,对key进行区分,根据流量标识是否是压测流量,增加相关的key前缀标识。
    /*** 构建 key,用于区分压测和正式流量* @param key* @return*/private String buildFinalKey(String key){Integer testFlag =  RequestInterceptor.threadLocal.get();String finalKey = "";//压测流量if(testFlag !=null && testFlag ==1){finalKey = "shadow:"+key;}else {finalKey = key;}return finalKey;}

(5)消息队列隔离

  • 采用队列隔离或者消息隔离,隔离策略也是基于消息的生产者封装方法进行投递

在这里插入图片描述

		/*** 构建 routingKey的时候,用于区分压测和正式流量* @param routingKey* @return*/private String buildFinalRoutingKey(String routingKey){Integer testFlag =  RequestInterceptor.threadLocal.get();String finalKey = "";//压测流量if(testFlag !=null && testFlag ==1){finalKey = "SHADOW."+routingKey;}else {finalKey = routingKey;}return finalKey;}
  • 监听器这块用于区分不同的队列
@Slf4j
@Component
public class OrderMQListener {@RabbitListener(queuesToDeclare = { @Queue("ORDER_QUEUE") })public void orderQueue(ProductOrderDO productOrderDO, Message message, Channel channel) throws IOException {log.info("监听到正式消息:{}",message);long msgTag = message.getMessageProperties().getDeliveryTag();handleOrderMsg(productOrderDO);channel.basicAck(msgTag,false);}@RabbitListener(queuesToDeclare = { @Queue("SHADOW_ORDER_QUEUE") })public void shadowOrderQueue( ProductOrderDO productOrderDO,Message message, Channel channel) throws IOException {log.info("监听到影子消息:{}",message);long msgTag = message.getMessageProperties().getDeliveryTag();handleOrderMsg(productOrderDO);channel.basicAck(msgTag,false);}private void handleOrderMsg(ProductOrderDO productOrderDO){String type = "1".equals(productOrderDO.getType())?"正式逻辑":"压测逻辑";log.info("{}-处理订单消息",type);}
}

(6)数据源隔离

在这里插入图片描述

  • 双数据源配置类
/*** @author lixiang* @date 2024/1/6 15:50*/
@Configuration
public class DynamicDataSourceConfig {public static final String MASTER = "MASTER";public static final String SHADOW = "SHADOW";@Bean("masterDataSourceProperties")@ConfigurationProperties("spring.datasource.master")public DataSourceProperties masterDataSourceProperties() {return new DataSourceProperties();}@Bean("masterDataSource")@ConfigurationProperties(prefix = "spring.datasource.master.hikari")public HikariDataSource primaryDataSource() {return masterDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();}@Bean("shadowDataSourceProperties")@ConfigurationProperties("spring.datasource.shadow")public DataSourceProperties shadowDataSourceProperties() {return new DataSourceProperties();}@Bean("shadowDataSource")@ConfigurationProperties(prefix = "spring.datasource.shadow.hikari")public HikariDataSource secondaryDataSource() {return shadowDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();}}
  • 数据源路由配置
/*** @author lixiang* @date 2024/1/6 15:50*/
public class DynamicDataSource extends AbstractRoutingDataSource {private static final TransmittableThreadLocal<String> dataSourceContextHolder = new TransmittableThreadLocal<>();/*** 配置DataSource, defaultDataSource为主数据库*/public DynamicDataSource(DataSource defaultDataSource, Map<Object,Object> targetDataSourceMap) {super.setDefaultTargetDataSource(defaultDataSource);super.setTargetDataSources(targetDataSourceMap);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return getDataSource();}public static void setDataSource(String dataSource) {dataSourceContextHolder.set(dataSource);}public static String getDataSource() {return dataSourceContextHolder.get();}public static void clearDataSource() {dataSourceContextHolder.remove();}
}
  • 数据源切面区分正式库和测试库
@Slf4j
@Aspect
@Component
public class DataSourceAspect {@Pointcut("execution(public * com.lixiang.controller..*.*(..))")public void controllerPointcut() {}@Before(value = "controllerPointcut()")public void methodBefore(JoinPoint joinPoint) {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert requestAttributes != null;HttpServletRequest request = requestAttributes.getRequest();// 获取请求头String testFlag = request.getHeader("test_flag");// 通过 testFlag 判断if (StringUtils.isNotBlank(testFlag)) {System.out.println("压测流量,影子库,path = "+request.getRequestURI());DynamicDataSource.setDataSource(DynamicDataSourceConfig.SHADOW);} else {System.out.println("正式流量,正式库,path = "+request.getRequestURI());DynamicDataSource.setDataSource(DynamicDataSourceConfig.MASTER);}}
}
  • 启动主类配置数据源路由
    /*** 创建多个数据源对象* @param masterDataSource* @param shadowDataSource* @return*/@Bean@Primarypublic DynamicDataSource dataSource(DataSource masterDataSource, DataSource shadowDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("SHADOW", shadowDataSource);targetDataSources.put("MASTER", masterDataSource);return new DynamicDataSource(masterDataSource, targetDataSources);}

6.Jmeter全链路混合压测

项目的代码我会传到资源文件中哦,大家记得去找【案例实战】业务稳定性运行之全链路混合压测这个标题的资源文件。

(1)启动项目,查看服务情况

在这里插入图片描述

(2)接口准备

ip:8082/api/product/v1/findById 查看商品详情
ip:8082/api/product/v1/list 查看商品列表
ip:8082/api/product/v1/lock 扣件商品库存
ip:8081/api/order/v1/add 下单

在这里插入图片描述

  • 我们以这个链路进行测试。

(3)配置jmeter

  • 配置100个用户,持续60s

在这里插入图片描述

  • 配置请求头,添加压测标识

在这里插入图片描述

  • 配置用户变量

在这里插入图片描述

  • 添加压测接口

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 查看报告

在这里插入图片描述

在这里插入图片描述

OK,至此全链路压测就已经完成啦,大家可以根据公司自己的业务去实施。记得给博主三连哦!

在这里插入图片描述

这篇关于【案例实战】业务稳定性运行之全链路混合压测的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/588377

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

客户案例:安全海外中继助力知名家电企业化解海外通邮困境

1、客户背景 广东格兰仕集团有限公司(以下简称“格兰仕”),成立于1978年,是中国家电行业的领军企业之一。作为全球最大的微波炉生产基地,格兰仕拥有多项国际领先的家电制造技术,连续多年位列中国家电出口前列。格兰仕不仅注重业务的全球拓展,更重视业务流程的高效与顺畅,以确保在国际舞台上的竞争力。 2、需求痛点 随着格兰仕全球化战略的深入实施,其海外业务快速增长,电子邮件成为了关键的沟通工具。

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10