从很大的表中获取数据

时间:2018-07-08 11:13:04

标签: java mysql multithreading jdbc producer-consumer

我在MySQL数据库中有一个非常大的表,在表Users中有2亿条记录。

我使用JDBC进行查询:

public List<Pair<Long, String>> getUsersAll() throws SQLException {
        Connection cnn = null;
        CallableStatement cs = null;
        ResultSet rs = null;
        final List<Pair<Long, String>> res = new ArrayList<>();
        try {
            cnn = dataSource.getConnection();
            cs = cnn.prepareCall("select UserPropertyKindId, login from TEST.users;");
            rs = cs.executeQuery();
            while (rs.next()) {
                res.add(new ImmutablePair<>(rs.getLong(1), rs.getString(2)));
            }
            return res;
        } catch (SQLException ex) {
            throw ex;
        } finally {
            DbUtils.closeQuietly(cnn, cs, rs);
        }
    }

接下来,我处理结果:

List<Pair<Long, String>> users= dao.getUsersAll();
            if (CollectionUtils.isNotEmpty(users)) {
                for (List<Pair<Long, String>> partition : Lists.partition(users, 2000)) {
                    InconsistsUsers.InconsistsUsersCallable callable = new InconsistsUsers.InconsistsUsersCallable (new ArrayList<>(partition));
                    processExecutor.submit(callable);
                }
            }

但是由于表非常大,并且都已卸载到内存中,因此我的应用程序崩溃并出现错误:

  

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException:通信链接失败

     

成功从服务器接收到的最后一个数据包是105619毫秒之前。

如何部分接收数据并按优先级顺序处理它们,以免一次将所有结果上传到内存中?创建游标并将数据上载到非阻塞队列并在数据到达时对其进行处理是可能的。该怎么办?

更新:

我的数据库结构:https://www.db-fiddle.com/f/v377ZHkG1YZcdQsETtPm9L/3

当前算法:

  1. Users表中获取所有数据用户:select UserPropertyKindId, login from Users;

  2. 该结果分为2000对,并提交给ThreadPoolTaskExecutor

    List<Pair<Long, String>> users= dao.getUsersAll();
    
    if (CollectionUtils.isNotEmpty(users)) {
        for (List<Pair<Long, String>> partition : Lists.partition(users, 2000)) {
            InconsistsUsers.InconsistsUsersCallable callable = new InconsistsUsers.InconsistsUsersCallable(new ArrayList<>(partition));
            processExecutor.submit(callable));
        }
    }
    
  3. 在每对中可调用的两个查询中:

    第一个查询:

    select distinct entityId 
    from UserPropertyValue 
    where userPropertyKindId= ? and value = ? -- value its login from Users table
    

    第二个查询:

    select UserIds 
    from UserPropertyIndex 
    where UserPropertyKindId = ? and Value = ?
    

可能有两种情况:

  1. 第一个查询的结果为空:记录日志,发送通知,继续下一个配对
  2. 第二个查询的结果不等于第一个查询的结果(解码的二进制数据。已存储已编码的entityId)。然后登录,发送通知,转到下一对。

我无法更改底座的结构。我必须在Java代码方面进行的所有操作。

4 个答案:

答案 0 :(得分:4)

您应该在几个级别上进行处理:

JDBC驱动程序访存大小

JDBC具有Statement.setFetchSize()方法,该方法指示在从JDBC获取行之前,JDBC驱动程序将预取多少行。请注意,MySQL JDBC驱动程序并未真正正确地实现此目的,但是您可以设置setFetchSize(Integer.MIN_VALUE)来防止它一次性获取所有行。 See also this answer here

请注意,您也可以使用useCursorFetch

激活连接上的功能

您自己的逻辑

您不应将整个用户列表存储在内存中。您现在正在做的是从JDBC收集所有行,然后稍后使用Lists.partition(users, 2000)对列表进行分区。这是朝着正确的方向发展,但您做的还不正确。而是:

try (ResultSet rs = cs.executeQuery()) {
    while (rs.next()) {
        res.add(new ImmutablePair<>(rs.getLong(1), rs.getString(2)));
    }

    // Process a batch of rows:
    if (res.size() >= 2000) {
        process(res);
        res.clear();
    }
}

// Process the remaining rows
process(res);

这里重要的信息是不要将所有行加载到内存中,然后批量处理它们,而是在从JDBC流传输行时直接处理它们。

答案 1 :(得分:3)

而不是Java端的Lists.partition(users,2000),您应该将每个请求的mysql结果集限制为2000。

select UserPropertyKindId, login from TEST.users limit <offset>, 2000;

更新:正如Raymond Nijland在下面的评论中提到的那样,如果偏移量太大,则查询速度可能会大大降低。

一种解决方法是代替使用offset,引入where语句,例如where id> last_user_id。

由于下面@All_safe进行了注释,因此不存在自动递增ID,因此另一种针对较大偏移量的解决方法是:仅在子查询中获取主键,然后再联接回主表。这将迫使mysql不执行早期行查找,这是偏移量限制较大的主要问题。

但是您的原始查询仅获取主键列,我认为不适用早期行查找。

答案 2 :(得分:1)

您可以优先考虑查询 例如WHERE my_priority = 1 ORDER BY my_sub_priority DESC

和雅各布一样,请使用限制LIMIT 0, 2000

您可能可以打破inconsistent_users中的逻辑以查找特定缺陷,然后利用在EXPLAIN中获得的见解来优化那些查询。也许find_user_defect(defect)这种方法将帮助您明智地处理用户。

答案 3 :(得分:1)

我遇到了类似的情况。我正在从MySQL数据库读取数据并将其复制到MS SQL Server数据库中。不是2亿,每天只有400万。但是我在通信链接失败时也遇到了同样的错误消息。我可以通过设置PreparedStatement.setFetchSize(Integer.MIN_VALUE);的fetchsize来解决它。 因此,通信链接故障消失了。我知道,这不能解决您的列表问题。