使用分布式锁要满足的几个条件:
系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)
共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)
同步访问(即有很多个进程同时访问同一个共享资源)
什么是分布式锁?
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
应用的场景
有这样一个情景,线程A和线程B都共享某个变量X。
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。
这里主要讲解如何用redis实现分布式锁
使用redis的setNX命令实现分布式锁
实现的原理
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。
基本命令解析
1)setNX (SET if Not Exists)
语法:
SETNX key value
复制
将 key的值设为value,当且仅当key不存在。
若给定的key已经存在,则SETNX不做任何动作。
SETNX是【SET if Not eXists】(如果不存在,则SET)的简写
返回值:
设置成功,返回1
设置失败,返回0
栗子:
192.168.199.128:6379> EXISTS job # job 不存在
(integer) 0
192.168.199.128:6379> SETNX job "programmer" # job 设置成功
(integer) 1
192.168.199.128:6379> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
192.168.199.128:6379> GET job # 没有被覆盖
"programmer"
复制
客户端可以尝试以下方式:
SETNX lock.foo <current Unix time + lock timeout + 1>
复制
GETSET key value
复制
同一时刻只能有一个进程获取到锁。
sentx
释放锁:
锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;
(最简单的方式就是del,如果在删除之前死锁了)
public static boolean lock(String lockName) {
Jedis jedis = RedisPool.getJedis();
//lockName可以为共享变量名,也可以为方法名,主要是用于模拟锁信息
System.out.println(Thread.currentThread() + "开始尝试加锁!");
Long result = jedis.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
if (result != null && result.intValue() == 1){
System.out.println(Thread.currentThread() + "加锁成功!");
jedis.expire(lockName, 5);
System.out.println(Thread.currentThread() + "执行业务逻辑!");
jedis.del(lockName);
return true;
} else {//判断是否死锁
String lockValueA = jedis.get(lockName);
//得到锁的过期时间,判断小于当前时间,说明已超时但是没释放锁,通过下面的操作来尝试获得锁。下面逻辑防止死锁
[已经过期但是没有释放锁的情况]
if (lockValueA != null && Long.parseLong(lockValueA) < System.currentTimeMillis()){
String lockValueB = jedis.getSet(lockName,
String.valueOf(System.currentTimeMillis() + 5000));
//这里返回的值是旧值,如果有的话。之前没有值就返回null,设置的是新超时。
if (lockValueB == null || lockValueB.equals(lockValueA)){
System.out.println(Thread.currentThread() + "加锁成功!");
jedis.expire(lockName, 5);
System.out.println(Thread.currentThread() + "执行业务逻辑!");
jedis.del(lockName);
return true;
} else {
return false;
}
} else {
return false;
}
}
}
复制