1、保证可靠性
a、互斥性:在任意时刻,只有一个客户端能持有锁。
b、不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
c、解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
d、具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
2、代码实现采用java redis客户端实现,maven的pom.xml配置如下
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
3、加锁
3.1、正确示例
public class TestDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static Jedis jedis = new Jedis();
public static void main(String[] args) {
for (int i=0;i<5;i++){
boolean result = lock(jedis,"redis_key", "123456",10 );
System.out.println(result);
}
}
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
main运行结果:
true
false
false
false
false
加锁代码:
jedis.set(String key, String value, String nxxx, String expx, int time);
各个参数含义:
key:锁唯一的key。
value:锁的value,用于解锁时判断锁是谁的,从而保证可靠性:“解铃还须系铃人”。
nxxx:此处参数值为NX,SET IF NOT EXIST,当key不存在时,我们进行set操作;若key已经存在,则不做任何操作。
expx:此处参数值为PX,意思是要给这个key加一个过期的时间,具体时间由第五个参数决定。
time:代表key的过期时间,单位s。
以上参数保证了分布式锁的可靠性,NX参数保证了任意时间只会有一个客户端持有锁;PX参数保证了即使程序奔溃无法手动释放锁,也可以通过锁过期自动释放;value保证了手动释放锁的时候判断解锁和加锁是否是同一个客户端,从而保证自己的锁不会被其他人解了。
3.2、错误示例
/**
* 错误的分布式锁1
* @param jedis
* @param lockKey
* @param requestId
* @param expireTime
*/
public static void errLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若此处程序崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
}
4、解锁
4.1、正确示例
public class TestDistributedLock {
private static Jedis jedis = new Jedis();
public static void main(String[] args) {
for (int i=0;i<5;i++){
boolean result = lock(jedis,"redis_key", "123456",60 );
System.out.println("lock result = " + result);
}
boolean result = unlock(jedis,"redis_key","123456");
System.out.println("unlock result = "+ result);
boolean result2 = lock(jedis,"redis_key", "123456",60 );
System.out.println("again lock result = " + result2);
}
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean unlock(Jedis jedis, String lockKey, String requestId) {
// Lua脚本代码
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;
}
}
运行结果:
先加锁,然后成功解锁,再加锁
lock result = true
lock result = false
lock result = false
lock result = false
lock result = false
unlock result = true
again lock result = true
第一行代码,我们写了一个简单的Lua脚本代码,第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行,eval执行Lua命令确保了原子性,eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
4.2、错误示例
/**
* 错误的释放锁1
* @param jedis
* @param lockKey
*/
public static void errUnLock1(Jedis jedis, String lockKey) {
// 这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。
jedis.del(lockKey);
}
/**
* 错误的释放锁2
* @param jedis
* @param lockKey
* @param requestId
*/
public static void errUnLock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
5、总结
本文采用java redis客户端,验证了redis加锁和解锁的可靠性问题,如有错漏,望指出!!!
参考资料:
芋道源码