一、为什么要有分布式锁
#效率: 使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。 #正确性: 加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
二、分布式锁需求分析
在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件: #1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; #2、高可用的获取锁与释放锁; #3、高性能的获取锁与释放锁; #4、具备可重入特性; #5、具备锁失效机制,防止死锁; #6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
三、分布式锁的三种实现方式
1、基于数据库实现分布式锁

#基于数据库的实现方式的核心思想是: 在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。 (1)创建一个表: DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名', `desc` varchar(255) NOT NULL COMMENT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; (2)想要执行某个方法,就使用这个方法名向表中插入数据: INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName'); 因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 (3)成功插入则获取锁,执行完成后删除对应的行数据释放锁: delete from method_lock where method_name ='methodName'; 注意:这只是使用基于数据库的一种方法,使用数据库实现分布式锁还有很多其他的玩法! 使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化: 1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换; 2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁; 3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据; 4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。 5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。
2、基于redis实现
#1、选用Redis实现分布式锁原因: (1)Redis有很高的性能; (2)Redis命令对此支持较好,实现起来比较方便 #2、使用命令介绍: (1)SETNX SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。 (2)expire expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。 (3)delete delete key:删除key 在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。 #3、实现思想: (1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 (2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 (3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

import redis import time import uuid from threading import Thread #连接redis redis_client = redis.Redis(host="localhost", port=6379, # password=password, db=10) #获取一个锁 # lock_name:锁定名称 # acquire_time: 客户端等待获取锁的时间 # time_out: 锁的超时时间 def acquire_lock(lock_name, acquire_time=10, time_out=10): """获取一个分布式锁""" identifier = str(uuid.uuid4()) #获取uuid,也就是redis的value值 end = time.time() + acquire_time #从现在加10秒 lock = "string:lock:" + lock_name # key值 while time.time() < end: #如果当前时间小于超时时间 if redis_client.setnx(lock, identifier): #当前key值不存在时 # 给锁设置超时时间, 防止进程崩溃导致其他进程无法获取锁 redis_client.expire(lock, time_out) return identifier #走到这一步,说明拿到锁了 elif not redis_client.ttl(lock): redis_client.expire(lock, time_out) time.sleep(0.001) return False #释放一个锁 def release_lock(lock_name, identifier): """通用的锁释放函数""" lock = "string:lock:" + lock_name pip = redis_client.pipeline(True) while True: try: pip.watch(lock) lock_value = redis_client.get(lock) if not lock_value: return True if lock_value.decode() == identifier: pip.multi() pip.delete(lock) pip.execute() return True pip.unwatch() break except redis.excetions.WacthcError: pass return False ----------------------------------------------- 测试 def seckill(): identifier=acquire_lock('resource') print(Thread.getName(),"获得了锁") release_lock('resource',identifier) for i in range(50): t = Thread(target=seckill) t.start()
3、基于ZooKeeper方式
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: (1)创建一个目录mylock; (2)线程A想获取锁就在mylock目录下创建临时顺序节点; (3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; (4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; (5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。 优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。