缓存redis分布式锁实现

高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。

分布式锁特点:

  • 安全性。在任意时刻,只有一个客户端可以获得锁(排他性)
  • 避免死锁。client最终一定可以获得锁,即使锁住资源的客户端在release锁之前崩溃或不可达
  • 容错性。只要锁服务集群中的大部分节点存活,Client 就可以进行加锁解锁操作。

分布式锁服务,你需要考虑下面几个设计

  • 需要给一个锁被释放的方式,以避免请求者不把锁还回来,导致死锁的问题。Redis 使用超时时间,ZooKeeper 可以依靠自身的 sessionTimeout 来删除节点。
  • 分布式锁服务应该是高可用的,而且需要持久化。可以参考 Redis 的文档 RedLock
  • 非阻塞方式的锁服务。
  • 支持锁的可重入性。

1.使用redis的setnx()、expire()方法

这个方案相对于memcached()的add()方案,redis占优势的是,其支持的数据类型更多,而memcached只支持String一种数据类型。除此之外,无论是从性能上来说,还是操作方便性来说,其实都没有太多的差异,完全看你的选择,比如公司中用哪个比较多,你就可以用哪个。

首先说明一下setnx()命令,setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。但是要注意的是setnx命令不能设置key的超时时间,只能通过expire()来对key设置。

具体的使用步骤如下:

  1. setnx(lockkey, 1) 如果返回0,则说明占位失败;如果返回1,则说明占位成功

  2. expire()命令对lockkey设置超时时间,为的是避免死锁问题。

  3. 执行完业务代码后,可以通过delete命令删除key。

这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步setnx执行成功后,在expire()命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用redis的setnx()、get()和getset()方法来实现分布式锁。

2.使用redis的setnx()、get()、getset()方法

这个方案的背景主要是在setnx()和expire()的方案上针对可能存在的死锁问题,做了一版优化。

那么先说明一下这三个命令,对于setnx()和get()这两个命令,相信不用再多说什么。那么getset()命令?这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:

  1. getset(key, “value1″) 返回nil 此时key的值会被设置为value1

  2. getset(key, “value2″) 返回value1 此时key的值会被设置为value2

  3. 依次类推!

介绍完要使用的命令后,具体的使用步骤如下:

  1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。

  2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。

  3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。

  4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

  5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

3.SET NX(推荐)

SET resource_name my_random_value NX PX 30000

4、SET EX

SETEX cd 3000 "goodbye my love"

Redis 的官方文档

  • my_random_value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志

  • NX表示只有当resource_name对应的key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁

  • PX 30000表示这个锁有一个30秒的自动过期时间。

至于解锁,为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁

if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else 
    return 0 
end

在执行这段LUA脚本的时候,KEYS[1]的值为resource_name,ARGV[1]的值为my_random_value。原理就是先获取锁对应的value值,保证和客户端传进去的my_random_value值相等,这样就能避免自己的锁被其他人释放。另外,采取Lua脚本操作保证了原子性。

例如,下面的例子演示了不区分 Client 会导致错误

  • Client A 获得了一个锁。
  • 当尝试释放锁的请求发送给 Redis 时被阻塞,没有及时到达Redis
  • 锁定时间超时,Redis 认为锁的租约到期,释放了这个锁。
  • Client B 重新申请到了这个锁。
  • Client A 的解锁请求到达,将 Client B 锁定的key解锁
  • Client C 也获得了锁。
  • Client B 和 Client C 同时持有锁。

通过执行上面脚本的方式释放锁,Client 的解锁操作只会解锁自己曾经加锁的资源,所以是安全的。

常用的四种方案

  1. 基于数据库表做乐观锁,用于分布式锁。

  2. 使用memcached的add()方法,用于分布式锁。

  3. 使用redis的setnx()、expire()方法,用于分布式锁。

  4. 使用redis的setnx()、get()、getset()方法,用于分布式锁。

  5. 使用redis的setex(),用于分布式锁

转载于:https://my.oschina.net/u/437232/blog/3012226

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值