地址查询优化

地址查询优化

地址服务是比较常见的服务,一般国家地址分了3~4个层级,省,市,区,镇。一般的查询如名称,等级都比较好查询。但是如果是以下场景可能会出现问题。

  • 查询一个省下的所有级别地址往往可能需要查询3次DB,一次查询一个级别,如果碰到地址特别多的情况,如最后一级有1w个地址,使用select * from xx where id in(xx,xx…) 这种形式,性能也不会也别好。
  • 再者还有一种情况,如果是知道了区,想要知道他的省和市也是需要查询多次DB 才能够平凑出结果。而这个过程太慢了。需要秒级。对用户来说来慢了。

优化

DB层

查看建表 sql.

  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_location_id` (`location_id`),
  KEY `idx_name` (`name`),

发现只有 location_id, name 有索引。对于parent_id 则没有,怪不得这么慢。我们可以增加parent_id 索引进行优化。

业务层

可以知道查询慢主要是subtree 功能, 以及查询地址的path 功能。这个树结构非常的切合。除了树结构之外还需要快速的找到某个节点在树中的位置。那么map 无疑是最佳的选择。想好了之后就开始动手实现。代码如下:

from manager.locations.models import LogisticLocationTab
import sys
import schedule
from common.logger import log

"""
build location tree

1. try to use information_schema update_time, test env not update.
2. use schedule to update for now
"""


def get_location_all_by_county(country):
    return LogisticLocationTab.objects.filter(country=country)


def get_location_all():
    return LogisticLocationTab.objects.all()


def create_location_id_map(location_infos):
    return {location.location_id: location for location in location_infos}


def get_sub_location_by_tree(location_node_list, sub, size=sys.maxint):
    if location_node_list:
        sub.extend(location_node_list)
        if len(sub) > size:
            return
    else:
        return
    
    for location_node in location_node_list:
        get_sub_location_by_tree(location_node.sub, sub, size)
        

def build_tree(all_location, location_map):
    
    for location in all_location:
        parent_location = location_map.get(location.parent_id, None)
        setattr(location, 'parent', parent_location)
        if parent_location:
            sub_list = getattr(parent_location, 'sub', None)
            if sub_list:
                sub_list.append(location)
                setattr(parent_location, 'sub', sub_list)
            else:
                setattr(parent_location, 'sub', [location])
                
    for location in all_location:
        if getattr(location, 'sub', None) is None:
            # print location.__dict__
            setattr(location, 'sub', None)
    return all_location
    

class LocationTree(object):
    
    def __init__(self):
        all_location = get_location_all()
        self.location_map = create_location_id_map(all_location)
        self.location_tree = build_tree(all_location, self.location_map)
        
    def rebuild_location_tree(self):
        all_location = get_location_all()
        self.location_map = create_location_id_map(all_location)
        self.location_tree = build_tree(all_location, self.location_map)
    
    def get_path_by_location_id(self, location_id):
        location_node = self.location_map.get(location_id)
        if not location_node:
            return []
        path = [location_node]
        while True:
            if location_node.parent:
                path.append(location_node.parent)
                location_node = location_node.parent
            else:
                break
        path.reverse()
        return path
    
    def get_path_name_by_location_id(self, location_id):
        location_node = self.location_map.get(location_id)
        if not location_node:
            return []
        path = [location_node]
        while True:
            if location_node.parent:
                path.append(location_node.parent)
                location_node = location_node.parent
            else:
                break
        path.reverse()
        return [p.name for p in path]

    def get_location_sub(self, location_id):
        
        location = self.location_map.get(location_id)
        if not location:
            return []
        sub = [location]
        get_sub_location_by_tree(location.sub, sub)
    
        return sub
    
    def get_location_sub_by_size(self, location_id, size):
        location = self.location_map.get(location_id)
        if not location:
            return []
        sub = [location]
        get_sub_location_by_tree(location.sub, sub, size)
    
        return sub
    
    def get_country_node(self, country):
        country_node = []
        for location_id, location in self.location_map.iteritems():
            if location.country == country:
                country_node.append(location)
        return country_node
    
    def search_parent_id_level_name(self, country, parent_id=None, location_id=None, level=None, name=None):
        country_node = self.get_country_node(country)
        result = []
        for node in country_node:
            if self._batch_fit_attr(node, **{'parent_id': parent_id, 'location_id': location_id,
                                             'level': level, 'name': name}):
                result.append(node)
                
        return result
    
    @staticmethod
    def _fit_attr(node, attr_name, attr_value):
        if not attr_value:
            return True
        if getattr(node, attr_name, None) == attr_value:
            return True
        return False
    
    def _batch_fit_attr(self, node, **attr_dict):
        for attr_name, attr_value in attr_dict.iteritems():
            if not self._fit_attr(node, attr_name, attr_value):
                return False
        return True
    

location_tree = LocationTree()
schedule.every(5).minutes.do(location_tree.rebuild_location_tree)


def run_schedule():
    while True:
        schedule.run_pending()
    
run_schedule = run_schedule()

主要功能点:

  • 构建树结构,通过python setattr, getattr 功能,在原有的DB结构体增加parent, sub的属性。遍历得到location_id 和对应节点的功能。
  • 定时刷新功能。关于这个刷新有点无奈,后面讨论。
  • 其中的一些查询和业务逻辑就不用太多说明了。对于search 功能原本是想不符合就从所有的国家node中删除,结果性能太差了,还是筛选添加更加迅速,查了一下remove的时间复杂度是O(n),并不是O(1)。挂不得这么慢,只能告辞了。

更新时机

脑补了几个方案,最终选择了最粗狂,误差较大的方式实现。

使用DB 表的update_time.

SELECT 
    *
FROM 
    `information_schema`.`TABLES` 
WHERE 
    `information_schema`.`TABLES`.`TABLE_SCHEMA` = 'xxx' 
AND
    `information_schema`.`TABLES`.`TABLE_NAME` = 'xxx';

这种形式,会返回最新的更新时间。我们可以记下当前的更新时间,然后while true 间隔一个时间去查询,如果比较最新更新时间和原有不一致,那么重新加载location_tree。但是这个在测试环境尝试更新表数据之后,这个值没有变化,live倒是有,但是不敢用了。。资料说重启会被设置为0,但是当前肯定是没有重启,所以这个方式补靠谱。

分布式机制

在更新,删除,创建的api上添加装饰器,上报到redis,把时间戳带上,这个时候就实现了类似mysql update_time 类似的功能。这个编码上稍微复杂一丢丢,但是问题在于如果修改的入口不是这些api,那么就无法监控到数据的修改了,导致了这个方案也不那么靠谱。

定时更新

定时更新比较简单粗暴,就是间隔一定时间就去重新加载location_tree。 不用在意更新来源。但是这个就会导致数据短时间的不一致现象。存在风险。

小结

使用了location_tree 接口的响应时间从几十秒优化到了毫秒级。更新时机有待进一步探讨。关于查询可以细分需要实时和非实时两种场景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章