借助 SETNX(不完全正确)
Redis 中 SETNE 只有在 key 不存在时设置 key 的值,因此非常容易就实现了锁功能。只需要客户端对指定 KEY 成功设置一个随机值,借助这个值来防止其他的进程取得锁。
127.0.0.1:6379> get simpleLock
(nil)
127.0.0.1:6379> setnx simpleLock Locked
(integer) 1 //成功得到锁返回1
127.0.0.1:6379> get simpleLock
"Locked"
127.0.0.1:6379> setnx simpleLock Release
(integer) 0 // 这里重新设置锁的值,返回0代表其他客户端获得锁
127.0.0.1:6379> get simpleLock
"Locked"
127.0.0.1:6379>
127.0.0.1:6379> del simpleLock // 删除键,释放锁
(integer) 1
127.0.0.1:6379> setnx simpleLock Release //再重新获取锁
(integer) 1
127.0.0.1:6379>
通过 SETNX 基本能实现一个不完全正确的锁,Java代码如下:
package me.touch.redis;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@RunWith(JUnit4.class)
public class SimpleLock {
private Jedis jedis;
private JedisPool pool;
@Before
public void setUp() {
pool = new JedisPool(new JedisPoolConfig(), "localhost");
jedis = pool.getResource();
}
@After
public void after() {
jedis.close();
pool.destroy();
}
/**
* 获得简单锁
* @return
*/
public boolean acquireSimpleLock(String lockName){
return jedis.setnx(lockName, "Locked") == 1 ;
}
/**
* 释放锁
* @return
*/
public boolean releaseSimpleLock(String lockName){
return jedis.del(lockName, "Locked") == 1 ;
}
@Test
public void test(){
if(acquireSimpleLock("simpleLock")){
System.out.println("获取锁成功 ·····");
// Do something ........
if(releaseSimpleLock("simpleLock")){
System.out.println("释放锁成功 ·····");
}
}
}
}
运行结果:
获取锁成功 ·····
释放锁成功 ·····
但是这个锁是不完全正确的,缺少超时机制,缺少重试机制,释放锁的时候没有验证当前锁是否由当前进程拥有等。
一个不完全正确的锁会导致一些不正确的行为,如:
- 当缺少超时机制时,当持有锁的进程死掉后,锁得不释放,造成死锁。
- 当持有锁的进程操作时间过长导致锁自动释放,但是很进程本身不知道,使得逻辑完成后错误的释放其他进程的锁(需要验证锁是否是当前进程持有)。
- 当持有锁的进程崩溃后,其他进程无法检测到,只能浪费时间等待锁达到超时时候被释放。
- 当一个进程持有锁过期后,其他多个进程同时尝试去获取锁,并且都获取了锁,而且都认为自己是唯一一个获取到锁的进程(需要验证锁是否是当前进程持有)
使用 Luna 脚本 (基本正确)
Redis 中的命令是原子执行,的所以我们可以在 Lua 脚本中组合多个命令来完成我们的的逻辑。
lua 脚本获取锁
-- EXISTS 判断是否存在 KEY ,如果存在,说明其他进程已经获得锁,不存在这,设置KEY
if redis.call('EXISTS', KEYS[1]) == 0 then
return redis.call('SETEX', KEYS[1], unpack(ARGV))
end
lua 脚本释放锁
-- GET 获取 KEY 值,判断是否与指定的值相等,相等则删除KEY
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1]) or true
end
Java 源码
/**
* 获得锁
* @param keyName 锁的名称
* @param keyVlaue 锁的值,建议使用UUID
* @param expire 锁的过期时间
* @param timeout 获取所得超时时间,毫秒
* @return
*/
public boolean acquireLockWhithTimeOut(String keyName, String keyVlaue,
String expire, long timeout){
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('EXISTS', KEYS[1]) == 0 then \n")
.append(" return redis.call('SETEX', KEYS[1], unpack(ARGV)) \n")
.append("end");
long now = System.currentTimeMillis();
do{
if("OK".equals(jedis.eval(sb.toString(), 1, keyName, expire, keyVlaue))){
return true;
}
}while( System.currentTimeMillis() < (now + timeout));
return false;
}
/**
* 释放锁
* @param keyName 锁名称
* @param keyVlaue 锁的值
* @return
*/
public boolean releaseLock(String keyName, String keyVlaue){
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('GET', KEYS[1]) == ARGV[1] then \n")
.append(" return redis.call('DEL', KEYS[1]) or true \n")
.append("end");
return ((Long) jedis.eval(sb.toString(), 1, keyName, keyVlaue)) == 1 ;
}
@Test
public void test() throws InterruptedException{
//使用 uuid 作为锁的值
String uuid = UUID.randomUUID().toString();
if(acquireLockWhithTimeOut("simpleLock", uuid, "60", 60*1000)){
System.out.println("获取锁成功 ·····");
// Do something ........
TimeUnit.SECONDS.sleep(1); // 线程睡上30秒
if(releaseLock("simpleLock", uuid)){
System.out.println("释放锁成功 ·····");
}
}
}
运行结果:
b308b026-8b01-4cf0-b145-b9061bf617f6
获取锁成功 ·····
释放锁成功 ·····
在这个例子中通过传入 timeout 设置获取锁的超时时间实现了锁获取的重试机制;同时,通过 expire 指定了 key 的过期时间,避免照成了死锁。在获取锁时指定的值为UUID,保证了锁的唯一性。此外,在释放锁时比较 UUID 成功避免错误释放其他进程锁的问题,因此也不会出现多个进程多获取到锁的情况。当前实现已经是基本正确的锁实现了,能用于绝大部分应用场景,但是依然没有解决因为持有锁的进程崩溃造成其他进程浪费时间等待锁过期的问题。