MySQL 读写性能测试:基于 Java JDBC 并发压测方案

Ryan
Ryan
发布时间:2026-10-15 | 6人浏览过
原创

本文详细介绍如何利用 Java JDBC 技术对 MySQL 数据库进行并发读写性能压测。通过自定义代码动态适配表结构,支持灵活调整事务提交批次大小与并发线程数,旨在量化评估数据库在不同负载下的写入吞吐量(QPS)。

准备条件

测试环境需具备基础连接能力。在进行压测前,确保 MySQL 服务正常运行,且已创建用于测试的目标表(如 student)。由于程序会读取表元数据以自动生成插入语句,建议提前构建具有代表性的表结构。

推荐使用以下 SQL 创建测试表,注意主键字段长度需预留足够空间:

-- 删除旧测试表
drop table student;

-- 创建测试表,主键 name 预留较大空间以避免 UUID 截断报错
create table student (
    name varchar(120) not null,
    goal varchar(20) not null,
    primary key(name)
);

Java 压测核心逻辑解析

测试程序主要依赖于 java.sql.PreparedStatement 的批处理功能以及 java.util.concurrent.CountDownLatch 控制多线程同步。以下是关键步骤说明:

第1步:配置数据库连接与参数

修改源码中的常量以匹配实际环境。重点包含驱动名、URL(需开启 rewriteBatchedStatements=true 以优化批量插入)、用户名、密码以及目标表名。

图注:典型的测试运行结果输出示例,展示不同批次大小与并发数下的吞吐量统计。

第2步:执行并发写入循环

程序通过嵌套循环遍历预设的并发数列表(如 1, 5, 10, 20)和批次大小列表(如 100, 500, 1000)。每个组合启动一个独立的线程池进行测试:

  • 多线程控制:使用 CountDownLatch 确保所有线程同时开始执行,减少时间差带来的误差。
  • 动态 SQL 生成:通过 queryTableColumns 方法获取表列信息,动态拼接 INSERT INTO 语句,使代码具备通用性。
  • 批量提交:在单次事务中,循环添加多条记录至 PreparedStatement,最后调用 executeBatch() 一次性提交,显著降低网络 IO 开销。

测试过程中产生的大量数据可通过标准 SQL 验证。输入以下命令查看表中行数激增情况:

select count(*) from student;

第3步:结果分析与统计

测试结束后,系统会打印出平均吞吐量(AVG r/s)、最小值(MIN r/s)和最大值(MAX r/s)。这些数据反映了数据库在特定配置下的性能瓶颈。建议在每次测试前后清理或重建测试表,以确保结果的可比性,避免脏数据干扰。

常见问题与排错

主键长度导致的截断错误

代码使用 UUID.randomUUID().toString() 生成唯一标识作为主键。生成的 UUID 字符串较长(通常为 36 字符),若数据库中对应字段(如 name)长度设置过小,将引发 SQL 异常。请确保该字段长度至少能容纳 UUID 字符串。

图注:因主键字段过短导致的报错示例。

测试数据残留影响准确性

由于采用 INSERT INTO ... VALUES 追加模式,多次连续运行同一张表会导致数据量累积,可能影响索引性能测试的真实性。建议在每组实验前执行 drop table student; 并重新建表,或使用 TRUNCATE TABLE 清空数据(需注意事务回滚行为)。

完整 Java 源代码

以下代码实现了上述逻辑,可直接编译运行。请根据实际需求调整 concurrentList 和 batchSizeList 数组内容。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Formatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class InsertTest {
 
    private static Logger logger = Logger.getLogger(InsertTest.class.getName());
 
    // 驱动程序名
    private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver";
    // URL指向要访问的数据库名mysql,务必开启 rewriteBatchedStatements 优化批量插入
    private static final String DB_URL = "jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useSSL=false&serverTimezone=UTC";
    // MySQL配置时的用户名
    private static final String DB_USERNAME = "root";
    // MySQL配置时的密码
    private static final String DB_PASSWORD = "your_password_here";
 
    private static Random random = new Random(10000);
    // 我们要测试的表的名称
    private static final String TABLE_NAME = "student";
    private int batchSize;// 一批提交的事务数
    private int concurrent;// 并发线程数
    private int sampling;// 采样次数
 
    public static void main(String[] args) throws Exception {
        printHeader();
 
        // 默认测试 1,5,10,20 个并发
        int[] concurrentList = new int[]{1, 5, 10, 20};
        // 一批提交的事务数:100, 200, 500, 1000
        int[] batchSizeList = new int[] {100, 200, 500, 1000};
        
        for (int concurrent : concurrentList) {
            for (int batchSize : batchSizeList) {
                // 对以上每种组合都 run 一次
                new InsertTest(batchSize, concurrent).run(true);
            }
            Thread.sleep(10000); // 每次组合间隔休息,防止数据库过载
        }
    }
   
    /*-----------InsertTest类的构造函数一-------------*/
    public InsertTest(final int batchSize, final int concurrent) throws Exception {
        this.batchSize = batchSize;
        this.concurrent = concurrent;
        this.sampling = 100;
    }
    
    /*-----------InsertTest类的构造函数二-------------*/
    public InsertTest(final int batchSize, final int concurrent, final int sampling) throws Exception {
        this.batchSize = batchSize;
        this.concurrent = concurrent;
        this.sampling = sampling;
    }
    
    /*-----------开始运行run方法-------------*/
    public void run(boolean printResult) throws Exception {
        final List<Long> results = Collections.synchronizedList(new ArrayList<Long>());
        final CountDownLatch startGate = new CountDownLatch(concurrent);
        final CountDownLatch endGate = new CountDownLatch(concurrent);
 
        for (int idxConcurrent = 0; idxConcurrent < concurrent; idxConcurrent++) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        startGate.countDown(); // 通知可以开始了
                        long time = execute();
                        // 计算速率:总行数 * 采样数 * 1000ms / 耗时(ms)
                        long avg = batchSize * sampling * 1000 / time;
                        results.add(Long.valueOf(avg));
                    } catch(Exception ex) {
                        ex.printStackTrace();
                    } finally {
                        endGate.countDown(); // 结束计数
                    }
                }
            }).start();
        }
        endGate.await(); // 等待所有线程完成
 
        Collections.sort(results);
        if (printResult) {
            printResult(batchSize, concurrent, results);
        }
    }
 
    public long execute() throws Exception {
        Connection conn = getConnection();
        Map<String, Integer> columns = queryTableColumns(conn);
        String insertSQL = generateInsertSQL(columns);
        PreparedStatement ps = conn.prepareStatement(insertSQL);
        try {
            long start = System.currentTimeMillis();
            // 重复采样多次取平均值以提高稳定性
            for (int i = 0; i < sampling; i++) {
                execute(conn, ps, columns);
            }
            long stop = System.currentTimeMillis();
            return stop - start;
        } catch(Exception ex) {
            logger.log(Level.SEVERE, null, ex);
            conn.rollback();
            conn.close();
            throw ex;
        } finally {
            conn.close();
        }
    }
    
    //执行插入语句
    public void execute(Connection conn, PreparedStatement ps, Map<String, Integer> columns) throws Exception {
        try {
            for (int idx = 0; idx < batchSize; idx++) {
                int idxColumn = 1;
                // 遍历表的所有列
                for (String column : columns.keySet()) {
                    // 如果该列为name列即key对应的列,就单独为他生成一个主键。
                    if (column.equalsIgnoreCase("name")) {
                        // UUID.randomUUID().toString()是java JDK提供的一个自动生成主键的方法
                        ps.setObject(idxColumn, UUID.randomUUID().toString());
                    } 
                    // 否则就是普通列,随机填充数据
                    else {
                        ps.setObject(idxColumn, generateColumnValue(columns.get(column)));
                    }
                    idxColumn ++;
                }
                ps.addBatch();
            }
            // 批量执行SQL语句
            ps.executeBatch();
            conn.commit();
 
            ps.clearBatch();
        } 
        catch (SQLException ex) {
            logger.log(Level.SEVERE, null, ex);
            if (null != ex.getNextException()) {
                logger.log(Level.SEVERE, null, ex.getNextException());
            }
            conn.rollback();
            throw ex;
        }
    }
    
    //根据获取的列信息,生成插入的sql语句
    private String generateInsertSQL(Map<String, Integer> columns) throws SQLException {
        StringBuilder sb = new StringBuilder();
        StringBuffer sbColumns = new StringBuffer();
        StringBuffer sbValues = new StringBuffer();
 
        sb.append("INSERT INTO ").append(TABLE_NAME);
 
        for (String column : columns.keySet()) {
            if (sbColumns.length() > 0) {
                sbColumns.append(",");
                sbValues.append(",");
            }
            sbColumns.append(column);
            sbValues.append("?");
        }
        sb.append("(").append(sbColumns).append(")");
        sb.append("VALUES");
        sb.append("(").append(sbValues).append(")");
        return sb.toString();
    }
 
    //查询表格结构信息
    private Map<String, Integer> queryTableColumns(Connection conn) throws Exception {
        Map<String, Integer> columns = new LinkedHashMap<String, Integer>();
        String sql = "SELECT * FROM " + TABLE_NAME + " WHERE 1=0";
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);
        ResultSetMetaData rsmd = rs.getMetaData();
        for (int i = 1; i <= rsmd.getColumnCount(); i++) {
            columns.put(rsmd.getColumnName(i), rsmd.getColumnType(i));
        }
        return columns;
    }
    
    //生成列值,根据字段类型随机填充
    private Object generateColumnValue(int type) {
        Object obj = null;
        switch (type) {
            case Types.DECIMAL:
            case Types.NUMERIC:
            case Types.DOUBLE:
            case Types.FLOAT:
            case Types.REAL:
            case Types.BIGINT:
            case Types.TINYINT:
            case Types.SMALLINT:
            case Types.INTEGER:
                obj = random.nextInt(10000);
                break;
            case Types.DATE:
                obj = Calendar.getInstance().getTime();
                break;
            case Types.TIMESTAMP:
                obj = new Timestamp(System.currentTimeMillis());
                break;
            default:
                obj = String.valueOf(random.nextInt(10000));
                break;
        }
        return obj;
    }
    
    //连接MYSQL数据库
    private Connection getConnection() throws Exception {
        Class.forName(DB_DRIVER);
        Connection conn = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
        // 关闭自动提交,手动管理事务以配合 batch 使用
        conn.setAutoCommit(false);
        return conn;
    }
    
    //打印出表头
    private static void printHeader() {
        StringBuilder sb = new StringBuilder();
        sb.append("\n");
        sb.append(new Formatter().format("%15s|%15s|%15s|%15s|%15s", "BATCH_SIZE", "CONCURRENT", "AVG (r/s)", "MIN (r/s)", "MAX (r/s)"));
        System.out.println(sb.toString());
    }
    
    //打印每次跑完后的统计信息
    private static void printResult(int batch, int concurrent, List<Long> results) {
        Long total = Long.valueOf(0);
        for (Long result : results) {
            total += result;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(new Formatter().format("%15s|%15s|%15s|%15s|%15s", batch, concurrent, (total/results.size()), results.get(0), results.get(results.size() - 1)));
        System.out.println(sb.toString());
    }
}

以上就是 MySQL 读写性能测试的详细内容,更多关于 MySQL JDBC 并发压测的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表