Django中的分布式锁定教程

460 阅读3分钟

当你开始横向扩展一个应用程序时(增加更多的服务器/实例),你可能会遇到一个需要分布式锁定的问题。这是一个花哨的术语,但其概念很简单。有时你必须确保当一个代码块在运行时(通常是在某个地方修改数据),没有其他实例运行相同的代码块。理想情况下,你可以设计你的代码不需要锁,但有时这是不可避免的。

一般来说,当你需要以原子的方式修改状态(如数据库)时,就会用到锁。一些关于你可能需要分布式锁的例子。

  • Cron作业/计划任务,其运行时间可能超过触发它们的时间间隔
  • 回写缓存冲到数据库中。
  • 批量处理文件

如果在多个服务器上同时运行代码会导致数据的损坏或重复,你可能需要使用分布式锁。有关的代码将在执行前获得锁。一旦它获得了锁,任何其他试图获取锁的行为都会失败。

实现方法

锁定数据必须存储在一个所有应用程序实例都能访问的位置。如果这是一个标准的Django网站,你可能已经有了两个这样的系统,缓存和数据库。锁定的棘手之处在于,它必须是原子性的,以避免在两个进程同时试图获取锁的时候出现竞赛条件。值得庆幸的是,在Django使用的标准缓存和数据库后端中,这个问题已经解决了。对于这些后端,你可以找到第三方库,它们在Python中暴露了这些功能。

Redis

SETNX是用于锁定的Redis基元。该 django-redis包为这个功能提供了一个上下文管理器。

from django.core.cache import cache

with cache.lock("somekey"):
    do_some_thing()

Memcached

Memcached的 ADD的工作原理类似于Redis的SETNX 。我对任何围绕这个实现上下文管理器的库都没有经验,但django-cache-locksherlock似乎都提供了这个功能。

Postgres

Postgres有一个 pg_advisory_lock函数,该函数被django-pglocks所利用。

from django_pglocks import advisory_lock

with advisory_lock("somekey"):
    do_some_thing()

MySQL

MySQL提供了一个 GET_LOCK函数,用于分布式锁。它通过django-mysql库中的一个上下文管理器暴露出来。

from django_mysql.locks import Lock

with Lock("somekey"):
    do_some_thing()

其他考虑因素

超时

虽然上下文管理程序应该在退出时释放锁,但总是有可能在这之前你的应用程序就崩溃了。如果不给锁设置超时,它将永远被保留,并阻止任何类似代码的运行。请参考你选择的库的文档,看看如何指定超时。你应该把这个值设置得比代码执行的时间长一些,但又足够短,不会阻止连续的运行。

遇到一个锁

当你的应用程序遇到一个已经被获取的锁时,它所做的事情将根据它的要求来确定。大多数实现会在无法获得锁的情况下抛出一个异常,所以你通常会把这段代码包在一个try/except 。在异常情况下做什么的可能性包括。

  • 稍后重试
  • 等待锁被释放
  • 记录一个错误
  • 什么都不做

数据库行锁定

如果你的代码需要独占性地修改数据库中的特定行,并且你想在代码执行过程中防止对这些行的任何其他修改,那么数据库行锁可能是一个更好的选择。Django提供了以下功能 select_for_updatequeryset方法。


分布式锁定听起来是一个困难的技术问题,但一般来说,这是一个已经解决的问题。任何Django应用都应该能够抓住一个成熟的现成的解决方案来解决这个问题。唯一需要解决的问题是确定需要锁的代码,以及遇到锁时应该发生什么。