Django分页慢的问题解决方案

       在使用Django进行分页的时候,发现数据量大了会变得很慢,有明显的延迟几百万数据延迟竟能达到1-2秒。经过一番分析,本人总结出了几个关键点,并提供了一种实测可行的解决方案。

      首先介绍一下环境,本人使用的是layUI后台管理模板。分页layUI已经搞定,后台服务只需要按照layUI的请求,返回正确页面的数据和总数据量。前端请求url类似这样:/use/ship/data?page=632&limit=10,其中page为查询的页码,limit表示每页多少条数据。后台需要根据page和limit,筛选出正确的页面数据,并且还要返回在总数据量。

      一开始,本人使用Paginator进行处理,代码如下:

        from django.core.paginator import Paginator

        ship_list = ShipModel.objects.all() 
        paginator = Paginator(ship_list , limit)
        count=paginator .count
        ship_list = paginator.page(page)

      其中ShipModel是预定义好的模型。page为查询的页码,limit表示每页多少条数据。

      代码不复杂,但是当数据量达到200万的时候,明显感到迟钝。经过分析首先发现最耗时的是count=paginator .count这句话。

      这是统计总数据量的操作,数据量大慢一点也算说得过去,实测大约500毫秒。这句话实际上会产生类似select count(*) from shipmodel这样的最简单的SQL,看上去没有可优化的手段了。于是我想到可以使用缓存优化,可以把总数据量结果放到缓存,当然要这么做必须保证应用上可行。除了这个地方慢,发现ShipModel.取数据的时候有一个现象,那就是随着page的增大,变得越来越慢,由几毫秒到几十毫秒最终甚至达到几百毫秒。

      这又是为什么呢?实际上,Django分页模型最终将用户请求转化为特定的SQL,在语句后面添加了限定,类似“limit 10 offset 1000”这样的参数。limit 10指出限定选择10条记录,而offset 1000指出,从当前查询条件选中的数据中往后偏移1000条。这样的话就很容易解析这个问题了。数据库的索引是一种数据结构,它可以经过很少次数的判定定位到要找的数据,但是从特定数据往后再偏移多少条记录就不好确定了,这是没有办法直接计算出要找的数据的地址的,因此遍历就再所难免,因此偏移量越大遍历数据就越多,速度自然越慢。好了,我们知道了这个原理就能够理解这个问题的原因,同时还能想出很多解决这个问题的方案。本人给出一种简单实用的方案,可以解决实际问题。

        本方案基于这样一种假设,即所有人的操作大部分情况下都是从前面往后翻页,而非从后往前翻(更复杂的方案可以考虑这一点,但考虑这种情况发生概率低,所以不予考虑)。实际上本方案分成了两部分情况来考虑,一种情况是用户已经查看过至少一页,那么用户再查询新的一页的时候,如果这一新页距离上一页不远,也就是相差记录数不多,我们就可以利用上一页最后一条记录的id来往下查找,而非从总数据集的第一条记录开始往下找,这可以大大提高查询效率。另一种情况就是排除了第一种情况之外的各种情况,这种情况下我们没有一个已知id的记录距离目标页面很近,此时我们采用这样一种手段即直接定位出目标页面的起始id和终止id。然后再根据起始id和终止id直接定位,性能同样可控,但比第一种情况还是要差不少。本方案使用了缓存,主要记录总数据量、上次浏览页面的最后一条记录的偏移量以及id。

具体代码如下:

def cacu_page_url_key(url):
    '''
    该方法将去掉分页请求url中的分页相关的page和limit两个参数
    这样的话对于同一查询条件的分页URL,不论请求哪一页,其URL是相同的
    进一步可以利用该URL作为缓存的Key
    '''
    p_page=url.find("?page=")
    p_limit=url.find("&limit=",p_page+7)
    p_limit_end=url.find("&",p_limit+8)
    if p_limit==-1 or p_page==-1:
        return None
    elif p_limit_end==-1:
        return url[:p_page]
    else:
        return url[:p_page+1]+url[p_limit_end+1:]

def fetch_page(ship_list,page_index , limit): 
    id_list=ship_list.values('id')[(page_index-1)*limit:page_index*limit-1]
    id_len=len(id_list)
    start_id=id_list[0]['id']
    end_id=id_list[id_len-1]['id']
    end_index=(page_index-1)*limit+id_len-1
    ship_list=ship_list.filter(id__range=(start_id,end_id))
    return (ship_list,end_index,end_id)

def cacu_page_with_cache(ship_list,url,page_index , limit):
    '''
    计算分页数据,page_index 为页索引号,即第几页,起始值为1,limit 每页大小限制
    '''
    url_key=cacu_page_url_key(url)
    count=1
    if url_key  :
        page_info=cache.get(url_key)
        if  page_info :
            count,end_index,end_id=page_info
            begin_index=(page_index-1)*limit
            if begin_index>end_index and begin_index-end_index<10000:
                ship_list=ship_list.filter(id__gt=end_id)[begin_index-end_index-1:begin_index-end_index-1+limit]
                plan_list_len=len(ship_list)
                cache.set(url_key,(count,begin_index+plan_list_len-1,ship_list[plan_list_len-1].id))
            else:
                ship_list,end_index,end_id=fetch_page(ship_list,page_index , limit)
                cache.set(url_key,(count,end_index,end_id))
        else:
            count=ship_list.count()
            ship_list,end_index,end_id=fetch_page(ship_list,page_index , limit)
            cache.set(url_key,(count,end_index,end_id))
    else:
        count=ship_list.count()
        ship_list,end_index,end_id=fetch_page(ship_list,page_index , limit)
    return (count,ship_list)

其中cacu_page_url_key函数是为了把url中page和limit参数删除掉,这样得到的url作为缓存的key,这个key就能够代表对所有相同条件的查询。fetch_page函数是根据page_index(页码) , limit直接查询出该页面对应的所有记录的id列表。其中:

          id_list=ship_list.values('id')[(page_index-1)*limit:page_index*limit-1]

         这句话仅仅利用了主键,因此查询速度应该说还是有保障的。而倒数第二句:

         ship_list=ship_list.filter(id__range=(start_id,end_id))

         这句话就是根据id范围直接获取数据,这条语句效率非常高。

         关键就是最后这个cacu_page_with_cache函数。首先第一句话是计算url_key

          url_key=cacu_page_url_key(url)

          如果url中没有page和limit参数,url_key就是None,虽然正确情况下不应如此,但是作为函数还是做了处理,也就是不考虑缓存了,我们主要讨论正常情况。

          正常情况我们通过

         page_info=cache.get(url_key)

         来获取缓存的上次访问的页面信息。有两个结果,一个是缓存过,一个是没有缓存过。

         对于没有缓存过的情况直接调用fetch_page来获取即可,并缓存本次访问信息。注意count为总记录数,end_index为本页面最后一条数据在总数据集中的偏移量,end_id本页面最后一条记录的id。

       如果缓存过又分两种情况,第一种

       if begin_index>end_index and begin_index-end_index<10000:

       满足此条件即为第一种,也就说上次请求的页码比本次小,本次起始记录距离上次页面的最后一条记录不超过10000条(具体允许多少需要实测,只要速度赶超fetch_page就行),这种情况下,我们以上次请求为参考,直接从上次请求往下查找,代码为:

    ship_list=ship_list.filter(id__gt=end_id)[begin_index-end_index-1:begin_index-end_index-1+limit]

    只要偏移量不过大速度相当快。如果偏移量超过10000条(具体可实测设定合适大小),则仍然采用fetch_page获取。

有了这些函数,实际使用时很简单:

        url=request.get_full_path()
        count,ship_list=cacu_page_with_cache(ship_list,url,page , limit)

       一句话即可得到数据集记录总数以及相应页码的数据。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值