程序员修神之路--redis作分布式锁可能不那么简单

菜菜哥,复联四上映了,要不要一块儿去看看?程序员

又想骗我电影票,对不对?web

呵呵,想去看了叫我呀redis

看来你工做不饱和呀shell

哪有,这两天我刚基于redis写了一个分布式锁,很简单数据库

无论你基于什么作分布式锁,你以为很简单吗?来来来服务器


        在计算机世界里,对于锁你们并不陌生,在现代全部的语言中几乎都提供了语言级别锁的实现,为何咱们的程序有时候会这么依赖锁呢?这个问题仍是要从计算机的发展提及,随着计算机硬件的不断升级,多核cpu,多线程,多通道等技术把计算机的计算速度大幅度提高,原来同一时间只能执行一条cpu指令的时代已通过去。随着多条cpu指令能够并行执行的缘由,原来未曾出现的资源竞争随着出现,在程序中的体现就是随处可见的多线程环境。好比要更新数据库的一个信息,若是没有并发控制,多个线程同时操做的话,就会出现互相覆盖的现象发生。网络

锁要解决的就是资源竞争的问题,也就是要把执行的指令顺序化多线程


0 1
为何须要分布式锁


        随着互联网的兴起,现代软件发生了翻天覆地的变化,之前单机的程序,已经支撑不了现代的业务。不管是在抗压,仍是在高可用等方面都须要多台计算机协同工做来解决问题。现代的互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提高,但为此,咱们就须要多解决一个分布式环境下,数据一致性的问题。并发

        当某个资源在多系统之间共享的时候,为了保证你们访问这个资源数据是一致的,那么就必需要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写有人读,你们访问到的数据就不一致了。app

        在分布式系统的时代,传统线程之间的锁机制,就没做用了,系统会有多份而且部署在不一样的机器上,这些资源已经不是在线程之间共享了,而是属于进程(服务器)之间共享的资源。

        所以,为了解决这个问题,咱们就必须引入「分布式锁」。分布式锁,是指在分布式的部署环境下,经过锁机制来让多客户端互斥的对共享资源进行访问。分布式锁的特色以下:

1
互斥性
和咱们本地锁同样互斥性是最基本,可是分布式锁须要保证在不一样节点的不一样线程的互斥。
2
可重入性
同一个节点上的同一个线程若是获取了锁以后那么也能够再次获取这个锁。
3
锁超时
和本地锁同样支持锁超时,防止死锁。
4
高效,高可用
加锁和解锁须要高效,同时也须要保证高可用防止分布式锁失效,能够增长降级。
5
支持阻塞和非阻塞
和 ReentrantLock 同样支持 lock 和 trylock 以及 tryLock(long timeOut)。


02
基于redis分布式锁


        若是你经过网络搜索分布式锁,最多的就是基于redis的了。基于redis的分布式锁得益于redis的单线程执行机制,单线程在执行上就保证了指令的顺序化,因此很大程度上下降了开发人员的思考设计成本。可是,基于redis作分布式锁难道真的这么容易吗?

1
原子操做

基于redis的分布式锁经常使用命令是

SETNX key value

        只在键 key 不存在的状况下,将键 key的值设置为value 。若键key 已经存在, 则SETNX 命令不作任何动做。SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。代码示例:

redis> SETNX redislock "redislock"    # redislock 设置成功
(integer1
redis> SETNX redislock "redislock2"   # 尝试覆盖 redislock ,失败
(integer0
redis> GET redislock                   # 没有被覆盖
"redislock"

        成功获取到锁以后,而后设置一个过时时间(这里避免了客户端down掉,锁得不到释放的问题)

redis> expire redislock 5

成功拿到锁的客户端顺利进行本身的业务,业务代码执行完,而后再删除该key

redis> DEL redislock

        若是一切都想一想象的那么顺利,程序员TMD就不用996了。假如客户端拿到锁以后,执行设置超时指令以前down掉了(现实老是那么悲剧),那这个锁就永远都释放不了.也许你会想到用 Redis 事务来解决。可是这里不行,由于 expire 是依赖于 setnx 的执行结果的,若是 setnx 没抢到锁,expire 是不该该执行的。事务里没有 if-else 分支逻辑,事务的特色是一口气执行,要么所有执行要么一个都不执行。公司几个亿的业务又被你耽误了...

        以上状况的出现是由于两个命令并不是一个原子性操做,因此在redis 2.8 版本以后出现了新的命令

SETEX key seconds value

因此如今能够利用一条原子性操做的命令来获取锁

redis> SETEX redislock 60 redislock
OK redis> GET redislock  # 值
"redislock" redis> TTL redislock  # 剩余生存时间
(integer) 49
2
超时问题

        在正常的业务当中,当一个线程获取到锁而且设置了锁的过时时间以后,会出现因为业务代码执行时间过长,锁因为到达超时时间自动释放的状况。自动释放以后,其余的线程就会获取到分布式锁,致使业务代码不会串行执行。若是业务上容许这样的状况偶尔发生,那程序员就开干吧,最后顶多人工干预一下,update 一下数据库。

        为了不这类状况发生,在使用redis分布式锁的时候,业务方应尽可能避免长时间执行的代码任务。

        若是设置锁的超时时间比较长,在必定程度上能够缓解业务代码执行时间长锁自动到期的问题,可是一旦业务代码down掉,其余等待锁的线程等待的时间会比较长,这种状况下,确保获取到锁的程序不会down 成为了主要问题。

3
获取锁失败

当锁被一个调用方获取以后,其余调用方在获取锁失败以后,是继续轮询仍是直接业务失败呢?若是是继续轮询的话,同步状况下当前线程会一直处于阻塞状态,因此这里轮询的状况仍是建议使用异步。

4
可重入性

        可重入性是指已经拥有锁的客户端再次请求加锁,若是锁支持同一个客户端重复加锁,那么这个锁就是可重入的。若是基于redis的分布式锁要想支持可重入性,须要客户端封装,可使用threadlocal存储持有锁的信息。这个封装过程会增长代码的复杂度,因此菜菜不推荐这样作。

5
redis挂了

        若是在多个客户端获取锁的过程当中,redis 挂了怎么办呢?假如一个客户端已经获取到了锁,这个时候redis挂了(假如是redis集群),其余的redis服务器会接着提供服务,这个时候其余客户端能够在新的服务器上获取到锁了,这也致使了锁意义的丢失。有兴趣的同窗能够去看看RedLock,这种方案以牺牲性能的代价解决了这个问题。

6
时钟跳跃问题

        在某些时候,redis的服务器时间发生的跳跃,因为锁的过时时间依赖于服务器时间,因此也会出现两个客户端同时获取到锁的状况发生。


当把以上问题都有解决方案了以后,基于redis的分布式锁才能够放心使用


基于redis设计简单分布式锁容易,可是设计完美分布式锁不易, 还以为基于redis的分布式锁好作吗?


相关文章
相关标签/搜索