[图书查询场景-Redis查询图书+记录用户查询行为到MySQL:写速度慢造成了抗QPS能力低]同步转异步,单条转批量:QPS优化从2000到8000

5 阅读3分钟

批处理步骤概述

  1. 收集用户数据:每次用户查询时,将查询行为数据存储在内存中的 List 结构中。
  2. 定时批量写入:每5分钟触发一次批量写入操作,将内存中的数据(即 List 中的数据)一次性写入数据库。
  3. 使用JDBC批处理:在写入数据库时,使用JDBC的批处理(batch insert)来一次性插入多条记录,从而避免频繁的数据库连接和写操作。

JDBC 批处理步骤

  1. 创建数据库连接:使用数据库连接池来管理连接,提高效率。
  2. 创建 PreparedStatement:使用批量插入的 PreparedStatement 来减少数据库交互的次数。
  3. 收集数据:将内存中的 List 数据逐条添加到批处理中。
  4. 执行批处理:执行一次批量提交,将数据写入数据库。
  5. 清理资源:操作完成后,释放相关资源。

业务代码示例

import java.sql.*;
import java.util.List;
​
public class UserQueryBatchProcessor {
​
    private static final int BATCH_SIZE = 1000; // 批量大小,可以根据实际情况调整
    private static final int FLUSH_INTERVAL = 5 * 60 * 1000; // 5分钟的时间间隔,单位:毫秒
​
    // 模拟用户查询数据
    private List<UserQueryData> queryDataList; 
​
    // 数据库连接池,假设你有一个数据库连接池管理类
    private DataSource dataSource;
​
    public UserQueryBatchProcessor(DataSource dataSource) {
        this.dataSource = dataSource;
    }
​
    // 处理用户查询记录的批量写入
    public void processUserQueries() {
        while (true) {
            try {
                // 模拟每次收到查询请求后,将数据加入到 List 中
                storeUserQueryData();
​
                // 每隔 5 分钟执行一次批量插入
                Thread.sleep(FLUSH_INTERVAL);
​
                // 定时批量写入数据库
                batchInsertUserQueries();
​
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
​
    // 模拟查询记录的存储
    private void storeUserQueryData() {
        // 此处加入查询行为数据,这部分可以根据具体业务调整
        queryDataList.add(new UserQueryData("user1", "query1", System.currentTimeMillis()));
        queryDataList.add(new UserQueryData("user2", "query2", System.currentTimeMillis()));
        // ...模拟更多查询记录
    }
​
    // 批量插入数据到MySQL
    private void batchInsertUserQueries() {
        if (queryDataList.isEmpty()) {
            return; // 如果没有数据,直接跳过
        }
​
        Connection connection = null;
        PreparedStatement preparedStatement = null;
​
        try {
            // 获取数据库连接
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);  // 开始批处理,不自动提交
​
            String insertSQL = "INSERT INTO user_query_logs (user_id, query, query_time) VALUES (?, ?, ?)";
            preparedStatement = connection.prepareStatement(insertSQL);
​
            // 遍历 List,将每条数据添加到批处理中
            int count = 0;
            for (UserQueryData data : queryDataList) {
                preparedStatement.setString(1, data.getUserId());
                preparedStatement.setString(2, data.getQuery());
                preparedStatement.setLong(3, data.getQueryTime());
                
                preparedStatement.addBatch();  // 将当前的参数添加到批处理
​
                // 每插入一定数量的数据就执行一次批处理
                if (++count % BATCH_SIZE == 0) {
                    preparedStatement.executeBatch();
                }
            }
​
            // 执行剩余的批处理
            preparedStatement.executeBatch();
            
            connection.commit(); // 提交事务
            queryDataList.clear();  // 清空 List,准备下次存储
        } catch (SQLException e) {
            e.printStackTrace();
            if (connection != null) {
                try {
                    connection.rollback();  // 如果发生错误,则回滚事务
                } catch (SQLException rollbackEx) {
                    rollbackEx.printStackTrace();
                }
            }
        } finally {
            // 关闭资源
            try {
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
​
    // 假设这是查询数据的一个简单POJO类
    public static class UserQueryData {
        private String userId;
        private String query;
        private long queryTime;
​
        public UserQueryData(String userId, String query, long queryTime) {
            this.userId = userId;
            this.query = query;
            this.queryTime = queryTime;
        }
​
        public String getUserId() {
            return userId;
        }
​
        public String getQuery() {
            return query;
        }
​
        public long getQueryTime() {
            return queryTime;
        }
    }
}

代码解释

  1. processUserQueries() :这个方法会模拟不断收到用户查询请求,每隔5分钟将积累的查询数据批量写入数据库。
  2. storeUserQueryData() :模拟用户查询数据的存储,可以替换为你实际的用户行为采集方式。
  3. batchInsertUserQueries() :这是核心的批量写入过程,使用了JDBC的批处理(PreparedStatement.addBatch())和事务(connection.setAutoCommit(false))来确保高效的批量插入。每次批量插入后,都会清空内存中的数据列表。
  4. UserQueryData:封装了查询数据的基本结构,存储用户ID、查询内容及查询时间。

相关性能优化

  • 批量大小调整BATCH_SIZE 参数可以根据你的系统的负载情况进行调整。如果数据量非常大,可以适当增大批量插入的大小,但需要注意过大的批量可能导致内存占用过高。
  • 数据库连接池:为了避免频繁地打开和关闭数据库连接,应该使用数据库连接池来管理连接。常见的连接池有 HikariCP、Druid 等。
  • 事务管理:使用事务确保批量插入的原子性。如果批量插入的过程中出现错误,整个操作将会回滚。