在进入正题之前要搞清楚两个问题:一是为什么需要分布式锁,二是Redis为什么能够实现分布式锁。
假设现在有一个应用部署在了三台机器上,应用的某个资源需要进行加锁控制,如果用关键字synchronized加锁能控制住么?显然是不行的,因为synchronized是线程锁,只能作用在当前的JVM里,获取的锁是各自JVM主内存上的锁资源。就好比一个房间有三个门,不惯是打开哪个门上的锁都能进入这个房间。此时就需要一个统一的入口来提供锁资源,这就是分布式锁。能作为锁还必须满足一个条件,就是操作必须是原子性的,锁本身要能保证线程安全。能实现分布式锁的有Redis、ZooKeeper、数据库等,本文只介绍Redis。
setNX:SET if Not eXists,命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1;设置失败,返回 0 。
get:获取指定key的值
getSet:设置指定 key 的值,并返回 key 的旧值。
很多人可能用了setNX和expire命令,这种操作不是原子操作,如果在调用expire设置过期时间之前机器挂了,这种情况会导致锁没法释放,即死锁。
public boolean lock(String key) throws InterruptedException {
//自旋超时时间
int timeout = 3000;
while(timeout-- >= 0) {
Jedis resource = jedisPool.getResource();
//过期时间
long expires = System.currentTimeMillis() + 1000L;
try {
Long result = resource.setnx(key, String.valueOf(expires));
//通过setnx加锁成功
if (result.equals(1L)) {
return true;
}
//通过setnx加锁失败,使用get和getSet组合命令加锁
else {
String currentExpires = resource.get(key);
//判断锁是否过期,如果过期重新加锁
if (StringUtils.isNotBlank(currentExpires) && Long.parseLong(currentExpires) < System.currentTimeMillis()) {
String oldExpires = resource.getSet(key, String.valueOf(expires));
//判断值是否已被修改
if (StringUtils.isNotBlank(oldExpires) && oldExpires.equals(currentExpires)) {
return true;
}
}
}
} finally {
if (resource != null)
resource.close();
}
Thread.sleep(100L);
}
return false;
}
锁 使 用 完 之 后 , 记 得 在 f i n a l l y 里 将 锁 释 放 。 \color{red}{锁使用完之后,记得在finally里将锁释放。} 锁使用完之后,记得在finally里将锁释放。
先了解几个参数
XX:只有key存在时才设置
NX:与setNX类似,只有key不存在时才设置
PX:表示过期时间的单位为毫秒
EX:表示过期时间的单位为秒
public boolean lock(String key) throws InterruptedException {
//自旋超时时间
int timeout = 3000;
while(timeout-- >= 0) {
Jedis resource = jedisPool.getResource();
SetParams params = new SetParams();
//设置为setnx模式
params.nx();
//设置过期时间,单位为毫秒
params.px(1000);
try {
String result = resource.set(key, key, params);
//返回OK表示设置成功
if ("OK".equals(result)) {
return true;
}
} finally {
if (resource != null)
resource.close();
}
Thread.sleep(100L);
}
return false;
}
用set命令,设置key和过期时间是一个命令,所以不会出现应用层导致的死锁问题。
锁 使 用 完 之 后 , 记 得 在 f i n a l l y 里 将 锁 释 放 。 \color{red}{锁使用完之后,记得在finally里将锁释放。} 锁使用完之后,记得在finally里将锁释放。
lua脚本可以将一组Redis命令放在一次请求里完成,Redis会将脚本作为一个整体执行,保证了原子性
public boolean lock(String key) throws InterruptedException {
//自旋超时时间
int timeout = 3000;
while(timeout-- >= 0) {
Jedis resource = jedisPool.getResource();
try {
//lua脚本
String script = "if redis.call('set', " + key + "," + key + ",'nx','px',time)=='OK' then\n" +
"return 'OK'\n" +
"else\n" +
" if redis.call('get'," + key + ") == " + key + " then\n" +
" if redis.call('EXPIRE'," + key + ", 1000)==1 then\n" +
" return 'OK'\n" +
" end\n" +
" end\n" +
"end";
Object result = resource.eval(script);
if ("OK".equals(result)) {
return true;
}
} finally {
if (resource != null)
resource.close();
}
Thread.sleep(100L);
}
return false;
}
锁 使 用 完 之 后 , 记 得 在 f i n a l l y 里 将 锁 释 放 。 \color{red}{锁使用完之后,记得在finally里将锁释放。} 锁使用完之后,记得在finally里将锁释放。
推荐使用Redisson,该组件提供了多种分布式锁实现机制,如可重入锁(Reentrant Lock)、公平锁(Fair Lock)、读写锁(ReadWriteLock)、信号量(Semaphore) 等。Redisson的加锁和释放锁都是使用的lua脚本来实现的。