分布式锁比较Redis,MySQL,Zookeeper

Redis分布式锁
【分布式锁】
分布式场景中的数据一致性问题一直是一个比较重要的话题,其中的核心就是分布式锁。在大多数系统设计时我们一般会牺牲掉强一致性来保证数据的最终一致性,这需要我们合理地使用分布式锁和分布式事务。
一个合格的分布式锁需要做到多客户端互斥、安全(谁持有该锁谁才能删除)、避免死锁(客户端未能成功释放时其它客户端可以获取该锁)、容错(服务器部分节点故障时客户端仍能正常获取和释放锁)。
【常见的分布式锁】
当前比较常见的分布式锁主要有基于Redis分布式锁、基于zookeeper分布式锁以及数据库乐观锁。
基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

这里也给出一个适合作为锁表的ddl语句,供大家自行实验:

CREATE TABLE `method_lock` (
    `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `method_name` VARCHAR(64) NOT NULL COMMENT '方法名',
    `method_desc` VARCHAR(1024) NOT NULL COMMENT '方法描述',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE INDEX `uniq_method_name` (`method_name`)
)
COMMENT='分布式锁'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;

如果这个过程中出现了生成raw的线程出现问题,那么需要专门的定时任务进行过期数据的清理。我们可以在update_time中设置它的过期时间。

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
与MySQL的方案相比,它具备了可重入、可阻塞的特性。
【Redis的set ex与setnx】
在redis中我们可以使用setnx来进行分布式的任务抢夺,谁先使用setnx成功,则谁拿到锁,其余的就会失败。

在使用完该锁之后需要对其进行del(注意它可以被其他客户端del掉),但如果由于某些原因如逻辑异常,没有执行到del,那么这个锁就一直不会被释放。
一般情况下,在setnx时也会直接对其设置过期时间expire,但需要注意这两个操作并不是原子操作,所以当setnx成功而执行expire之前客户端挂掉,那么expire也不会得到执行。
所以在Redis2.8之后,可以使用如下原子操作给锁添加过期时间:set key value ex timeout nx

这里有一个小细节需要注意,就是使用redis时,key值设定还是需要有一定的规范,否则需要手动处理这些key-value的时候,一定是不知所措的。比如这里的分布式锁,我们就可以像这样使用lock:xxx的形式,一看key就明白用途了,这一点也有不少规范强调过。
关于set ex nx这个指令,其实我们可以使用lua脚本对两个操作进行原子化设置也能达到同样的效果,很多第三方包装的API中就是使用了这种思路。
【包装set成为可重入锁】
一个锁允许相同的线程在持有锁的情况下再次执行加锁操作,那么这个锁就是可重入锁。比较典型的场景就是,在一个线程中执行一个加锁的递归方法,所以可重入锁也被称为递归锁。
如果想把Redis的set作为可重入锁使用,直接使用上述的指令是不可行的,需要再次进行包装。

【谁拿到的锁,谁来释放】

对于分布式锁,如果只是用key去进行对比,还是很危险,因为不同的客户端有可能会因为代码问题即使抢不到也能够删除。所以拿到锁时,我们可以设置一个value,这个value始终只有单一的客户端持有这个值,在删除这个锁时对值进行一次对比,对上了再删除。另外,为了防止客户端挂掉导致别的客户端都拿不到锁,我们最好是设置一个过期时间。

【参考】
1.《Redis深度历险 核心原理与应用实践》
2.https://www.cnblogs.com/liuqingzheng/p/11080501.html

原文地址:https://www.cnblogs.com/bruceChan0018/p/15646245.html