题外话:标题来自于道格拉斯·亚当斯的经典科幻小说《银河系漫游指南》
目前越来越多的应用使用负载均衡,以往传统单体应用单机部署的情况下使用的JAVA并发处理资源竞争方式(J.U.C或synchronized等)在集群部署中已经无法保证资源的安全访问。
为什么需要分布式锁
需要考虑以下情况:
只允许一个客户端操作共享资源:这种情况下,对共享资源的操作一般是非幂等性操作。在这种情况下,如果出现多个客户端操作共享资源,就可能意味着数据不一致,数据丢失。
允许多个客户端操作共享资源:对共享资源的操作一定是幂等性操作,无论你操作多少次都不会出现不同结果。在这里使用锁,无外乎就是为了避免重复操作共享资源从而提高效率。
为了解决分布式应用中对资源的安全访问于是便有了分布式锁。
实现分布式锁一般基于Zookeeper或者Redis,前者可靠性高而后者效率高。
如果并发量不大但是追求可靠性那么可以选择Zookeeper,反之可以选择Redis。
Redis实现分布式锁
分布式锁一大特点就是排他性,临界条件下仅有获得锁的调用者才能访问资源。
Redis是基于内存设计的K-V数据库且单线程执行命令。所以使用Redis作为分布式锁的中间件具有两个重要的优势:
基于内存:加锁/解锁效率高
单线程:请求先后顺序执行没有并发冲突,所以任意时刻只有一个调用者能成功获取锁。
而且Redis支持集群部署(sentinel, cluster),保障了可用性。
可以利用Redis提供的两个命令(SETNX和DEL)实现加锁和解锁的操作。
加 锁
使用SETNX()便可以完成加锁操作。setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。
setnx key value
这样便只允许一个调用者可以访问。等等似乎少了什么?没错我们没有设置KEY的过期时间,如果此时调用者宕机这把锁就无法释放了。
我们使用EXPIRE加上过期时间,默认单位为秒。
EXPIRE key seconds
但是这样做就完了吗?SETNX跟EXPIRE是两个独立的操作,即加锁操作不具备原子性。假如加锁调用成功,但是设置过期时间的时候调用服务宕机,依然存在锁无法正常释放的问题。
于是我们还需要把这两条命令合为一条命令。
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
expiration:过期时间,EX表示秒,PX表示毫秒
NX:表示如果不存在则写入(显然我们需要这条语句)
XX:表示如果存在则写入
目前终于算是完成了原子加锁命令并且锁存在过期时间即使调用者宕机到期之后也会自动释放。
解 锁
使用DEL便可以删除锁。
DEL key [key ...]
但是事情不是这么简单的,因为:
假设调用者A加锁成功并且设置锁超时时间为30秒。但是因为某些原因导致调用者A处理业务逻辑所花费的时间超过了30秒。
锁超时自动释放之后调用者B刚好加锁成功,这时调用者A使用DEL语句释放了调用者B的锁。
因为锁被调用者A释放导致后续的调用者可以参与锁竞争,例如调用者C获取到锁。
从而发送在同一个时间内调用者B与调用者C同时运行业务逻辑从而破坏了资源的安全访问。
如图所示:
从图中可以发现A、B有并行,B、C有并行不符合我们的要求。
那么怎样才能让调用者只能释放自己占用的锁呢?这个时候K-V中的V就发挥作用了。如果我们的Value都是唯一(例如