基于lua 脚本实现 redis 锁

2020/07/11 技术

基于lua 脚本实现 redis 锁

本文演示如何实现一个基于 lua 脚本, 并支持超时的 redis 锁. 源码见redis-lock.

锁的代码如下

import logging
import os
import random
import time
import uuid

import redis


def redis_from_url(url: str, **kwargs) -> redis.Redis:
    if "decode_responses" not in kwargs:
        kwargs["decode_responses"] = True
    return redis.from_url(url=url, **kwargs)


class RedisLockV2(object):

    def __init__(self, name, redis_db: redis.Redis, key: str, expire: int):
        """
        :param redis_db:
        :param key:
        """
        self.logger = logging.getLogger(self.__class__.__name__)
        self._name = name
        self._redis = redis_db
        self._key = key
        self._expire = expire

        #
        self._identifier: str = None  # 获取到锁的标识

        # cache
        self._ttl_lua_instance = None

    @property
    def have_lock(self) -> bool:
        """ """
        return bool(self._identifier is not None)

    def set_lock_ttl(self, ttl: int) -> bool:
        """" """

        self.logger.debug("[name {}][extend_lock][lock {}][ttl {}]extending lock...".format(self._name, self._key, ttl))
        if self.have_lock:
            if self._ttl_lua_instance is None:
                ttl_script = """
                               if redis.call("get",KEYS[1]) == ARGV[1] then
                                   return redis.call("EXPIRE", KEYS[1], ARGV[2])
                               else
                                   return 0
                               end
                               """
                self._ttl_lua_instance = self._redis.register_script(ttl_script)
            result = self._ttl_lua_instance(keys=[self._key], args=[self._identifier, ttl])
            if result:
                return True

        return False

    def acquire_lock(self, ) -> bool:
        """
        基于 Redis 实现的分布式锁
        """
        self.logger.debug("[name {}][acquire_lock][lock {}]getting lock...".format(self._name, self._key))
        identifier = str(uuid.uuid4())
        lock_timeout = int(self._expire)

        if self._redis.set(self._key, identifier, ex=lock_timeout, nx=True):
            self._identifier = identifier
            self.logger.debug("[name {}][acquire_lock][lock {}][identifier {}]success to get lock!".format(self._name, self._key, identifier))
            return True

        self._identifier = None
        return False

    def release_lock(self) -> bool:
        """
        释放锁
        :return:
        """
        self.logger.debug("[name {}][release_lock][lock {}]releasing lock...".format(self._name, self._key))
        if not self._identifier:
            return False

        unlock_script = """
        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("del",KEYS[1])
        else
            return 0
        end
        """
        unlock = self._redis.register_script(unlock_script)
        result = unlock(keys=[self._key], args=[self._identifier])

        if result:
            self.logger.debug(
                "[name {}][release_lock][lock {}][identifier {}]success to release lock!".format(self._name, self._key, self._identifier))

            # clean
            self._identifier = None

            return True
        else:
            self._identifier = None

            return False


演示代码:



class LockDemo(object):
    def __init__(self):
        self._ttl = 2
        self.host_name = os.environ.get("HOSTNAME")
        self.redis_lock = RedisLockV2(name=self.host_name, redis_db=redis_from_url("redis://redis-stream:6379/0"), key="lock", expire=self._ttl)
        self._lock_file = None

    def acquire_lock(self):
        print("[name {}][acquire_lock]acquiring lock...".format(self.host_name))
        _time_start = time.time()
        while True:
            if self.redis_lock.acquire_lock():
                break
            time.sleep(0.1)
        print("[name {}][acquire_lock][identifier {}] waited {:.3f} seconds".format(self.host_name, self.redis_lock._identifier,  time.time() - _time_start))
        print("<lock status> <{}> got".format(self.host_name))

    def release_lock(self):
        success = self.redis_lock.release_lock()
        print("[name {}][release_lock] {} to release lock".format(self.host_name, "success" if success else "fail"))
        print("<lock status> <{}> release".format(self.host_name))

    def run(self):
        """ """
        while True:
            loss_lock = bool(random.random() > 0.8)
            self.acquire_lock()
            if loss_lock:
                time.sleep(self._ttl)
                print("[name {}]lock timeout!".format(self.host_name))
                time.sleep(self._ttl * 2)
            else:
                print("[name {}]doing sth...".format(self.host_name))
                for _ in range(int(self._ttl / 0.2)):
                    time.sleep(0.2)
                    success = self.redis_lock.set_lock_ttl(ttl=self._ttl)
                    if not success:
                        print("[name {}]fail to update ttl for lock!".format(self.host_name))
                print("[name {}]release lock!".format(self.host_name))
            self.release_lock()

            time.sleep(random.randint(max(1, self._ttl - 2), max(4, self._ttl + 2)))


if __name__ == '__main__':
    LockDemo().run()

使用 docker-compose 同时启动十个进程, 执行上述代码.

docker-compose.yaml 配置为:

version: '3.5'

networks:
    network-lock:
        driver: bridge

volumes:
    data-redis:

services:
    redis-stream:
        image: redis:5.0.7-buster
        container_name: redis-stream
        hostname: redis-stream
        restart: always
        ports:
            - "127.0.0.1:6379:6379"
        command: ["redis-server", "--appendonly", "yes"]
        volumes:
            - data-redis:/data
        networks:
            - network-lock
            
    py-server:
        build: .
        image: python:3.7.6-alpine-redis
        hostname: server-${SERVER_INDEX}
        command: ["python", "-u", "python_worker.py"]
        working_dir: /app
        depends_on:
          - redis-stream
        restart: always
        volumes:
            - ./:/app
        environment:
            TZ: Asia/Shanghai
        networks:
            - network-lock

docker 镜像 python:3.7.6-alpine-redis的 Dockerfile:

FROM python:3.7.6-alpine

RUN pip3 install redis

执行指令:

for i in $(seq 1 4); do
  SERVER_INDEX=$i docker-compose up -d --scale py-server=$i --no-recreate;
  # if not work, use command below
  # echo "SERVER_INDEX=${i}" > .env && SERVER_INDEX=$i docker-compose up -d --scale py-server=$i --no-recreate
done

通过 docker-compose logs -f 可看到如下日志:

py-server_4     | [name server-4][acquire_lock]acquiring lock...
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_1     | [name server-1]release lock!
py-server_1     | [name server-1][release_lock] success to release lock
py-server_1     | <lock status> <server-1> release
py-server_3     | [name server-3][acquire_lock][identifier a8cf90a3-9ddc-4725-b029-a894ebb9e48c] waited 0.105 seconds
py-server_3     | <lock status> <server-3> got
py-server_3     | [name server-3]doing sth...
py-server_3     | [name server-3]release lock!
py-server_3     | [name server-3][release_lock] success to release lock
py-server_3     | <lock status> <server-3> release
py-server_2     | [name server-2][acquire_lock][identifier 038499c5-c7b7-4fc7-85d4-4f579acba352] waited 8.389 seconds
py-server_2     | <lock status> <server-2> got
py-server_2     | [name server-2]doing sth...
py-server_1     | [name server-1][acquire_lock]acquiring lock...
py-server_2     | [name server-2]release lock!
py-server_2     | [name server-2][release_lock] success to release lock
py-server_2     | <lock status> <server-2> release
py-server_1     | [name server-1][acquire_lock][identifier 8eb612e5-d8dc-4e15-8749-0c3df86dcafb] waited 0.207 seconds
py-server_1     | <lock status> <server-1> got
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_1     | [name server-1]lock timeout!
py-server_3     | [name server-3][acquire_lock][identifier 9d60daf1-55fe-4ffb-a19b-0b6e3df599fe] waited 1.145 seconds
py-server_3     | <lock status> <server-3> got
py-server_2     | [name server-2][acquire_lock]acquiring lock...
py-server_3     | [name server-3]lock timeout!
py-server_4     | [name server-4][acquire_lock][identifier b3c5b3df-6341-4d4c-aee5-21fcecc7ece5] waited 8.348 seconds
py-server_4     | <lock status> <server-4> got
py-server_4     | [name server-4]doing sth...
py-server_1     | [name server-1][release_lock] fail to release lock
py-server_1     | <lock status> <server-1> release
py-server_4     | [name server-4]release lock!
py-server_4     | [name server-4][release_lock] success to release lock
py-server_4     | <lock status> <server-4> release
py-server_2     | [name server-2][acquire_lock][identifier 1c5b0cc1-45fb-4aad-ac92-475cada80a6b] waited 2.183 seconds
py-server_2     | <lock status> <server-2> got
py-server_2     | [name server-2]doing sth...
py-server_1     | [name server-1][acquire_lock]acquiring lock...
py-server_3     | [name server-3][release_lock] fail to release lock
py-server_3     | <lock status> <server-3> release
py-server_2     | [name server-2]release lock!
py-server_2     | [name server-2][release_lock] success to release lock
py-server_2     | <lock status> <server-2> release
py-server_1     | [name server-1][acquire_lock][identifier 40e442bc-b50c-4ee4-bef2-dc2db0c6f993] waited 0.315 seconds
py-server_1     | <lock status> <server-1> got
py-server_1     | [name server-1]doing sth...
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_2     | [name server-2][acquire_lock]acquiring lock...
py-server_4     | [name server-4][acquire_lock]acquiring lock...
py-server_1     | [name server-1]release lock!
py-server_1     | [name server-1][release_lock] success to release lock
py-server_1     | <lock status> <server-1> release
py-server_3     | [name server-3][acquire_lock][identifier 7db7e437-e469-4a0c-aaa7-176082be8a09] waited 1.357 seconds
py-server_3     | <lock status> <server-3> got
py-server_3     | [name server-3]doing sth...
py-server_1     | [name server-1][acquire_lock]acquiring lock...
py-server_3     | [name server-3]release lock!
py-server_3     | [name server-3][release_lock] success to release lock
py-server_3     | <lock status> <server-3> release
py-server_2     | [name server-2][acquire_lock][identifier d9763bdb-8fdf-47b0-81b5-fadb7ba9eafa] waited 3.203 seconds
py-server_2     | <lock status> <server-2> got
py-server_1     | [name server-1][acquire_lock][identifier af676488-84ee-43c9-8e49-37db0ad6fa17] waited 2.059 seconds
py-server_1     | <lock status> <server-1> got
py-server_2     | [name server-2]lock timeout!
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_1     | [name server-1]lock timeout!
py-server_3     | [name server-3][acquire_lock][identifier 18fd6dc4-8226-420a-8fd0-3419f046fdff] waited 1.141 seconds
py-server_3     | <lock status> <server-3> got
py-server_3     | [name server-3]doing sth...
py-server_2     | [name server-2][release_lock] fail to release lock
py-server_2     | <lock status> <server-2> release
py-server_3     | [name server-3]release lock!
py-server_3     | [name server-3][release_lock] success to release lock
py-server_3     | <lock status> <server-3> release
py-server_4     | [name server-4][acquire_lock][identifier cceb2740-9b81-41f7-ac71-37b70237ee8c] waited 8.537 seconds
py-server_4     | <lock status> <server-4> got
py-server_4     | [name server-4]doing sth...
py-server_2     | [name server-2][acquire_lock]acquiring lock...
py-server_1     | [name server-1][release_lock] fail to release lock
py-server_1     | <lock status> <server-1> release
py-server_4     | [name server-4]release lock!
py-server_4     | [name server-4][release_lock] success to release lock
py-server_4     | <lock status> <server-4> release
py-server_2     | [name server-2][acquire_lock][identifier 9860e5fb-288f-44a5-9343-f6b8eed914cd] waited 0.310 seconds
py-server_2     | <lock status> <server-2> got
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_4     | [name server-4][acquire_lock]acquiring lock...
py-server_2     | [name server-2]lock timeout!
py-server_3     | [name server-3][acquire_lock][identifier 00bfbfda-9807-4e49-8dae-cd5f5cd69319] waited 1.152 seconds
py-server_3     | <lock status> <server-3> got
py-server_3     | [name server-3]doing sth...
py-server_1     | [name server-1][acquire_lock]acquiring lock...
py-server_3     | [name server-3]release lock!
py-server_3     | [name server-3][release_lock] success to release lock
py-server_3     | <lock status> <server-3> release
py-server_4     | [name server-4][acquire_lock][identifier a017ed5d-cd13-44de-9ae2-6f47fdb0e68c] waited 2.172 seconds
py-server_4     | <lock status> <server-4> got
py-server_4     | [name server-4]doing sth...
py-server_2     | [name server-2][release_lock] fail to release lock
py-server_2     | <lock status> <server-2> release
py-server_4     | [name server-4]release lock!
py-server_4     | [name server-4][release_lock] success to release lock
py-server_4     | <lock status> <server-4> release
py-server_1     | [name server-1][acquire_lock][identifier 59ff0d1b-d381-4d1d-89e8-5a30e0132acc] waited 2.471 seconds
py-server_1     | <lock status> <server-1> got
py-server_1     | [name server-1]doing sth...
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_1     | [name server-1]release lock!
py-server_1     | [name server-1][release_lock] success to release lock
py-server_1     | <lock status> <server-1> release
py-server_3     | [name server-3][acquire_lock][identifier 88ee4487-5978-4610-af60-c40ae1072ed1] waited 1.139 seconds
py-server_3     | <lock status> <server-3> got
py-server_3     | [name server-3]doing sth...
py-server_4     | [name server-4][acquire_lock]acquiring lock...
py-server_2     | [name server-2][acquire_lock]acquiring lock...
py-server_3     | [name server-3]release lock!
py-server_3     | [name server-3][release_lock] success to release lock
py-server_3     | <lock status> <server-3> release
py-server_4     | [name server-4][acquire_lock][identifier 800a2a8b-70ad-4abb-b8bb-c59e787bdc22] waited 1.127 seconds
py-server_4     | <lock status> <server-4> got
py-server_4     | [name server-4]doing sth...
py-server_1     | [name server-1][acquire_lock]acquiring lock...
py-server_3     | [name server-3][acquire_lock]acquiring lock...
py-server_4     | [name server-4]release lock!
py-server_4     | [name server-4][release_lock] success to release lock
py-server_4     | <lock status> <server-4> release
py-server_1     | [name server-1][acquire_lock][identifier 776fd937-6a31-44ed-9acc-b85e3bd3d5a4] waited 1.142 seconds
py-server_1     | <lock status> <server-1> got
py-server_1     | [name server-1]doing sth...
py-server_4     | [name server-4][acquire_lock]acquiring lock...
py-server_1     | [name server-1]release lock!
py-server_1     | [name server-1][release_lock] success to release lock
py-server_1     | <lock status> <server-1> release

日志中, 可能出现 A成功获取锁, B成功获取锁, A成功释放锁 的情形, 这是因为输入日志时的时间误差引起的.

Search

    Table of Contents