A7.Sysbench和Spring JdbcTemplate 插入性能测试实践
前言
这是一个基于Java 21 的 六边形架构与领域驱动设计的一个通用项目,并且结合现有的最新版本技术架构实现了 领域驱动设计模式和六边形架构模式组件定义. 并且结合微服务,介绍了领域层,领域事件,资源库,分布式锁,序列化,安全认证,日志等,并提供了实现功能. 并且我会以日常发布文章和更新代码的形式来完善它.
简介
该篇来源于在一次测试性能场景, 发现生产数据库在插入时,有其中一张表的插入时,使用了8个线程, 每个线程插入1千条数据,每条数据大小为4KB,每个线程需要花费5秒中。没有达到预期。故产生了对数据库插入测试的想法.
本篇首先会使用Sysbench对数据库进行压测, 然后再使用实际场景的Spring JdbcTemplate对不同个数的字段,不同大小的数据进行测试。 但整个测试过程在本地环境,只是给出对测试的一种实践过程。不具备测试结果的参考性,所以在本文没有给出本机的硬件资源参数。
Sysbench数据库压测实践
Sysbench 是一个模块化的、跨平台的多线程基准测试工具,主要用于评估数据库和系统的性能。
我们可以使用Sysbench测试CPU的性能.例如将计算从 1 到 20000 之间的所有质数。
sysbench --time=60 --threads=8 --report-interval=3 cpu --cpu-max-prime=20000 run
--time: 指定测试时常 --threads: 指定线程数量 --report-interval: 指定报告时间间隔 (长时间测试,可以设置中间结果报告) --cpu-max-prime: 指定最大质数
比如还可以测试内存情况.
sysbench --time=60 --threads=8 --events=10000000 memory --memory-block-size=8k --memory-total-size=100T --memory-access-mode=seq run
--events: 指定事件数量 --memory-block-size: 指定内存块大小 --memory-total-size: 指定总内存大小 --memory-access-mode: 访问模式,seq:顺序访问,rnd:随机访问
然后,我们测试Mysql 的插入性能。 分3步:准备数据,压测,清理数据。
准备数据
sysbench /usr/share/sysbench/oltp_insert.lua --mysql-host=172.23.112.1 --mysql-port=3306 --mysql-user=root --mysql-password=123456 --mysql-db=test-db --db-driver=mysql --tables=15 --table-size=1000000 --report-interval=10 --threads=128 --time=120 prepare
--db-driver: 指定数据库类型,mysql, pgsql, oracle, mssql, mysql_async, oci8, sqlite3, ibmdb2, firebird, ibmdb2_async, odbc, mysql_innodb_proxy, mysql_sandbox, mysql_cluster, mysql_xdevapi, mysql_cluster_async, mysql_aurora, mysql_aurora_async --mysql-host: 指定mysql地址 --mysql-port: 指定mysql端口 --mysql-user: 指定mysql用户名 --mysql-password: 指定mysql密码 --mysql-db: 指定mysql数据库 --table_size: 指定表大小 (行数) --tables: 指定表数量 --threads: 指定线程数量 --events: 指定事件数量 --time: 指定测试时常 --rand-type: 指定随机数生成算法,uniform:均匀分布,gaussian:高斯分布,pareto:pareto分布,seq:顺序 --oltp: 指定测试类型,read_only:只读,simple:简单,complex:复杂,nontrx:非事务
压测
sysbench /usr/share/sysbench/oltp_insert.lua --mysql-host=172.23.112.1 --mysql-port=3306 --mysql-user=root --mysql-password=123456 --mysql-db=test-db --db-driver=mysql --tables=15 --table-size=1000000 --report-interval=10 --threads=128 --time=120 run
SQL statistics: queries performed: read: 0 // 执行读请求的总数,如SELECT操作 write: 585345 // 执行写请求的总数,如INSERT操作 other: 0 // 其他请求的 total: 585345 // 执行所有请求操作的总数 transactions: 585345 (4873.56 per sec.) // queries: 585345 (4873.56 per sec.) ignored errors: 0 (0.00 per sec.) reconnects: 0 (0.00 per sec.) General statistics: total time: 120.1050s // 总耗时 total number of events: 585345 // Latency (ms): min: 2.70 // 最小耗时 avg: 26.25 // 平均耗时 max: 1046.70 // 最大耗时 95th percentile: 44.17 // 表示95%的event耗时在这个时间完成 sum: 15363260.73 // 总耗时和 Threads fairness: events (avg/stddev): 4573.0078/15.55 //前者表示平均每个线程完成的event数量,后者表示标准差 execution time (avg/stddev): 120.0255/0.01 // 前者表示平均每个线程执行的时间,后者表示标准差
名词解释:
TPS: 每秒处理事务数 QPS: 每秒处理查询数
Spring JdbcTemplate 实践
JdbcTemplate 这里我只是添加了对insert 语句的测试。
package com.iokays.sample;
import com.google.common.collect.Lists;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class SampleJdbcService {
private final JdbcTemplate jdbcTemplate;
public SampleJdbcService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void insert(final String table, final List<String> content, final int batchSize) {
final var sql = "INSERT INTO " + table + " (CONTENT) VALUES (?)";
jdbcTemplate.batchUpdate(sql, content, batchSize, (ps, c) -> ps.setString(1, c));
}
public void multiInsert(final String table, final List<String> content, final int batchSize) {
final var sql = "INSERT INTO " + table + " (CONTENT) VALUES ";
Lists.partition(content, batchSize).forEach(v -> {
jdbcTemplate.update(sql + v.stream().map(v1 -> "('" + v1 + "')").collect(Collectors.joining(",")));
});
}
}
然后添加了一些测试方法
package com.iokays.sample;
import com.google.common.collect.Lists;
import jakarta.annotation.Resource;
import net.datafaker.Faker;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import java.util.List;
import java.util.concurrent.Executors;
/**
* 本次采
*/
@Rollback(value = false)
@SpringBootTest
class SampleJdbcServiceTest {
private static final Logger log = LoggerFactory.getLogger(SampleJdbcServiceTest.class);
@Resource
private SampleJdbcService service;
/**
* 生成指定大小的文本
*
* @param byteSize
* @param listSize
* @return
*/
private static List<String> create(final int byteSize, final int listSize) {
final var faker = new Faker();
final List<String> result = Lists.newArrayList();
while (result.size() <= listSize) {
StringBuilder content = new StringBuilder();
while (content.length() < byteSize) {
// 使用Faker生成随机文本
String sentence = faker.lorem().sentence();
content.append(sentence).append(" ");
}
result.add(content.toString());
}
log.info("数据已准备");
return result;
}
public static void main(String[] args) {
System.out.println(create(4000, 1));
}
/**
* mysql, 10w, 1tc, 26s
*/
@Test
void testInsert() {
final var content = create(4000, 10 * 10000);
final var now = System.currentTimeMillis();
service.insert("t_content", content, 1000);
log.info("耗时: {}", System.currentTimeMillis() - now);
}
@Test
void testMultiInsert() {
final var content = create(4000, 10 * 10000);
final var now = System.currentTimeMillis();
service.multiInsert("t_content", content, 1000);
log.info("耗时: {}", System.currentTimeMillis() - now);
}
/**
* 测试 Stream 并行处理
* mysql, 10w, 1tc, 49s
*/
@Test
void testParallelInsert() {
final var content = create(4000, 10 * 10000);
final var now = System.currentTimeMillis();
Lists.partition(content, 8).stream().parallel().forEach(v -> {
service.insert("t_content", v, 1000);
});
log.info("耗时: {}", System.currentTimeMillis() - now);
}
/**
* mysql, 10w, 1tc, 46s
*/
@Test
void testParallelMultiInsert() {
final var content = create(4000, 10 * 10000);
final var now = System.currentTimeMillis();
Lists.partition(content, 8).stream().parallel().forEach(v -> {
service.multiInsert("t_content", v, 1000);
});
log.info("耗时: {}", System.currentTimeMillis() - now);
}
/**
* 多线程插入测试
* mysql, 10w, 1tc, 1.24s
*/
@Test
void testMultiThreadsInsert() {
final var content = create(4000, 10 * 10000);
final var now = System.currentTimeMillis();
final var nThreads = 8;
try (final var executorService = Executors.newFixedThreadPool(nThreads)) {
Lists.partition(content, nThreads).forEach(v -> executorService.execute(() -> service.insert("t_content", v, 1000)));
service.multiInsert("t_content", content, 1000);
}
log.info("耗时: {}", System.currentTimeMillis() - now);
}
}
工具安装
Sysbench 在线安装 (Ubuntu)
curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.deb.sh | sudo bash
sudo apt -y install sysbench
MYSQL 安装
-
管理员权限CMD, 指定目录: bin. 执行: mysqld --initialize --console, 安装后会显示密码. [A temporary password is generated for root@localhost: cerl7bGX4H#h]
-
上面操作可以重试, 删除 datadir 目录,再执行一遍初始化命令.
-
安装: mysqld --install mysql8
mysql8 自定义名称
-
启动: net start mysql8, 关闭: net stop mysql8
-
登录: mysql -u root -p
-
修改密码:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; //简单登录
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY '123456';
user mysql; update user set Host='%' where User='root'; //设置远程登录 FLUSH PRIVILEGES;