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: 每秒处理查询数

清理数据

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 cleanup

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 安装

  1. 下载: https://dev.mysql.com/downloads/file/?id=476233

  2. 管理员权限CMD, 指定目录: bin. 执行: mysqld --initialize --console, 安装后会显示密码. [A temporary password is generated for root@localhost: cerl7bGX4H#h]

  3. 上面操作可以重试, 删除 datadir 目录,再执行一遍初始化命令.

  4. 安装: mysqld --install mysql8 mysql8 自定义名称

  5. 启动: net start mysql8, 关闭: net stop mysql8

  6. 登录: mysql -u root -p

  7. 修改密码:

    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;

WSL安装的linux环境访问windows的服务

  1. 打开防火墙,并设置入站规则

  2. 新建规则, 选端口。

  3. 选TCP, 填写本地端口范围:3306或全部

  4. 保存即可

未完待续…​

Sysbench和Spring JdbcTemplate 插入性能测试实践的开篇就讲完了,下篇将会介绍 Spring Security简明教程与微服务的融合实践。