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