在JVM同一个进程内的线程
单进程的并发场景,我们可以使用语言和类库提供的锁如Sychronized和ReentranLock,对于部分不是的场景,我们需要使用分布式锁。
分布式锁的实现方式
- Memcached分布式锁:利用Memcached的add命令,此命令是原子性操作,只有在key不存在的情况下,才能add成功。
- Redis分布式锁:类似于Memcached,redis的setnx命令也是原子性操作。
- Zookeeper分布式锁;
如何使用Redis实现分布式锁
加锁 setnx(key,1)
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁,当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
解锁 del(key)
当得到锁的线程执行完任务,需要释放锁,以便其他线程进入,释放锁的简单方式执行del指令。
锁超时 (key,30)
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。
if(setnx(key,1)==1){
expire(key,30);
try{
do something ...
}finally{
del(key)
}
}
setnx和expire
之间不是原子性操作,如果执行完setnx,节点宕机了,那么锁就永远得不到释放。在redis2.6.12提供了set指令增加了可选参数。set(key,1,30,NX)
del导致误删
如果某些原因导致线程A执行很慢,过了30秒都还没有执行完,这时候锁过期自动释放,线程B得到了锁,随后线程A执行完任务,线程A接着执行del释放锁,但这时候B还没有执行完,线程A释放的是线程B加的锁。
解决方法:在del释放锁之前加一个判断,验证当前的锁是不是自己加的,可以在加锁的时候那当前线程id当做value,并在删除之前验证key对应的value是不是自己的线程ID。
//加锁
String threadId=Thread.currentThread().getId;
set(key,threadId,30,NX);
//解锁
if(threadId.equals(redisClient.get(key))){
del(key);
}
出现并发的可能性
虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然使不完美的。我们可以获得锁的线程开启一个守护线程,用来给快要过期的锁续航。