Redis 分布式锁实现与原理分析
为了保持数据一致性,锁 应运而生。
什么是分布式锁
单机锁:
我们正常跑的单机项目(也就是在tomcat下跑一个项目不配置集群)想要在高并发的时候加锁很容易就可以搞定,java提供了很多的机制例如:synchronized、volatile、ReentrantLock等锁的机制。
为什么要使用分布式锁:
当我们的项目比较庞大的时候,单机版的项目已经不能满足吞吐量的需求了,需要对项目做负载均衡,有可能还需要对项目进行解耦拆分成不同的服务,那么肯定是做成分布式的项目,分布式的项目因为是不同的程序控制,所以使用java提供的锁并不能完全保证并发需求,需要借助第三方的框架来实现对并发的阻塞控制,来满足实际业务的需要。
使用分布式锁要满足的条件
系统是一个分布式系统
关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现。
共享资源
各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL。
同步访问
即有很多个进程同事访问同一个共享资源。没有同步访问,谁管你资源竞争不竞争。
应用场景
场景一:
从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能在某一个时刻会有二笔一样的单子同时到达系统后台。
场景二:
在App中下订单的时候,点击确认之后,没反应,就又点击了几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题。
场景三:
在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。
类似这种场景,我们有很多种方法,可以使用幂等操作,也可以使用锁的操作。
我们先来解释一下什么是幂等操作:
所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。扩展一下,这里的接口,可以理解为对外发布的HTTP接口或者Thrift接口,也可以是接收消息的内部接口,甚至是一个内部方法或操作。
场景四:
管理后台的部署架构(多台tomcat服务器+redis【多台tomcat服务器访问一台redis】+mysql【多台tomcat服务器访问一台服务器上的mysql】)就满足使用分布式锁的条件。多台服务器要访问redis全局缓存的资源,如果不使用分布式锁就会出现问题。
看如下伪代码:
[code] long N = 0L; // N从redis获取值 if (N < 5) { N++; // N写回redis }
上面的代码主要实现的功能:
从redis获取值N,对数值N进行边界检查,自加1,然后N写回redis中。 这种应用场景很常见,像秒杀,全局递增ID、IP访问限制等。以IP访问限制来说,恶意攻击者可能发起无限次访问,并发量比较大,分布式环境下对N的边界检查就不可靠,因为从redis读的N可能已经是脏数据。传统的加锁的做法(如java的synchronized和Lock)也没用,因为这是分布式环境,这个同步问题的救火队员也束手无策。在这危急存亡之秋,分布式锁终于有用武之地了。
分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
这里主要讲如何用redis实现分布式锁。
使用redis的setNX命令实现分布式锁
实现原理
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。
基本命令
以下命令基于 Redis 存储类型 String使用。
SETNX key value
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
语法
[code]redis 127.0.0.1:6379> SETNX KEY_NAME VALUE
返回值
设置成功,返回 1 。 设置失败,返回 0 。
实例
[code]127.0.0.1:6379> exists job # job 不存在 (integer) 0 127.0.0.1:6379> get job # job 不存在 (nil) 127.0.0.1:6379> setnx job "programmer" # job 设置成功 (integer) 1 127.0.0.1:6379> setnx job "code-farmer" # 尝试覆盖 job ,失败 (integer) 0 127.0.0.1:6379> get job # 没有被覆盖,还是第一次设置的 value "programmer"
总结
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
所以我们使用执行下面的命令
[code]SETNX lock.foo <current Unix time + lock timeout + 1>
-
如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
-
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
Getset
Redis Getset 命令用于设置指定 key 的值,并返回 key 的旧值。
语法
redis Getset 命令基本语法如下:
[code]redis 127.0.0.1:6379> GETSET KEY_NAME VALUE
返回值
返回给定 key 的旧值。 当 key 没有旧值时,即 key 不存在时,返回 nil 。
当 key 存在但不是字符串类型时,返回一个错误。
实例
首先,设置 mykey 的值并截取字符串。
[code]127.0.0.1:6379> get mongodb # 没有值,返回 nil (nil) 127.0.0.1:6379> getset db mongodb # 没有旧值,返回 nil (nil) 127.0.0.1:6379> get db # 得到新值 "mongodb" 127.0.0.1:6379> getset db mysql # 返回旧值 mongodb,重新设置 新值 "mongodb" 127.0.0.1:6379> get db # 得到新值 "mysql"
Get
Redis Get 命令用于获取指定 key 的值。如果 key 不存在,返回 nil 。如果key 储存的值不是字符串类型,返回一个错误。
语法
redis Get 命令基本语法如下:
[code]redis 127.0.0.1:6379> GET KEY_NAME
返回值
返回 key 的值,如果 key 不存在时,返回 nil。
如果 key 不是字符串类型,那么返回一个错误。
实例
[code]# 对不存在的 key 或字符串类型 key 进行 GET 127.0.0.1:6379> get db (nil) 127.0.0.1:6379> set db redis OK 127.0.0.1:6379> get db "redis" # 对不是字符串类型的 key 进行 GET 127.0.0.1:6379> del db (integer) 1 127.0.0.1:6379> lpush db redis mongodb mysql (integer) 3 127.0.0.1:6379> get db (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> LRANGE db 0 3 1) "mysql" 2) "mongodb" 3) "redis"
解决死锁
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?
我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次(讲道理,删除锁的操作应该是锁拥有这执行的,这里只需要等它超时即可),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
C1 发送DEL lock.foo
C1 发送SETNX lock.foo 并且成功了。
C2 发送DEL lock.foo
C2 发送SETNX lock.foo 并且成功了。
这样一来,C1,C2都拿到了锁!问题大了!
幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:
C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
反之,如果已超时,C3通过下面的操作来尝试获得锁:
GETSET lock.foo <current Unix time + lock timeout + 1>
通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
注意:
为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
代码实现
常量解释
expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放
timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会
文章部分内容来源于网络,如侵联删。
感谢以下博主提供文章:
- Redis主从实现原理分析
- 利用多写Redis实现分布式锁原理与实现分析(转)
- Redis实现原理:数据同步机制分析
- [转]Redis有序集内部实现原理分析
- 利用多写Redis实现分布式锁原理与实现分析
- redis事务实现原理(源码分析)【转】
- [转]Redis有序集内部实现原理分析(二)
- 利用多写Redis实现分布式锁原理与实现分析
- Redis-LFU与LRU内部实现原理源码分析
- Redis有序集内部实现原理分析
- Redis实现分布式锁原理与实现分析
- 利用多写Redis实现分布式锁原理与实现分析
- [置顶] Spring Boot系列十二 通过redis实现Tomcat集群的Session同步及从源码分析其原理
- 利用多写Redis实现分布式锁原理与实现分析
- Redis数据库存储和过期键实现原理
- 分布式锁原理及实现方式
- Java NIO原理 图文分析及代码实现
- JavaScript中使用import 和require打包后实现原理分析
- ASP.NET 图片防盗链的实现原理分析
- hostapd wpa_supplicant madwifi详细分析(九)——wps原理及实现 一