├── LICENSE ├── main.py └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 今日长剑在握 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #coding:utf8 2 | import time 3 | import uuid 4 | import logging 5 | from collections import namedtuple 6 | 7 | import redis 8 | 9 | 10 | LockTuple = namedtuple("Lock", ("validity", "resource", "key")) 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format='%(levelname)s %(asctime)s %(filename)s Line: %(lineno)d %(message)s', 14 | datefmt='%Y-%m-%d %H:%M:%S %A', 15 | ) 16 | 17 | 18 | class ConditionException(Exception): 19 | def __init__(self, reason, *args, **kwargs): 20 | super(ConditionException, self).__init__(*args, **kwargs) 21 | if not isinstance(reason, str): 22 | reason = str(reason) 23 | self.reason = reason 24 | 25 | def __str__(self): 26 | return self.reason 27 | 28 | def __repr__(self): 29 | return self.__str__() 30 | 31 | 32 | class LockException(Exception): 33 | def __init__(self, errors, *args, **kwargs): 34 | super(LockException, self).__init__(*args, **kwargs) 35 | self.errors = errors 36 | 37 | def __str__(self): 38 | return "::".join([str(ext) for ext in self.errors]) 39 | 40 | def __repr__(self): 41 | return self.__str__() 42 | 43 | 44 | class RedLock: 45 | """ 分布式锁 46 | 基于 https://redis.io/topics/distlock/ 提出的设想实现; 47 | 参考 https://github.com/SPSCommerce/redlock-py 设计的代码实现; 48 | 具备以下优点 49 | - 互斥性 任一时刻有且只有一个客户端持有锁; 50 | - 无死锁 即使持有锁的客户端或者小部分服务器崩溃也能稳定运行; 51 | - 容错性 只要大多数服务器正常运转,就能够提供稳定的锁服务; 52 | """ 53 | def __init__(self, servers, retry=3, delay=0.2): 54 | self.release_script = """ 55 | if redis.call("get",KEYS[1]) == ARGV[1] then 56 | return redis.call("del",KEYS[1]) 57 | else 58 | return 0 59 | end""" 60 | 61 | self.retry = retry 62 | self.delay = delay 63 | self.clock_drift_factor = 0.01 64 | self.quorum = len(servers) // 2 + 1 65 | self.redis_servers = [] 66 | for ser in servers: 67 | server = redis.Redis(**ser) 68 | try: 69 | result = server.set(str(uuid.uuid4()), 1, ex=1) 70 | if result: 71 | self.redis_servers.append(server) 72 | logging.info("节点 {} 连接结果为 [{}];".format(server, result)) 73 | except Exception as ext: 74 | logging.error(ext) 75 | if self.quorum > len(self.redis_servers): 76 | ext = "哦豁!仅能连接包括 [{}] 的一小部分服务器,这种条件下无法保持锁的稳定性,请您检查;".format(self.redis_servers) 77 | raise ConditionException(ext) 78 | logging.info("创建一个拥有 {}/{} 个有效节点的 RedLock 实例;期望的节点为 {};有效节点为 {}".format(len(self.redis_servers), len(servers), servers, self.redis_servers)) 79 | 80 | @staticmethod 81 | def _lockout(server, resource, value, ttl): 82 | try: 83 | result = server.set(resource, value, nx=True, px=ttl) 84 | logging.info("服务器 [{}] 上执行锁 [{}] 操作的结果为 [{}],此时锁值为 [{}];".format(server, resource, result, value)) 85 | return result 86 | except Exception as ext: 87 | ext = "哦豁!在服务器 [{}] 上执行锁 [{}] 操作时失败,异常信息为 [{}],请您检查;".format(server, resource, str(ext)) 88 | logging.error(ext) 89 | 90 | def _release(self, server, resource, value): 91 | try: 92 | result = server.eval(self.release_script, 1, resource, value) 93 | logging.info("服务器 [{}] 上执行锁 [{}] 释放的结果为 [{}],此时锁值为 [{}];".format(server, resource, result, value)) 94 | except Exception as ext: 95 | ext = "哦豁!在服务器 [{}] 上执行锁 [{}] 释放时失败,异常信息为 [{}],请您检查;".format(server, resource, str(ext)) 96 | logging.error(ext) 97 | 98 | @staticmethod 99 | def _generator_unique_identifier(): 100 | identifier = "{}-{}".format(str(uuid.uuid4()), str(uuid.uuid4())) 101 | return identifier 102 | 103 | def locking(self, resource, ttl): 104 | retry = 0 105 | identifier = self._generator_unique_identifier() 106 | drift = int(ttl * self.clock_drift_factor) + 2 107 | errors = [] 108 | while retry < self.retry: 109 | success_lock_times = 0 110 | start = int(time.time() * 1e3) 111 | del errors[:] 112 | for server in self.redis_servers: 113 | try: 114 | if self._lockout(server, resource, identifier, ttl): 115 | success_lock_times += 1 116 | except Exception as ext: 117 | errors.append(str(ext)) 118 | elapsed = int(time.time() * 1e3) - start 119 | validity = int(ttl - elapsed - drift) 120 | if validity > 0 and success_lock_times >= self.quorum: 121 | if errors: 122 | raise LockException(errors) 123 | information = LockTuple(validity, resource, identifier) 124 | logging.info("一帆风顺,现在将锁 [{}] 交给客户端;".format(information)) 125 | return information, True 126 | else: 127 | for server in self.redis_servers: 128 | try: 129 | self._release(server, resource, identifier) 130 | except Exception as ext: 131 | logging.error(ext) 132 | retry += 1 133 | logging.info("故事曲折,即将开始第 {} 轮尝试;".format(retry + 1)) 134 | time.sleep(self.delay) 135 | logging.info("很遗憾,本次未能获得锁 [{}];".format(resource)) 136 | return LockTuple(0, resource, identifier), False 137 | 138 | def release(self, lock): 139 | _release = [] 140 | for server in self.redis_servers: 141 | result = self._release(server, lock.resource, lock.key) 142 | if result: 143 | _release.append(result) 144 | if len(_release) <= len(self.redis_servers) // 2: 145 | raise LockException("正常工作的节点数量少于一半") 146 | 147 | 148 | if __name__ == "__main__": 149 | redis_servers = [ 150 | {"host": "localhost", "port": 6379, "db": 5}, 151 | {"host": "localhost", "port": 6389, "db": 5}, 152 | {"host": "localhost", "port": 6399, "db": 5}, 153 | ] 154 | rek = RedLock(redis_servers) 155 | instance, result = rek.locking("吾身如剑", 100000) 156 | if result: 157 | rek.release(instance) 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedLock 分布式锁 2 | 3 | 基于 Redis 官方提出的设想 https://redis.io/topics/distlock/ 实现; 4 | 5 | 参考 https://github.com/SPSCommerce/redlock-py 设计的代码实现; 6 | 7 | 具备以下优点 8 | 9 | - 互斥性 任一时刻有且只有一个客户端持有锁; 10 | - 无死锁 即使持有锁的客户端或者小部分服务器崩溃也能稳定运行; 11 | - 容错性 只要大多数服务器正常运转,就能够提供稳定的锁服务; 12 | 13 | 网络上有非常多关于分布式锁具体实现的文章和案例,其中基于 Redis 和 Python 的也不少;综合多方意见 14 | 15 | 我认为 Redis 官方提出的 Redlock 是一种很有意义的分布式锁实现方式,接着参考了 SPSCommerce 团队的实现 16 | 17 | 编写了你现在看到的这段代码; 18 | 19 | 在 Redlock 的 Python 范例的基础上,我做了一些小的改动,让它更贴近实际场景; 20 | 21 | 22 | 23 | ### 稳定性 24 | 25 | RedLock 在理论上有 Redis Redlock 的支撑,同时参考了 SPSCommerce 的具体实现,我认为它是稳定的,完全可以用在生产环境当中; 26 | 27 | ### 使用方法 28 | 29 | 近段时间工作忙,还没来得及将它制作成为一个库,大家可以直接把代码复制到项目中使用,文件中有具体的调用方法; 30 | 31 | 假设你在本机启动了 3 个 Redis 服务,它们分别是 ` localhost 6379丨localhost 6389丨localhost 6399`,那么运行文件时你会在终端看到如下提示: 32 | 33 | ``` 34 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 72 节点 Redis>> 连接结果为 [True]; 35 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 72 节点 Redis>> 连接结果为 [True]; 36 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 72 节点 Redis>> 连接结果为 [True]; 37 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 78 创建一个拥有 3/3 个有效节点的 RedLock 实例;期望的节点为 [{'host': 'localhost', 'port': 6379, 'db': 5}, {'host': 'localhost', 'port': 6389, 'db': 5}, {'host': 'localhost', 'port': 6399, 'db': 5}];有效节点为 [Redis>>, Redis>>, Redis>>] 38 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 84 服务器 [Redis>>] 上执行锁 [吾身如剑] 操作的结果为 [True],此时锁值为 [91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d]; 39 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 84 服务器 [Redis>>] 上执行锁 [吾身如剑] 操作的结果为 [True],此时锁值为 [91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d]; 40 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 84 服务器 [Redis>>] 上执行锁 [吾身如剑] 操作的结果为 [True],此时锁值为 [91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d]; 41 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 124 一帆风顺,现在将锁 [Lock(validity=98998, resource='吾身如剑', key='91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d')] 交给客户端; 42 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 93 服务器 [Redis>>] 上执行锁 [吾身如剑] 释放的结果为 [1],此时锁值为 [91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d]; 43 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 93 服务器 [Redis>>] 上执行锁 [吾身如剑] 释放的结果为 [1],此时锁值为 [91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d]; 44 | INFO 2020-10-31 18:46:36 Saturday main.py Line: 93 服务器 [Redis>>] 上执行锁 [吾身如剑] 释放的结果为 [1],此时锁值为 [91e29a16-6da0-490b-b273-47942eeee623-0b6c87d4-dded-47c8-a753-7f1ff45b186d]; 45 | ``` 46 | 47 | 从运行日志可以看出,本次创建了一个拥有 3 个有效节点的 RedLock 实例。如果你填写了 3 个 Redis 服务,但实际上只有 2 个有效,那么你将会看到这样的信息: 48 | 49 | ``` 50 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 72 节点 Redis>> 连接结果为 [True]; 51 | ERROR 2020-10-31 18:50:48 Saturday main.py Line: 74 Error 61 connecting to localhost:6389. Connection refused. 52 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 72 节点 Redis>> 连接结果为 [True]; 53 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 78 创建一个拥有 2/3 个有效节点的 RedLock 实例;期望的节点为 [{'host': 'localhost', 'port': 6379, 'db': 5}, {'host': 'localhost', 'port': 6389, 'db': 5}, {'host': 'localhost', 'port': 6399, 'db': 5}];有效节点为 [Redis>>, Redis>>] 54 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 84 服务器 [Redis>>] 上执行锁 [吾身如剑] 操作的结果为 [True],此时锁值为 [d515a37b-0e9c-4327-9d44-fb74f6525cd2-4aecade4-1162-49ea-b0a9-f7968878fffb]; 55 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 84 服务器 [Redis>>] 上执行锁 [吾身如剑] 操作的结果为 [True],此时锁值为 [d515a37b-0e9c-4327-9d44-fb74f6525cd2-4aecade4-1162-49ea-b0a9-f7968878fffb]; 56 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 124 一帆风顺,现在将锁 [Lock(validity=98998, resource='吾身如剑', key='d515a37b-0e9c-4327-9d44-fb74f6525cd2-4aecade4-1162-49ea-b0a9-f7968878fffb')] 交给客户端; 57 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 93 服务器 [Redis>>] 上执行锁 [吾身如剑] 释放的结果为 [1],此时锁值为 [d515a37b-0e9c-4327-9d44-fb74f6525cd2-4aecade4-1162-49ea-b0a9-f7968878fffb]; 58 | INFO 2020-10-31 18:50:48 Saturday main.py Line: 93 服务器 [Redis>>] 上执行锁 [吾身如剑] 释放的结果为 [1],此时锁值为 [d515a37b-0e9c-4327-9d44-fb74f6525cd2-4aecade4-1162-49ea-b0a9-f7968878fffb]; 59 | ``` 60 | 61 | 3 个节点中只有 2 个有效,另外 1 个连接失败(可以想象是服务崩溃或者服务停止),这时候 RedLock 仍然能够正常工作,这是因为 Redis 服务列表中大多数服务是正常的; 62 | 63 | 我们来看一个反例,如果3 个节点中只有 1 个有效,另外 2 个连接失败,会发生什么: 64 | ``` 65 | INFO 2020-10-31 18:53:27 Saturday main.py Line: 72 节点 Redis>> 连接结果为 [True]; 66 | ERROR 2020-10-31 18:53:27 Saturday main.py Line: 74 Error 61 connecting to localhost:6389. Connection refused. 67 | ERROR 2020-10-31 18:53:27 Saturday main.py Line: 74 Error 61 connecting to localhost:6399. Connection refused. 68 | Traceback (most recent call last): 69 | File "/Users/vansen/Documents/GitHub/RedLock/main.py", line 155, in 70 | rek = RedLock(redis_servers) 71 | File "/Users/vansen/Documents/GitHub/RedLock/main.py", line 77, in __init__ 72 | raise ConditionException(ext) 73 | __main__.ConditionException: 哦豁!仅能连接包括 [[Redis>>]] 的一小部分服务器,这种条件下无法保持锁的稳定性,请您检查; 74 | ``` 75 | 76 | 运行日志中给出了明确的信息【一小部分服务器,这种条件下无法保持锁的稳定性】,于是它便不提供服务; 77 | 78 | 这是 RedLock 面对不同服务器场景的反应,阁下觉得如何? 79 | 80 | ### 当前版本号 81 | 82 | ``` 83 | v0.1 84 | ``` 85 | 86 | ### 开发日志 87 | 88 | ``` 89 | [2020-10-31] [v0.1] 已经具备完整功能,可用于生产环境; 90 | ``` 91 | 92 | 93 | ### 待办事项 94 | 95 | - 单元测试 96 | - 打包 97 | 98 | --------------------------------------------------------------------------------