什么是分布式锁?
与分布式锁相对应的是线程锁、进程锁。
线程锁:它主要是给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized和Lock。synchronized是java中的一个关键字,也就是说是Java语言内置的特性。Lock不是Java语言内置的,Lock是一个类。synchronized不需要用户去手动释放锁。而Lock则必须要用户去手动释放锁。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。可以用lock做线程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
实现要点
1.互斥性,同一时刻,只能有一个客户端持有锁。
2.防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。
3.加锁和释放锁必须是同一个客户端。
4.容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。
我们先看一些redis锁的方式。
代码实现
@Resource
private JedisPool jedisPool;
public static final int SLEEP_TIME = 500;
public boolean addLock(String lockkey, int maxWait) throws Exception {
long start = System.currentTimeMillis();
boolean success = true;
Jedis jedis = null;
try{
jedis = jedisPool.getResource();
System.out.println("获取锁:{}开始"+lockkey);
do {
//lockkey做锁
Long lock = jedis.incrBy(lockkey, 1L);
success = lock == 1;
if (!success) {
jedis.expire(lockkey, maxWait / 1000);
Thread.sleep(SLEEP_TIME);
}
} while (!success && System.currentTimeMillis() < start + maxWait);
}catch (Exception e) {
e.printStackTrace();
}finally {
if (jedis != null) {
jedis.close();
}
}
System.out.println("获取锁{}{},耗时{}ms"+lockkey+ (success ? "成功" : "失败")+
(System.currentTimeMillis() - start));
return success;
}
//解锁
public void unLock(String lockkey) {
long start = System.currentTimeMillis();
Jedis jedis = null;
try{
jedis = jedisPool.getResource();
jedis.del(lockkey);
}catch (Exception e) {
e.printStackTrace();
}finally {
if (jedis != null) {
jedis.close();
}
}
System.out.println("释放锁{}成功,耗时{}ms"+lockkey+(System.currentTimeMillis() - start));
}
测试;
@Test
public void getRedisLock() {
try {
System.out.println("#################开始加锁");
redisController.addLock(redislockKey,30 * 1000);
System.out.println("执行代码。。。。");
}catch (Exception e){
System.out.println("#################加锁失败");
e.printStackTrace();
}finally {
try {
redisController.unLock(redislockKey);
System.out.println("#################释放锁成功");
} catch (Exception e) {
System.out.println("#################释放锁失败"+e);
}
}
}
上面的这种式是肯定有问题,它并没有给自己一个时间限定,一旦死锁只能是别的哭护短解锁。加锁和释放锁必须是同一个客户端。锁不具备拥有者标识,即任何客户端都可以解锁。而且这个加锁代码逻辑也是很复杂。
还有就是上面的解锁方式最常见的就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。
还有一种和上面代码差不多的加锁方式。
public boolean addLock2(Jedis jedis,String lockkey, int maxWait) throws Exception {
long expires = System.currentTimeMillis() + maxWait;
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(lockkey, expiresStr) == 1) {
return true;
}
// 如果锁存在,获取锁的过期时间
String currentValueStr = jedis.get(lockkey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockkey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
return true;
}
}
// 其他情况,一律返回加锁失败
return false;
}
上面的代码问题和第一套代码问题差不多。而且由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
正确的解锁
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
private static final Long RELEASE_SUCCESS = 1L;
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。要确保上述操作是原子性。eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
什么是原子性操作
在多进程(线程)访问共享资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源,(要么完全执行,要么完全不执行)
而redis普通方式加锁太过于繁琐和复杂,要考虑的地方有很多。所以现在的大部分公司采用Redis做分布式锁,一般就是用Redisson框架,非常的简便易用。