当你开始横向扩展一个应用程序时(增加更多的服务器/实例),你可能会遇到一个需要分布式锁定的问题。这是一个花哨的术语,但其概念很简单。有时你必须确保当一个代码块在运行时(通常是在某个地方修改数据),没有其他实例运行相同的代码块。理想情况下,你可以设计你的代码不需要锁,但有时这是不可避免的。
一般来说,当你需要以原子的方式修改状态(如数据库)时,就会用到锁。一些关于你可能需要分布式锁的例子。
- 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-lock和sherlock似乎都提供了这个功能。
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应用都应该能够抓住一个成熟的现成的解决方案来解决这个问题。唯一需要解决的问题是确定需要锁的代码,以及遇到锁时应该发生什么。