setnx是原子操作吗_大家所推崇的 Redis 分布式锁,真的万无一失吗?

扫描下方海报二维码,试听课程:

(课程详细大纲,请参见文末)

1b4dca75edf7a153011f209640f0f3f3.png

===================================

本文来源:朱小厮的博客

===================================

在单实例JVM中,常见的处理并发问题的方法有很多,比如synchronized关键字进行访问控制、volatile关键字、ReentrantLock等常用方法。

但是在分布式环境中,上述方法却不能在跨JVM场景中用于处理并发问题,当业务场景需要对分布式环境中的并发问题进行处理时,需要使用分布式锁来实现。

分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。

目前比较常见的分布式锁实现方案有以下几种:

  1. 基于数据库,如MySQL
  2. 基于缓存,如Redis
  3. 基于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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值