Home Tags Posts tagged with "分页"

分页

0 96

我,一名新入职的工程师;

我,缺乏高并发实战经验;

我,后端回参上千条数据;

我,循环访问数据库一次查一条;

我,从不分页,从不批量处理;

我被称为性能杀手,服务器终结者。

 

秋招时,我能和面试官大谈高并发,高可用,高扩展;答辩时,我能和老师探讨多线程,分布式锁,性能优化…

面试官和老师眼中的我:

查看源图像

实际上的我:

动图

 

实际工作中,为什么要分页?为什么不应该在循环中查询数据库?怎样在工作中去考虑性能问题?相信本文能带给你一些思考。


一、为什么要分页?

分页功能在网页中是非常常见的一个功能,其作用也就是将数据分割成多个页面来进行显示。

  • 使用场景: 当取到的数据量达到一定的时候,就需要使用分页来进行数据分割。

当我们不使用分页功能的时候,会面临许多的问题:

  • 客户端的问题: 如果数据量太多,都显示在同一个页面的话,会因为页面太长严重影响到用户的体验,也不便于操作,也会出现加载太慢的问题
  • 服务端的问题: 如果数据量太多,可能会造成内存溢出(OOM),而且一次请求携带的数据太多,对服务器的性能也是一个考验

 

以C/S架构为例,要实现“展示所有的商品信息”功能,实现流程是:用户点击功能按钮,前端发起请求,后端接收请求去查对应库表,返回响应给前端,前端将数据渲染到App页面上。

假使商品信息表中有上百万条记录,如果不做分页处理,后端直接从表中获取百万条记录

服务器压力过大

首先服务器的内存可能就会溢出,即OOM。

假设公司老板财大气粗,服务器内存特别大,JVM的内存参数设置的也很大,内存没有爆掉,那么就来到了“返回响应给前端”这一步。

网络开销过大

我们都知道,网络传输的数据量越多,时延也会越高。

百万条记录在网络中的传输,究竟要多久呢?

以虚拟机为64位的机器为例,假设单个商品信息对象有8个字段且都是基本数据类型,对象头占用的内存是8(运行时数据)+4(类型指针)=12Byte,实例数据是(8个字段)8 * 4 = 32Byte,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍,所以按照一个商品信息对象 48Byte的大小进行计算,百万条数据的大小为:

48 * 100W = 45.77MB

传输时间过长

接下来,计算45.77MB在网络速度为100Mb/S的情况下的传输时长为:

45.77 MB* 8bit / 100Mb = 3.66s

实际消耗肯定比3.66s更长,因为有损耗

注意,这仅仅是数据在网络中传输需要的时长,在此之前,客户端和服务端还需要经历TCP三次握手建立连接等等,在前端接收到数据之后,还要对数据进行解析、格式处理等等…

即使假设系统优化做的非常好,客户端的网络、硬件配置也特别高,整个流程仅花费了网络传输的时长3.66s,也无法接受。

用户体验太差

根据1/3/5秒原则,在1s以内得到响应,用户会觉得系统响应很快,体验非常好;1-3秒得到响应,用户可以接收,体验还不错;3-5秒才响应,用户就感觉慢了,体验有点糟糕;一旦响应超过5秒,用户就会认为是个失败的体验,选择离开或重新发起请求。

所以,为什么要一次性返回百万级别的数据呢,如此庞大的网络开销,如此缓慢的响应时间,如此不可接受的用户体验…槽点太多以至于无法吐槽。

那么,我们必须思考一下,用户需要一次性查看这么多条数据吗?

显然不需要。用户想要查看所有的商品信息,但他不可能一目十行,而且手机显示屏也装不下百万条数据,所以他一定是分批次去查看数据。就好像我们看小说一样,一本书300W字,不可能在同一页面展示,所以我们一定是这页看完了再去看下一页。

二、有哪些分页方式?

分页的时间节点

对于用户来说,他并不关心分页怎么实现,但对程序员来说,分页的实现有多种选择。

以 “展示所有的商品信息” 为例,从请求发起到返回数据的整个过程如下图所示:

从上图观察得知,我们有三个节点可以进行分页处理,分别是:

  • 数据库分页
  • 后端逻辑分页
  • 前端逻辑分页

其中,数据库分页为物理分页,后端前端分页为逻辑分页

1.真分页(物理分页):

  • 实现原理:SELECT * FROM xxx [WHERE...] LIMIT #{param1}, #{param2}
    第一个参数是开始数据的索引位置
    第二个参数是要查询多少条数据
  • 优点: 不会造成内存溢出
  • 缺点: 翻页的速度比较慢

2.假分页(逻辑分页):

  • 实现原理: 一次性将所有的数据查询出来放在内存之中,每次需要查询的时候就直接从内存之中去取出相应索引区间的数据
  • 优点: 分页的速度比较快
  • 缺点: 可能造成内存溢出

分页时间节点的选择

选择的标准是速度,显而易见,数据库,服务器和客户端之间是网络,如果网络传递的数据量越少,则客户端获得响应的速度越快。而且一般来说,数据库和服务器的处理能力一般比客户端要强很多。从这两点来看,客户端分页的方案是最不可取的。

其次就剩下了在服务器端分页和在数据库端分页两种方式了,如果选择在服务器端分页的话,大部分的被过滤掉的数据还是被传输到了服务器端,与其这样还不如直接在数据库端进行分页。

前端要做的就是尽快接受数据并最快地展示给用户,对于数据不多的场景用前端实现也无妨,然而若考虑到以后会有成千上万条数据应用的场景,显然后端去处理分页更合适些。
每次点击下一页,前端只需发送分页数信息请求后端数据,假设一页显示十条数据,每次点击只需请求这十条数据的信息返回给前端来更快地进行交互。然而若是由前端来进行分页操作,那就得把成千上万条数据全部先拉下来,再进行操作,先不说操作这么多数据拉低的性能,光是先拉下来就得费很长时间了,所以对于数据量大的操作,一般都采用后端分页的操作更合适。
首先要了解为什么要分页。分页主要是为了避免一次性从数据库获取大量数据。其次才是为了展示效果。

因此:数据库端分页 > 后端分页 > 前端分页

 三、各个分页方式的实现

数据库分页(以MySQL为例)

1.LIMIT用法

LIMIT出现在查询语句的最后,可以使用一个参数或两个参数来限制取出的数据。其中第一个参数代表偏移量:offset(可选参数),第二个参数代表取出的数据条数:rows。

  • 单参数用法

当指定一个参数时,默认省略了偏移量,即偏移量为0,从第一行数据开始取,一共取rows条。

/* 查询前五条数据 */

SELECT * FROM Student LIMIT 5;

  • 双参数用法

当指定两个参数时,需要注意偏移量的取值是从0开始的,此时可以有两种写法:

/* 查询第1-10条数据 */

SELECT * FROM Student LIMIT 0,10;


/* 查询第11-20条数据 */

SELECT * FROM Student LIMIT 10 OFFSET 10;

学习到Mybatis分页时,了解到可以使用Pagehelper分页工具(开源)

该工具可以通过Maven导入项目,使用起来非常方便,但有几个要注意的点,特此记录!

 

1.导入Jar包

<!--        引入分页插件PageHelper5-->
        <!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>4.1.4</version>
        </dependency>

2.直接在Controller层调用PageHelper.startPage()方法
坑点:
需要分页的SQL语句后面不能加;!!!否则会报如下错误
正确的sql语句: