地址查询优化
地址服务是比较常见的服务,一般国家地址分了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 接口的响应时间从几十秒优化到了毫秒级。更新时机有待进一步探讨。关于查询可以细分需要实时和非实时两种场景。