地址查詢優化

地址查詢優化

地址服務是比較常見的服務,一般國家地址分了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 接口的響應時間從幾十秒優化到了毫秒級。更新時機有待進一步探討。關於查詢可以細分需要實時和非實時兩種場景。

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