扫描下方海报二维码,试听课程:
(课程详细大纲,请参见文末)
===================================
本文来源:朱小厮的博客
===================================
在单实例JVM中,常见的处理并发问题的方法有很多,比如synchronized关键字进行访问控制、volatile关键字、ReentrantLock等常用方法。
但是在分布式环境中,上述方法却不能在跨JVM场景中用于处理并发问题,当业务场景需要对分布式环境中的并发问题进行处理时,需要使用分布式锁来实现。
分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
目前比较常见的分布式锁实现方案有以下几种:
- 基于数据库,如MySQL
- 基于缓存,如Redis
- 基于Zookeeper、etcd等。
在上一篇《基于数据库实现分布式锁》中介绍了如何基于数据库实现分布式锁,这里介绍一下如何使用缓存(Redis)实现分布式锁。
使用Redis实现分布式锁最简单的方案是使用命令SETNX。
SETNX(SET if Not eXist)的使用方式为:SETNX key value,只在键key不存在的情况下,将键key的值设置为value,若键key存在,则SETNX不做任何动作。
SETNX在设置成功时返回,设置失败时返回0。当要获取锁时,直接使用SETNX获取锁,当要释放锁时,使用DEL命令删除掉对应的键key即可。
上面这种方案有一个致命问题,就是某个线程在获取锁之后由于某些异常因素(比如宕机)而不能正常的执行解锁操作,那么这个锁就永远释放不掉了。
为此,我们可以为这个锁加上一个超时时间,第一时间我们会联想到Redis的EXPIRE命令(EXPIRE key seconds)。
但是这里我们不能使用EXPIRE来实现分布式锁,因为它与SETNX一起是两个操作,在这两个操作之间可能会发生异常,从而还是达不到预期的结果,示例如下:
// STEP 1SETNX key value// 若在这里(STEP1和STEP2之间)程序突然崩溃,则无法设置过期时间,将有可能无法释放锁// STEP 2EXPIRE key expireTime
对此,正确的姿势应该是使用“SET key value [EX seconds] [PX milliseconds] [NX|XX]”这个命令。
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
- EX seconds :将键的过期时间设置为 seconds 秒。执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
- PX milliseconds :将键的过期时间设置为 milliseconds 毫秒。执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
- NX :只在键不存在时, 才对键进行设置操作。执行 SET key value NX 的效果等同于执行 SETNX key value 。
- XX :只在键已经存在时, 才对键进行设置操作。
举例,我们需要创建一个分布式锁,并且设置过期时间为10s,那么可以执行以下命令:
SET lockKey lockValue EX 10 NX或者SET lockKey lockValue PX 10000 NX
注意EX和PX不能同时使用,否则会报错:ERR syntax error。
解锁的时候还是使用DEL命令来解锁。
修改之后的方案看上去很完美,但实际上还是会有问题。
试想一下,某线程A获取了锁并且设置了过期时间为10s,然后在执行业务逻辑的时候耗费了15s,此时线程A获取的锁早已被Redis的过期机制自动释放了。
在线程A获取锁并经过10s之后,改锁可能已经被其它线程获取到了。当线程A执行完业务逻辑准备解锁(DEL key)的时候,有可能删除掉的是其它线程已经获取到的锁。
所以最好的方式是在解锁时判断锁是否是自己的,我们可以在设置key的时候将value设置为一个唯一值uniqueValue(可以是随机值、UUID、或者机器号+线程号的组合、签名等)。
当解锁时,也就是删除key的时候先判断一下key对应的value是否等于先前设置的值,如果相等才能删除key,伪代码示例如下:
if uniqueKey == GET(key) { DEL key}
这里我们一眼就可以看出问题来:GET和DEL是两个分开的操作,在GET执行之后且在DEL执行之前的间隙是可能会发生异常的。
如果我们只要保证解锁的代码是原子性的就能解决问题了。
这里我们引入了一种新的方式,就是Lua脚本,示例如下:
if redis.call("get