(3)redis分布式锁有哪些缺点?
(4)redis实现分布式锁有没有现成的轮子可以使用?
简介Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
本章我们将介绍如何基于redis实现分布式锁,并把其实现的进化史从头到尾讲明白,以便大家在面试的时候能讲清楚redis分布式锁的来(忽)龙(悠)去(考)脉(官)。
实现锁的条件基于前面关于锁(分布式锁)的学习,我们知道实现锁的条件有三个:
(1)状态(共享)变量,它是有状态的,这个状态的值标识了是否已经被加锁,在ReentrantLock中是通过控制state的值实现的,在ZookeeperLock中是通过控制子节点来实现的;
(2)队列,它是用来存放排队的线程,在ReentrantLock中是通过AQS的队列实现的,在ZookeeperLock中是通过子节点的有序性实现的;
(3)唤醒,上一个线程释放锁之后唤醒下一个等待的线程,在ReentrantLock中结合AQS的队列释放时自动唤醒下一个线程,在ZookeeperLock中是通过其监听机制来实现的;
那么上面三个条件是不是必要的呢?
其实不然,实现锁的必要条件只有第一个,对共享变量的控制,如果共享变量的值为null就给他设置个值(java中可以使用CAS操作进程内共享变量),如果共享变量有值则不断重复检查其是否有值(重试),待锁内逻辑执行完毕再把共享变量的值设置回null。
说白了,只要有个地方存这个共享变量就行了,而且要保证整个系统(多个进程)内只有这一份即可。
这也是redis实现分布式锁的关键【本篇文章由公众号“彤哥读源码”原创】。
redis分布式锁进化史 进化史一——set既然上面说了实现分布式锁只需要对共享变量控制到位即可,那么redis我们怎么控制这个共享变量呢?
首先,我们知道redis的基础命令有get/set/del,通过这三个命令可以实现分布式锁吗?当然可以。
在获取锁之前先get lock_user_1看这个锁存不存在,如果不存在则再set lock_user_1 value,如果存在则等待一段时间后再重试,最后使用完成了再删除这个锁del lock_user_1即可。
但是,这种方案有个问题,如果一开始这个锁是不存在的,两个线程去同时get,这个时候返回的都是null(nil),然后这两个线程都去set,这时候就出问题了,两个线程都可以set成功,相当于两个线程都获取到同一个锁了。
所以,这种方案不可行!
进化史二——setnx上面的方案不可行的主要原因是多个线程同时set都是可以成功的,所以后来有了setnx这个命令,它是set if not exist的缩写,也就是如果不存在就set。
可以看到,当重复对同一个key进行setnx的时候,只有第一次是可以成功的。
因此,方案二就是先使用setnx lock_user_1 value命令,如果返回1则表示加锁成功,如果返回0则表示其它线程先执行成功了,那就等待一段时间后重试,最后一样使用del lock_user_1释放锁。
但是,这种方案也有个问题,如果获取锁的这个客户端断线了怎么办?这个锁不是一直都不会释放吗?是的,是这样的。
所以,这种方案也不可行!
进化史三——setnx + setex上面的方案不可行的主要原因是获取锁之后客户端断线了无法释放锁的问题,那么,我在setnx之后立马再执行setex可以吗?
答案是可以的,2.6.12之前的版本使用redis实现分布式锁大家都是这么玩的。
因此,方案三就是先使用setnx lock_user_1 value命令拿到锁,再立即使用setex lock_user_1 30 value设置过期时间,最后使用del lock_user_1释放锁。
在setnx获取到锁之后再执行setex设置过期时间,这样就很大概率地解决了获取锁之后客户端断线不会释放锁的问题。