分布式锁
- 基于mysql实现分布式锁
- 1.1.创建一张存放锁标志的表格:
- 1.2 获取锁
- 1.3 释放锁
- 1.4 mysql实现的分布式锁的特性
- 基于zookeeper实现分布式锁
基于mysql实现分布式锁
1.1.创建一张存放锁标志的表格:
CREATE TABLE `database_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`lock` varchar(564)NOT NULL COMMENT '锁名称',
`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_lock` (`lock`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
1.2 获取锁
在表database_lock中,lock字段做了唯一性约束,这样如果有多个请求同时提交到数据库的话,数据库可以保证只有一个操作可以成功(其它的会报错:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_idx_lock’),那么那么我们就可以认为操作成功的那个请求获得了锁。 这利用了mysql的唯一键约束特性.
1.3 释放锁
DELETE FROM database_lock WHERE locak=’ ';//传入锁名称即可
1.4 mysql实现的分布式锁的特性
1.这种锁没有失效时间,一旦释放锁的操作失败就会导致锁记录一直在数据库中,其它线程无法获得锁。这个缺陷也很好解决,比如可以做一个定时任务去定时清理。
2.这种锁的可靠性依赖于数据库。建议设置备库,避免单点,进一步提高可靠性。
3.这种锁是非阻塞的,因为插入数据失败之后会直接报错,想要获得锁就需要再次操作。如果需要阻塞式的,可以弄个for循环、while循环之类的,直至INSERT成功再返回。
4…这种锁也是非可重入的,因为同一个线程在没有释放锁之前无法再次获得锁,因为数据库中已经存在同一份记录了。想要实现可重入锁,可以在数据库中添加一些字段,比如获得锁的主机信息、线程信息等,那么在再次获得锁的时候可以先查询数据,如果当前的主机信息和线程信息等能被查到的话,可以直接把锁分配给它。
基于zookeeper实现分布式锁
- 在获取分布式锁的时候在locker节点下创建临时顺序节点
- 然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。判断序列号数字是不是自己最小,是的话表示自己拿到了锁。如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,
- 此时客户端需要找到比自己小的那个紧挨着的节点(按照节点序号排序),然后对其调用exist()方法,同时对其注册事件监听器。之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如皋是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个。
- 还有一种方式比较简单,就是不做任何监听, 尝试去获取锁的客户端依旧是创建临时序列节点判断自己是不是最小的序列号,如果不是就删除刚才创建的节点,并返回结果。 这种写法有个弊端就是不能持续监听,如果想接着获取锁就要重新调用获取锁的代码,这就需要自己在外面封装一层while true. 下面是一个简单的demo
import time
import os
from kazoo.exceptions import NoNodeError
from core import LOGGER as _logger
from kazoo.client import KazooClient
class ZkError(Exception):
"""自定义异常类"""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class ZKClient(object):
def __init__(self, ip, port=2181, work_dir="/otter/canal/destinations"):
# work_dir 是zookeeper存储的canal的任务列表路径
self.ip = ip
self.port = port
self._client = None
self.work_dir = work_dir
self.lock_path = "/locks/lock-"
def connect(self):
# 连接到ZooKeeper服务器
try:
client = KazooClient(hosts="{ip}:{port}".format(ip=self.ip, port=self.port))
client.start(timeout=15)
self._client = client
except Exception as e:
raise ZkError("[ZK_Client] zookeeper connect error=%s" % e)
def _ls_nodes(self, path):
# 获取指定路径下的子节点
try:
children = self._client.get_children(path)
return children
except Exception as e:
if isinstance(e, NoNodeError):
# 路径不存在
raise ZkError("[ZK_Client] zookeeper path=%s not exist" % path)
else:
# 其他错误
raise ZkError("[ZK_Client] zookeeper path=%s,error=%s" % (path, e))
def close(self):
if self._client:
self._client.stop()
self._client.close()
def _create_temporary_seq_node(self):
#
assert isinstance(self._client,KazooClient)
re = self._client.create(self.lock_path,makepath=True,ephemeral=True,sequence=True)
print("创建临时序列节点成功,res=%s" % re)
return re
def delete_node(self,path):
assert isinstance(self._client,KazooClient)
self._client.delete(path,recursive=True)
def cluster_lock(self, flag):
assert isinstance(self._client,KazooClient)
# self._client.acquire_lock()
print("%s->尝试获取锁" % flag)
seq_path = self._create_temporary_seq_node()
print("seq_path=%s" % seq_path)
childs = self._ls_nodes("/locks")
# flag = self.last_node(seq_path,childs)
my_id = int(seq_path[self.lock_path.index("-") + 1:])
min_id = -1
for child in childs:
child_id = int(child.split("-")[1])
if min_id == -1 or child_id < min_id:
min_id = child_id
if my_id == min_id:
print("%s->尝试获取锁success" % flag)
return True
else:
print("%s->尝试获取锁失败,删除node=" % flag,seq_path)
self._client.delete(seq_path)
return False
zk_A = ZKClient("ip",2181)
zk_B = ZKClient("ip",2181)
zk_C = ZKClient("ip",2181)
zk_A.connect()
zk_B.connect()
zk_C.connect()
i = 1
while True:
if i<= 2:
lock_A=zk_A.cluster_lock("A")
if i ==2:
# 此时A断开意味着A释放了锁,则B应该会获取到锁
print("客户端A断开连接")
zk_A.close()
lock_B=zk_B.cluster_lock("B")
lock_C=zk_C.cluster_lock("C")
time.sleep(5)
i=i+1