一种方法;创建一个像这样的表:
class Queries(models.Model):
site = models.CharField(max_length=200, db_index=True)
start_time = models.DateTimeField(null = True)
finished = models.BooleanField(default=False)
这记录了每个查询何时发生,或者如果限制阻止它立即发生,则将来将会发生。 start_time 是动作开始的时间;如果该操作当前处于阻塞状态,则这是将来的情况。
我们不要考虑每秒的查询次数,而是考虑每次查询的秒数;在本例中,每个查询需要 1/3 秒。
每当要执行操作时,请执行以下操作:
- 为操作创建一行。 q = Queries.objects.create(site=sitename)
- 在您刚刚创建的对象(q.id)上,原子设置
start_time
为该站点的最大 start_time 加 1/3 秒。如果最大的是未来 10 秒,那么我们可以在 10 1/3 秒开始我们的行动。如果该时间是过去的时间,则将其固定到 now()。
- 如果刚刚设置的start_time是将来的时间,则休眠直到该时间。如果距离太远(例如超过 15 秒),请删除该行并出错。
- 查询完成后,将 finish 设置为 True,以便稍后可以清除该行。
原子行动才是重要的。您不能简单地对查询进行聚合然后保存它,因为它会发生竞争。我不知道 Django 是否可以本地执行此操作,但在原始 SQL 中这很容易:
UPDATE site_queries
SET start_time = MAX(now(), COALESCE(now(), (
SELECT MAX(start_time) + 1.0/3 FROM site_queries WHERE site = site_name
)))
WHERE id = object_id
然后,重新加载模型并在必要时休眠。您还需要清除旧行。像 Queries.objects.filter(site=site, finish=True).exclude(id=id).delete() 之类的东西可能会起作用:删除除刚刚创建的查询之外的所有已完成的查询。 (这样,你就永远不会删除latest查询,因为后面的查询需要安排该查询。)
最后,确保更新不会在事务中发生。必须打开自动提交才能使其工作。否则,更新将不是原子的:两个请求可能同时更新,并收到相同的结果。 Django 和 Python 通常会关闭自动提交,因此您需要将其打开然后关闭。对于 Postgres,这是 connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) 和 ISOLATION_LEVEL_READ_COMMITTED。我不知道如何使用 MySQL 来做到这一点。
(我认为 Python 的 DB-API 中默认关闭自动提交是一个严重的设计缺陷。)
这种方法的好处是它非常简单,状态简单;您不需要诸如事件监听器和唤醒之类的东西,它们有自己的一系列问题。
一个可能的问题是,如果用户在延迟期间取消请求,无论您是否执行该操作,延迟仍然会强制执行。如果您从未开始该操作,其他请求将不会移至未使用的“时间段”。
如果您无法让自动提交工作,解决方法是向 (site, start_time) 添加 UNIQUE 约束。 (我认为 Django 不能直接理解这一点,因此您需要自己添加约束。)然后,如果发生竞争并且对同一站点的两个请求同时结束,其中一个将引发约束您可以捕获异常,然后重试。您还可以使用普通的 Django 聚合来代替原始 SQL。不过,捕获约束异常并不那么强大。