分布式环境下,多台机器上多个线程对同一份数据进行操作如果不做处理防范,就有可能出现类似『数据重复入库』、『商品超卖』等情况,在这种情况下需要分布式锁来对分布式环境下临界资源做互斥。
那么如何来做呢?
想想单机多线程的时候我们是如何保证互斥的,单机多线程的情况下所有的线程都去尝试获取同一把锁。换到分布式环境下我们把局限于一台机器内的锁提出来,其实就是利用一个大家都能访问的公共资源来实现分布式锁。这个公共资源可以是redis,可以是zookeeper,也可以是其他的某个资源。这里我们主要考虑用redis的setnx来实现。
设计一个分布式锁主要考虑以下几点:
互斥
分布式系统中运行着多个节点,必须确保在同一时刻只能有一个节点的一个线程获得锁,这是最基本的。
死锁
用Redis来实现分布式锁最简单的方式就是在实例里用setnx命令来创建一个键值,当一个节点想要释放锁时,它只需要删除这个键值即可。
上述方案看上去很美好,在分布式环境里,当一个节点获取到锁之后宕机了,或者因为网络不通无法执行释放锁的操作,则其他节点都无法申请到锁。
(1)锁的时效性
所以这里要加一个锁的时效性,避免单点故障造成死锁,但同时也要保证一旦一个节点获取到锁,在节点存活时不能被其他节点解锁。
(2)避免误删锁
Case:一个节点获取到锁开始执行业务,不幸被某个操作阻塞了很长时间,过了超时时间后自动释放了这个锁,过了一段时间这个节点执行完之后又尝试删除这个其实已经被其他节点拿到的锁。所以简单粗暴的直接删除有可能造成一个节点删除了其他节点的锁。
为了避免误删可以给锁加一个节点ID+线程ID作为签名的属性(用hset命令),这样每个锁就只能被获取到锁的节点删除。
另外还可以给每个获取锁的线程设定一个TimerTask,在线程执行任务的过程中定时的延长锁的时效时间,这样就不会因为执行时间超过了超时时间导致锁自动释放,也就是续租锁。但为了提升性能应尽量避免加锁时间太长,尽量减少锁的粒度,如下面一条所说。
锁的性能
(1)加锁的事务或者操作尽量粒度小,减少其他节点申请锁的等待时间,提高处理效率和并发性。
(2)降低获取锁的频率,尽量减少Redis压力。让节点申请锁的时候有一个等待时间,而不是不停的循环尝试获取锁。
(3)持锁的客户端解锁后,要能通知到其他等待锁的节点,而不是隔一段时间尝试获取一次锁。
针对上面后两条,Redisson采用Semaphore及Redis的订阅/发布消息来控制,通过订阅/发布可以避免空转。
如果锁当前是被占用的,那么等待释放锁的消息,当锁释放并发布释放锁的消息后,信号量的 release() 方法会被调用,此时被信号量阻塞的等待队列中的一个线程就可以继续尝试获取锁了。
此处虽然Semaphore只是进程内部粒度的锁的,但是也可以一定程度减轻Redis节点的压力,因为每个节点上的请求数量少了,Redis的压力也就少了。
锁重入
可重入锁,即在已经获取锁的基础之上,再次获取当前的锁。在释放时,之前获取了N次,那么也需要相应的unlock N次,才表示最终锁被成功释放。
分布式锁是否需要设计为可重入的要根据具体的业务来判断。通过上文所说的锁的签名可以判断当前线程是否已经获得锁,如果已经获得则count增加。
最后
是否采用分布式锁及最终锁的实现方案还是要看具体的业务需求。
推荐看一下Redis官方文章:Distributed locks with Redis
中文翻译:用Redis构建分布式锁
另外,Redisson实现了Redis官方提出的RedLock算法