分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
常见分布式锁的实现一般有三种方式:
- 基于关系型数据库,如MySQL
- 基于缓存数据库,如Redis
- 基于Zookeeper
此处对Redis如何实现分布式锁进行一下讲解
一、 实现原理
主要利用Redis的几个特点:
- "SET key value [EX seconds] [PX milliseconds] [NX|XX] "命令 如:SET key value EX 10 NX 若key不存在设置一个10秒过期的key-value
- "DEL" 命令 释放锁即删除对应key
- Redis是单线程的, 保证多个锁的竞争者只有一个能够创建key成功即获取到锁
SET key value [EX seconds] [PX milliseconds] [NX|XX] 命令:
EX seconds | 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value |
PX milliseconds | 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value |
NX | 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。 |
XX | 只在键已经存在时, 才对键进行设置操作。 |
二、可能存在的问题
1.锁的释放
A获取锁之后, 执行时间为15s, 锁的过期时间为10s, 当A执行完之前, B获取到了锁, A执行完之后使用DEL强制删key, 删除掉的其实是B获取到的锁
解决方案: 们可以在设置key的时候将value设置为一个唯一值uniqueValue(可以是随机值、UUID、或者机器号+线程号的组合、签名等)。当解锁时,也就是删除key的时候先判断一下key对应的value是否等于先前设置的值,如果相等才能删除key
但我们要保证 判断value是否相等的操作 与 删除 操作 的原子性
// GET 与 DEL 是两个分开的操作
if uniqueKey == GET(key) {
DEL key
}
Lua脚本的原子性,在Redis执行该脚本的过程中,其他客户端的命令都需要等待该Lua脚本执行完才能执行
//其中ARGV[1]表示设置key时指定的唯一值。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
2.Redis的master节点宕机
- 线程A在master节点拿到了锁。
- master节点在把A创建的key写入slave之前宕机了。
- slave变成了master节点。
- 线程B也得到了和A还持有的相同的锁。(因为原来的slave里面还没有A持有锁的信息)
三、代码示例
public boolean lock(String lockKey, String uniqueValue, int seconds){
SetParams params = new SetParams();
params.nx().ex(seconds);
String result = jedis.set(lockKey, uniqueValue, params);
if ("OK".equals(result)) {
return true;
}
return false;
}
public boolean unlock(String lockKey, String uniqueValue){
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(uniqueValue));
if (result.equals(1)) {
return true;
}
return false;
}
转自: