什么是分布式锁?
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码
段。线程锁只在同一
JVM
中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如
synchronized是共享对象头,显示锁
Lock
是共享某个变(
state
)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程
的资源,因此无法通过
synchronized
等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问
使用分布式锁要满足的几个条件:
1. 系统是一个分布式系统(关键是分布式,单机的可以使用
ReentrantLock
或者
synchronized
代码块来实现)
2. 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者
NoSQL
)
3. 同步访问(即有很多个进程同时访问同一个共享资源。)
应用的场景
线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决
这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
有这样一个情境,线程A
和线程
B
都共享某个变量
X
。
如果是单机情况下(单JVM
),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM
),线程
A
和线程
B
很可能不是在同一
JVM
中,这样线程锁就无法起到作用了,这时候
就要用到分布式锁来解决。
分布式锁可以基于很多种方式实现,比如zookeeper
、
redis...
。不管哪种方式,他的基本原理是不变的:用一
个状态值表示锁,对锁的占用和释放通过状态值来标识。
redis实现分布式
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对
Redis
的连接并不存在竞争
关系。
redis
的
SETNX
命令可以方便的实现分布式锁。
SETNX key value
同一时刻只能有一个进程获取到锁。setnx
将 key
的值设为
value
,当且仅当
key
不存在。
若给定的 key
已经存在,则
SETNX
不做任何动作。
设置成功,返回 1
。
设置失败,返回
0
。
通常使用以下方式进行尝试获取锁:
SETNX lock.foo
(锁的结束时间)返回1时说明获取锁成功,
可以通过DEL lock.foo 来删除锁退出。返回0时说明获取失败,此时可以选择重试循环,直到成功或者锁超时结束。
释放锁:锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;
(最简单的方式就是
del
, 如果在删除之前死锁了。)
getSET
先获取key
对应的
value
值。若不存在则返回
nil
,然后将旧的
value
更新为新的
value
。
语法:
GETSET key value
将给定 key
的值设为
value
,并返回
key
的旧值
(old value)
。
当 key
存在但不是字符串类型时,返回一个错误。
解决死锁
有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决
?
我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo
的值,说明该锁已失效,可以被重新使用。
但不能简单的去删除锁,通常删除锁是由持有锁者进行的,所以只需要等待超时即可,请求setnx lock.info time 前去获取锁失败时再发送get lock.info 查看锁是否超时,若超时了则进行getset lock.info time 如果拿到的锁仍然是超时的那就说明,拿到锁了,如果不是超时的则被别人抢先一步了。虽然修改了别人的超时时间,但一点点并不会影响什么的。
为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,
再去做
DEL
操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就
不必解锁了。