从 MySQL 读取 100w 数据进行处理,应该怎么做?

背景

大数据量操作的场景大致如下:

  • 数据迁移

  • 数据导出

  • 批量处理数据

在实际工作中当指定查询数据过大时,我们一般使用分页查询的方式一页一页的将数据放到内存处理。但有些情况不需要分页的方式查询数据或分很大一页查询数据时,如果一下子将数据全部加载出来到内存中,很可能会发生OOM(内存溢出);而且查询会很慢,因为框架耗费大量的时间和内存去把数据库查询的结果封装成我们想要的对象(实体类)。

举例:在业务系统需要从 MySQL 数据库里读取 100w 数据行进行处理,应该怎么做?

做法通常如下:

  • 常规查询: 一次性读取 100w 数据到 JVM 内存中,或者分页读取

  • 流式查询: 建立长连接,利用服务端游标,每次读取一条加载到 JVM 内存(多次获取,一次一行)

  • 游标查询: 和流式一样,通过 fetchSize 参数,控制一次读取多少条数据(多次获取,一次多行)

常规查询

默认情况下,完整的检索结果集会将其存储在内存中。在大多数情况下,这是最有效的操作方式,并且由于 MySQL 网络协议的设计,因此更易于实现。

举例:假设单表 100w 数据量,一般会采用分页的方式查询:

@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
 
    @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
    Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
 
}
注:该示例使用的 MybatisPlus。

该方式比较简单,如果在不考虑 LIMIT 深分页优化情况下,估计你的数据库服务器就噶皮了,或者你能等上几十分钟或几小时,甚至几天时间检索数据。

流式查询

流式查询指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。

如果没有流式查询,我们想要从数据库取 100w 条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。

MyBatis 中使用流式查询避免数据量过大导致 OOM ,但在流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:

  • 执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。

  • 必须先读取(或关闭)结果集中的所有行,然后才能对连接发出任何其他查询,否则将引发异常。

MyBatis 流式查询接口

MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeable 和 java.lang.Iterable 接口,由此可知:

  • Cursor 是可关闭的;

  • Cursor 是可遍历的。

除此之外,Cursor 还提供了三个方法:

  • isOpen(): 用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;

  • isConsumed(): 用于判断查询结果是否全部取完。

  • getCurrentIndex(): 返回已经获取了多少条数据

使用流式查询,则要保持对产生结果集的语句所引用的表的并发访问,因为其查询会独占连接,所以必须尽快处理

为什么要用流式查询?

如果有一个很大的查询结果需要遍历处理,又不想一次性将结果集装入客户端内存,就可以考虑使用流式查询;

分库分表场景下,单个表的查询结果集虽然不大,但如果某个查询跨了多个库多个表,又要做结果集的合并、排序等动作,依然有可能撑爆内存;详细研究了sharding-sphere的代码不难发现,除了group by与order by字段不一样之外,其他的场景都非常适合使用流式查询,可以最大限度的降低对客户端内存的消耗。

游标查询

对大量数据进行处理时,为防止内存泄漏情况发生,也可以采用游标方式进行数据查询处理。这种处理方式比常规查询要快很多。

当查询百万级的数据的时候,还可以使用游标方式进行数据查询处理,不仅可以节省内存的消耗,而且还不需要一次性取出所有数据,可以进行逐条处理或逐条取出部分批量处理。一次查询指定 fetchSize 的数据,直到把数据全部处理完。

Mybatis 的处理加了两个注解:@Options 和 @ResultType

@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
 
    // 方式一 多次获取,一次多行
    @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000000)
    Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
 
    // 方式二 一次获取,一次一行
    @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
    @ResultType(BigDataSearchEntity.class)
    void listData(@Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper, ResultHandler<BigDataSearchEntity> handler);
 
}

@Options

  • ResultSet.FORWORD_ONLY:结果集的游标只能向下滚动

  • ResultSet.SCROLL_INSENSITIVE:结果集的游标可以上下移动,当数据库变化时,当前结果集不变

  • ResultSet.SCROLL_SENSITIVE:返回可滚动的结果集,当数据库变化时,当前结果集同步改变

  • fetchSize:每次获取量

@ResultType

  • @ResultType(BigDataSearchEntity.class):转换成返回实体类型

注意:返回类型必须为 void ,因为查询的结果在 ResultHandler 里处理数据,所以这个 hander 也是必须的,可以使用 lambda 实现一个依次处理逻辑。

注意:

虽然上面的代码中都有 @Options 但实际操作却有不同:

方式一是多次查询,一次返回多条;

方式二是一次查询,一次返回一条;

原因:

Oracle 是从服务器一次取出 fetch size 条记录放在客户端,客户端处理完成一个批次后再向服务器取下一个批次,直到所有数据处理完成。

MySQL 是在执行 ResultSet.next() 方法时,会通过数据库连接一条一条的返回。flush buffer 的过程是阻塞式的,如果网络中发生了拥塞,send buffer 被填满,会导致 buffer 一直 flush 不出去,那 MySQL 的处理线程会阻塞,从而避免数据把客户端内存撑爆。

非流式查询和流式查询区别:

  • 非流式查询:内存会随着查询记录的增长而近乎直线增长。

  • 流式查询:内存会保持稳定,不会随着记录的增长而增长。其内存大小取决于批处理大小BATCH_SIZE的设置,该尺寸越大,内存会越大。所以BATCH_SIZE应该根据业务情况设置合适的大小。

另外要切记每次处理完一批结果要记得释放存储每批数据的临时容器,即上文中的gxids.clear();

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 要从 MySQL 数据库读取数据,需要使用 Django 中的 ORM(对象关系映射)框架。首先需要在 Django 项目的 settings.py 文件中配置 MySQL 数据库连接信息,例如: ```python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'your_database_name', 'USER': 'your_database_user', 'PASSWORD': 'your_database_password', 'HOST': 'your_database_host', # 可选 'PORT': 'your_database_port', # 可选 } } ``` 接下来,在 Django 应用中创建一个与数据库表对应的模型类,例如: ```python from django.db import models class Person(models.Model): name = models.CharField(max_length=50) age = models.IntegerField() def __str__(self): return self.name ``` 其中,`models.Model` 是 Django ORM 中的基类,表示该类是一个模型类。`CharField` 和 `IntegerField` 分别表示字符型和整型字段。 最后,在 Django 应用中,可以使用以下代码从数据库中查询数据: ```python from .models import Person # 查询所有 Person 记录 persons = Person.objects.all() # 查询年龄大于等于 18 岁的 Person 记录 persons = Person.objects.filter(age__gte=18) # 查询名字为 "Alice" 的 Person 记录 person = Person.objects.get(name="Alice") ``` 其中,`objects` 是模型类的一个属性,表示数据库表中的所有记录。`all()` 方法返回所有记录,`filter()` 方法可以使用条件过滤记录,`get()` 方法返回符合条件的唯一一条记录。以上代码中的 `__gte` 表示大于等于的条件,`name` 和 `age` 分别为模型类中定义的字段名。 ### 回答2: django是一个用于构建Web应用程序的高级框架,它提供了许多方便的工具和库来简化开发过程。当我们想要从MySQL数据库读取数据时,可以使用django自带的ORM(对象关系映射)工具。 首先,我们需要在django的项目设置文件中配置MySQL数据库的连接信息。在其中,我们需要设置数据库的名称、用户名、密码以及主机地址等信息。 接下来,在django的应用程序中创建一个模型类来代表我们想要读取数据表的结构。模型类是一个python类,继承自django中提供的`models.Model`类。在模型类中,我们需要定义各个字段的类型和属性,以及与其他模型之间的关系。 在模型类中定义好后,我们需要运行`makemigrations`和`migrate`命令来创建数据表并将其迁移到MySQL数据库中。这两个命令会根据我们定义的模型类生成相应的SQL语句,并执行这些语句来创建或更新数据表。 一旦数据表创建好后,我们就可以使用django的查询API来从MySQL数据库读取数据。查询API提供了丰富的方法用于执行各种查询操作,例如过滤、排序、聚合等。我们可以根据实际需要调用这些方法来获取我们想要的数据。 最后,将读取数据传递给django的模板来呈现给用户。模板是一个html文件,可以使用django的模板语言来动态地生成页面内容。我们可以在模板中通过使用变量和模板标签来引用从数据库读取数据,并将其展示给用户。 总之,通过django的ORM工具,我们可以方便地从MySQL数据库读取数据。我们只需要配置数据库连接信息、定义模型类、运行迁移命令、使用查询API获取数据,并在模板中呈现给用户即可。这样,我们可以更高效地进行数据读取和展示的开发工作。 ### 回答3: Django是一个开源的Python Web框架,它支持多种数据库,包括MySQL。要从MySQL读取数据,我们需要以下几步。 首先,我们需要在Django的项目设置中配置MySQL数据库连接。我们可以通过在`settings.py`文件中的`DATABASES`设置中指定MySQL数据库的相关信息,包括数据库引擎、主机、端口、用户名、密码和数据库名称等。 其次,我们需要在Django中定义一个模型类来映射MySQL数据库中的表结构。我们可以使用Django提供的`models.Model`作为基类来创建模型类,并在模型类中定义字段来描述表中的列。例如,如果我们有一个名为`User`的表,其中包含`id`、`name`和`age`三个列,那么我们可以创建一个名为`User`的模型类,并在类中定义相应的字段。 然后,我们可以使用Django提供的ORM(对象关系映射)机制来操作MySQL数据库。通过在视图函数或方法中引入模型类,并使用`objects`属性来执行查询操作,我们可以从MySQL读取数据。例如,我们可以使用`User.objects.all()`方法来获取`User`表中的所有数据行,或使用`User.objects.get(id=1)`方法来获取`id`为1的数据行。 最后,我们可以将读取到的数据呈现给用户。在Django中,我们可以通过视图函数或方法来处理用户的请求,并使用模板系统将数据渲染到相应的HTML模板中,最终返回给用户。可以使用Django的内置模板标签和过滤器来对数据进行处理和展示,以满足用户的需求。 综上所述,要从MySQL读取数据,我们需要配置数据库连接、定义模型类、使用ORM机制执行查询操作,并将查询结果渲染到模板中最终返回给用户。通过Django提供的丰富功能和简洁易用的API,我们可以快速高效地实现从MySQL数据库读取数据的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一枚务实的码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值