
迟早,开发人员都面临着删除不必要数据的任务。 服务越复杂,您需要考虑的细微差别就越多。 在本文中,我将描述如何在具有数百个链接的数据库中实现“删除”。
背景知识
为了监视大多数项目的可操作性,
Mail.ru Group和
VKontakte使用了自己开发的服务
-Monitoring 。 从2012年底开始其历史,经过6年的发展,该项目已发展成为一个庞大的系统,并获得了许多功能。 监视定期检查服务器的可用性和对请求的响应的正确性,收集有关已用内存,CPU利用率的统计信息。 当受监视服务器的参数超过允许值时,负责服务器的参数将在系统中通过SMS接收通知。
记录所有检查和事件以跟踪服务器性能的动态,因此数据库已达到数亿条记录的顺序。 新服务器定期出现,而旧服务器不再使用。 必须从监视系统中删除有关未使用服务器的信息,以便:
a)避免不必要的信息使接口过载,并且
b)释放唯一标识符 。
删掉
我明知在文章的标题中,“删除”一词用引号引起来。 有几种方法可以从系统中删除对象:
- 从数据库中完全删除;
- 将对象标记为已删除并从界面隐藏。 作为标记,可以使用布尔值或DateTime进行更准确的记录。
迭代1
最初,当我们简单地执行
object.delete()
并删除具有所有依赖项的对象时,使用第一种方法。 但是随着时间的流逝,我们不得不放弃这种方法,因为一个对象可能与数百万个其他对象具有依赖性,并且级联删除会严格阻止表。 而且由于该服务每秒执行数千次检查并将其记录下来,因此阻塞表导致该服务严重降低速度,这对我们来说是无法接受的。
迭代2
为了避免长时间锁定,我们决定分批删除数据。 这将允许在删除对象之间的间隔中记录实际的监视数据。 删除对象时(确认删除时),可以通过管理面板中使用的方法来获取将要级联删除的所有对象的列表:
from django.contrib.admin.util import NestedObjects from django.db import DEFAULT_DB_ALIAS collector = NestedObjects(using=DEFAULT_DB_ALIAS) collector.collect([obj]) objects_to_delete = collector.nested()
情况有所改善:随着时间的推移分配了负载,开始更快地记录新数据。 但是我们立即遇到了下一个陷阱。 事实是,要删除的对象列表是在删除的最开始就形成的,如果在“部分”删除过程中添加了新的从属对象,则不能删除父元素。
我们立即放弃了递归删除错误的想法,以再次收集有关新依赖项的数据或在删除时禁止添加依赖项记录,因为
a)您可以进入无限循环,或者
b)您必须在整个代码中找到所有依赖项 。
迭代3
我们考虑了第二种删除方式,即在界面上标记和隐藏数据时。 最初,这种方法被拒绝了,因为这似乎要花至少一周的时间来查找所有查询并为缺少删除的父项添加过滤器。 另外,很可能会丢失必要的代码,这将导致不可预测的后果。
然后,我们决定使用装饰器来覆盖查询管理器。 此外,看代码比写一百个单词更好。
def exclude_objects_for_deleted_hosts(*fields): """ Decorator that adds .exclude({field__}is_deleted=True) for model_class.objects.get_queryset :param fields: fields for exclude condition """ def wrapper(model_class): def apply_filters(qs): for field in filter_fields: qs = qs.exclude(**{ '{}is_deleted'.format('{}__'.format(field) if field else ''): True, }) return qs model_class.all_objects = copy.deepcopy(model_class.objects) filter_fields = set(fields) get_queryset = model_class.objects.get_queryset model_class.objects.get_queryset = lambda: apply_filters(get_queryset())
字段模型的指定字段的
exclude_objects_for_deleted_hosts(fields)
修饰器会为每个请求自动添加一个
exclude
过滤器,该过滤器只会删除不应在界面中显示的条目。
现在,对于所有因删除而受到某种影响的模型而言,添加一个装饰器就足够了:
@exclude_objects_for_deleted_hosts('host') class Alias(models.Model): host = models.ForeignKey(to=Host, verbose_name='Host', related_name='alias')
现在,为了删除
Host
对象,只需更改
is_deleted
属性:
host.is_deleted = True
所有查询将自动排除引用远程对象的记录:
原来是以下SQL查询:
SELECT monitoring_checkertoalias.id FROM monitoring_checkertoalias INNER JOIN monitoring_checker ON (`monitoring_checkertoalias`.`checker_id` = monitoring_checker.`id`) INNER JOIN Hosts ON (`monitoring_checker`.`monhost_id` = Hosts.`id`) INNER JOIN dcmap_alias ON (`monitoring_checkertoalias`.`alias_id` = dcmap_alias.`id`) INNER JOIN Hosts T5 ON (`dcmap_alias`.`host_id` = T5.`id`) WHERE ( NOT (`Hosts`.`is_deleted` = TRUE)
如您所见,装饰器中指定的字段的其他联接以及对
`is_deleted` = TRUE
检查
`is_deleted` = TRUE
添加到请求中。
关于数字的一点
逻辑上,其他联接和条件会增加查询执行时间。 对这个问题的研究表明,“复杂性”的程度取决于数据库的结构,记录的数量和索引的存在。
具体来说,在我们的案例中,对于每个依赖级别,该请求都会被罚款约30%。 这是我们在具有数百万条记录的最大表上获得的最大惩罚;在较小的表上,惩罚被减少到百分之几。 幸运的是,我们已经配置了必要的索引,并且对于大多数关键查询而言,已经有了必要的联接,因此我们在性能上并没有太大的区别。
唯一标识符
在删除数据之前,可能有必要释放计划在将来使用的标识符,因为这会在创建新对象时引起非唯一错误。 尽管没有对象在Django应用程序中可见,但它们仍将存在于数据库中。 因此,对于已删除的对象,我们将uuid附加到标识符。
host.hostname = '{}_{}'.format(host.hostname, uuid.uuid4()) host.is_deleted = True host.save()
运作方式
对于每个新模型或依赖项,必要时需要更新装饰器。 为了简化对依赖模型的搜索,我们编写了一个“智能”测试:
def test_deleted_host_decorator_for_models(self): def recursive_host_finder(model, cache, path, filters):
该测试以递归方式检查所有模型是否存在要删除的模型的依赖项,然后查看是否已为此模型设置了必需字段的装饰器。 如果缺少某些内容,则测试会巧妙地告诉您在哪里添加装饰器。
结语
因此,借助装饰器,可以实现对具有大量依赖关系的数据的“小删除”。 所有请求都会自动收到所需的
exclude
过滤器。 施加附加条件会减慢数据获取的过程,“复杂”程度取决于数据库的结构,记录数和索引的可用性。 建议的测试将告诉您需要为哪些模型添加装饰器,并在将来监视其一致性。