分页功能优化
preface
最近运维平台新上一个功能,此功能涉及到对数据库某个表的操作,该表的数据量特别大,高达10W+,此时我们在在后端做好了分页,但是前端加载还是需要3秒的时间,用户体验不太好,可能你会说了,你可以先把html页面的整个框架先展现出来,表数据最后加载,没错我就是这么干的,但是表数据完全加载完也需三秒的时间,这还是第一页,当你点击第二页的时候,还是需要等待3秒加载,也就是说不管你点击哪一页数据,加载还是慢。
自此就需要考虑优化了,我通过review代码,发现性能瓶颈出现在mysql取数据,我这里使用的是mysql,取数据的语句是select * from xxx.xxxx
,没错,我在mysql上操作这条语句是挺快的,10W条数据刷刷的就在屏幕上打印完了。
优化步骤探索
先说说开发环境:
前/后端 | 语言 | 框架 |
---|---|---|
前端 | JS+html | 无 |
后端 | python2.7 | Django |
既然找到了瓶颈所在,那么就开始优化吧。,我先介绍下我的分页的环境:
- 分页是5页,前端按钮有下一页和上一页,末页和开始的按钮。
- 前端可以动态选择每页展示多少条记录,有下拉框选择,选择后会向后端发起ajax请求,重新加载数据。
- 所有分页功能在后端代码中实现。
其实我们所利用思想就是只取离用户所浏览页面的前后共五页的数据,当点击末页或者跳页的时候,再加载全部数据。
我前端页面传来这几个参数:
- offset 偏移量,表明获取第几条数据
- limit 每页显示的条目在前端。
这两个参数一结合,我们就可以计算出用户需要访问的记录是从mysql的第几条到第几条,比如说我的limit是20,现在offset是80,那么用户目前需要访问范围是80到(80+20),这个只是单纯的计算出用户访问下一页的范围,所以你的sql语句也可以写为select * from xxx.xxxx limit 80,100
,如果你真的那么单纯是这样想的,那么恭喜你采坑了,因为我刚才说了后端分页是分五页的,如果你只取20条数据,那么根本不够分页,那么是做错了。因为分5页,每页20条记录,最少也得取100条,且你要保证你前端的按钮在连续点击下一页的时候始终可以保持在中间。如果你理解了我的意思,那么请继续往下看。
我们刚才说分页是5页,此时limit是20,现在offset是40,那么这五页 的数据量范围是(40-100/2)到(40+100/2),为什么这么计算:是这样的,100是五页总记录量,且此时除了第一页和第二页之外,我们必须保证前端分页按钮始终是让中间那页的高亮(比如说现在是页数是3|4|5|6|7,5是中间页,必须高亮,就是这个意思。),所以我们就要把他要访问的页数的前后共五页的数量计算出来了。所以我们的代码可以这么写:
limit_interval = limit*5 # 前端页面只显示5页到按钮
limit_start = offset-limit_interval/2
if limit_start < 0: # 数据库里没有负数的说法, 如果小于0,那么就从0开始
limit_start = 0
sql = 'select * from xxx %s limit %d,%d' %(where_sql if where_sql else '',limit_start, offset+limit_interval/2)
了解如何计算前后页之后,那么就说说下面这个问题,上述的代码有个缺陷就是,如果你点击末页之后,只是往后翻了3页罢了,为啥,因为我们这个计算方式始终是计算前后五页的内容,所以根本无法使用末页的功能。好了,如果你还没有糊涂,那么下面请继续看:
我们可以这样去判断用户是否点击了末页,首先我这里观察的规律是,拿这次用户来访的offset减去上次用户访问的offset,然后除以limit,得出的商再去除以2,如果得到的余数是0,说明用户访问的就是最后一页了。此时sql语句就是select * from xxx.xxxx
。
详细解释下为什么是次用户来访的offset减去上次用户访问的offset,然后除以limit。
公式是我自己研究出来的: (offset - last_offset )/limit = 需要往后翻的页数
因为两次offset之差就能够判断用户是否连续点击了末页,因为我们知道刚才我们是点击末页只是往后翻了3页,所以是说,如果上次用户的offset是60,这次的offset是100,那么说明他点击了末页,所以给他调到我们第一次做从数据库拉取数据的末页,此时他再次点击末页,那么这次的offset是140 了,那么(140-100)/20=2,由于我们分页只做5页的分页,且他此时必定在中间页(这就是为啥我们刚才需要让中间页一直高亮),我们可以判断他再次点击了末页,还不懂?我们再次假设中间页为3,末页为5,(5-3)/2=1,刚好整除,余数为0,说明就是点击了末页。所以拉取数据库所有数据了。
这样做好的效果就是用户第一次点击末页是翻到当前分页量(offset+limit*5/2)的最后一页,再次点击末页的时候,就是直接到了所有分页的最后一页。
说了那么多,就看代码吧:
def modify_DNS_sql(request,limit=None,offset=None,where_sql=None):
'''
xxxxxx
:param request: django 携带的request
:param limit : 每页最大的条目
:param offset: 分页后的页面偏移量,
:param where_sql: sql 语句匹配条件
:return:
'''
db_run = database_handle.db_handle(settings.HTTPDNS_DATABASE)
if request.method == 'GET':
# 返回所有数据通过select查询
try:
offset = int(offset)
limit = int(limit)
last_offset = cache.get('last_offset_%s'%request.user)
cache.delete('last_offset_%s'%request.user)
if last_offset: # 说明不是第一次访问了
T = (offset - last_offset )/limit # 计算出这个T值是方便下面的代码判断用户有没有点击查看最后一页的按钮
r = T.__divmod__(2) # 使用除法,如果余数等于0,说明是用户点击查看最后一页的按钮
if not r[1]:
sql = 'select * from uc_ipaddress'
elif r[1] == 1: # 否则的话只查看当前页数的前后几页
limit_interval = limit*5 # 前端页面只显示5页到按钮
limit_start = offset-limit_interval/2
if limit_start < 0:
limit_start = 0
sql = 'select * from xxxx %s limit %d,%d' %(where_sql if where_sql else '',
limit_start,
offset+limit_interval/2)
else:
if not offset: # 如果没有offset,表示当前用户查看的是第一页
sql = 'select * from uc_ipaddress %s limit 0,%d' %(where_sql if where_sql else '',limit*5)
else: # 有offset,那么计算出用户当前页的前后几页的条目数量,这样在数据库select的时候能够加快前端显示速度
limit_interval = limit*5 # 前端页面只显示5页到按钮
limit_start = offset-limit_interval/2
if limit_start < 0:
limit_start = 0
sql = 'select * from xxx %s limit %d,%d' %(where_sql if where_sql else '',
limit_start,
offset+limit_interval/2)
cache.set('last_offset_%s'%request.user,offset)
return db_run.run(sql)
except (InternalError,BaseException) , e:
logger.error('[fuck you ] sql_cmd: %s ,Exception: %s ' % (sql, e))
return False
上述代码是早app/core.py下面,app是指你的app名字。
用户首先到url.py 后再到views.py里面,views.py里面再调用这个modify_DNS_sql方法去处理。
至此,前端点击下一页的时候,秒开,只有连续2次点击末页或者隔页才会进入3秒的等待刷新状态。