Cómo utilizar Redis para implementar el bloqueo distribuido
Usar SETNX para implementar el bloqueo distribuido
Usar SETNX para implementar el bloqueo distribuido es muy simple. Por ejemplo, si el cliente desea adquirir un bloqueo llamado foo:
SETNX lock.foo
Si se devuelve 1, el cliente Se adquiere el bloqueo, el valor clave de lock.foo se establece en un valor de tiempo que indica que el valor clave se ha bloqueado y, finalmente, el cliente puede liberar el bloqueo a través de DEL lock.foo.
Si se devuelve 0 significa que el bloqueo ha sido adquirido por otro cliente, por lo que podemos volver atrás o volver a intentarlo, esperando a que otros clientes completen la operación o que el bloqueo expire.
Resolución de interbloqueos
Hay un problema con la lógica de bloqueo anterior: ¿qué debemos hacer si el cliente que mantiene el bloqueo falla o falla y no puede liberar el bloqueo? Podemos determinar si esto ha ocurrido observando la marca de tiempo correspondiente a la clave de la cerradura; si la hora actual ya es mayor que el valor de lock.foo, la cerradura falló y se puede reutilizar.
Cuando esto sucede, no puedes simplemente quitar el bloqueo mediante DEL y luego reutilizar SETNX. Cuando varios clientes detectan un tiempo de espera de bloqueo, intentarán liberarlo. Puede haber una condición de carrera aquí. Simulemos esta situación:
C0 ha agotado el tiempo de espera, pero aún mantiene el bloqueo.
Después de que C1 encuentre el tiempo de espera de bloqueo, utilizará la marca de tiempo y encontrará el tiempo de espera uno tras otro.
C1 envía DEL lock.foo
C1 envía SETNX lock.foo y tiene éxito.
C2 envía DEL lock.foo
C2 envía SETNX lock.foo y tiene éxito.
¡De esta forma, tanto C1 como C2 obtienen el candado! ¡Gran problema!
Afortunadamente, este problema se puede evitar, veamos qué hace el cliente C3:
C3 envía SETNX lock.foo para intentar adquirir el bloqueo. Dado que C0 aún retiene el bloqueo, Redis devuelve 0 a C3
C3 envía GET lock.foo para comprobar si el bloqueo ha expirado; si no, espera o vuelve a intentarlo. Si no hay tiempo de espera, espere o vuelva a intentarlo.
En cambio, si se produce un tiempo de espera, C3 intenta adquirir el bloqueo a través de:
GETSET lock.foo
Pase GETSET, C3 obtendrá una marca de tiempo, si aún se agota el tiempo de espera, significa que C3 adquirió el bloqueo como se esperaba.
Si el cliente llamado C4 realiza la operación anterior un paso antes que C3, entonces C3 obtendrá una marca de tiempo que no ha expirado. En este momento, C3 no podrá obtener el bloqueo como se esperaba y necesita. esperar o volver a intentarlo. Tenga en cuenta que incluso si C3 no adquiere el bloqueo, sobrescribirá el valor de tiempo de espera del bloqueo establecido por C4, pero el impacto de este pequeño error es insignificante.
Nota: Para que el algoritmo de bloqueo distribuido sea más robusto, el cliente que mantiene el bloqueo debe verificar si se agotó el tiempo de espera antes de desbloquearlo y luego realizar la operación DEL, porque el cliente puede estar bloqueado. debido a operaciones que requieren mucho tiempo y se cuelga, y cuando se completa la operación, debido al tiempo de espera, otra persona ha obtenido el bloqueo, entonces no es necesario desbloquear el bloqueo.
Pseudocódigo de muestra
Basado en el código anterior, escribí un pequeño fragmento de código falso para describir todo el proceso de uso de bloqueos distribuidos:
# get lock
p>bloqueo = 0
mientras bloqueo = 1:
marca de tiempo = hora actual de Unix + tiempo de espera de bloqueo + 1
bloqueo = bloqueo SETNX. foo marca de tiempo
si bloqueo == 1 o (now() > (GET lock.foo) y ahora () > (GETSET lock.foo marca de tiempo)):
descanso;
otro:
dormir(10ms)
# haz tu trabajo
do_job()
# release
if now() < GET lock.foo:
DEL lock.foo
Sí, cuando piensas en esta lógica, puedes reutilizarla Si lo usas en Python, inmediatamente pensarás en Decorator, pero cuando lo usas en Java, ¿también pensaste en esa persona? ¿AOP + anotaciones? Es sólo cuestión de no duplicar código.