思路
先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。
但是如果在 setnx 之后执行 expire之前进程意外 crash 或者要重启维护了,那会怎么样?这个锁就永远得不到释放了。
set 指令有非常复杂的参数,可以同时把 setnx 和expire 合成一条指令来用的。
package com.xp.lock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
public class SingleRedisLock {
private static final String LOCK_SUCCESS = "OK";
//这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已
//经存在,则不做任何操作;
private static final String SET_IF_NOT_EXIST = "NX";
//这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时
//间由第五个参数决定。
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
private static SingleRedisLock singleRedisLock;
private static JedisPool jedisPool;
public SingleRedisLock() {
this.jedisPool = new JedisPool();
}
public static SingleRedisLock instance() {
if (singleRedisLock == null) {
synchronized (SingleRedisLock.class) {
if (singleRedisLock == null) {
singleRedisLock = new SingleRedisLock();
}
}
}
return singleRedisLock;
}
/**
* 尝试获取分布式锁
*
* @param lockKey 锁 我们使用key来当锁,因为key是唯一的。
* @param requestId 请求标识 我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件 , 解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
* @param expireTime 具体超期时间 与第四个参数相呼应,代表key的过期时间。
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
Jedis jedis = jedisPool.getResource();
SetParams setParams = new SetParams();
setParams.nx();
setParams.px(expireTime);
String result = jedis.set(lockKey, requestId, setParams);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 释放分布式锁
*
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(String lockKey, String requestId) {
Jedis jedis = jedisPool.getResource();
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;
}
}
IndexCount服务类:
package com.xp.lock;·
package com.xp.lock;
import org.springframework.stereotype.Service;
@Service
public class IndexCount {
private Long count=0l;
public Long getCount() {
return count;
}
public void setCount(Long count) {
this.count = count;
}
}
获取锁方法说明:
执行上面获取锁方法就只会导致两种结果:
-
当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
-
已有锁存在,不做任何操作。
我们的加锁代码满足我们可靠性里描述的三个条件:
我这里使用的Jedis的版本是3.3.0,必须使用SetParams
对象传入nx和expire相关设置。
- 首先,
setParams.nx()
可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。 - 其次,由于我们通过
setParams.px(expireTime)
对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。 - 最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。
释放锁方法说明:
- 第一行代码,我们写了一个简单的Lua脚本代码,
- 第二行代码,我们将Lua代码传到redis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
这段Lua代码的功能:
首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。
为了要确保上述操作是原子性的。就使用在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令
错误实现:
1.错误加锁
/**
*
* @param acquireTimeout 获取锁的超时时间
* @param timeOut 锁的过期时间
* @return
*/
public String getRedisLock(Long acquireTimeout, Long timeOut) {
Jedis conn = null;
try {
conn = jedisPool.getResource();
String identifierValue = UUID.randomUUID().toString();
int expireLock = (int) (timeOut / 1000);
Long endTime = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < endTime) {
if (conn.setnx(redislockKey, identifierValue) == 1) {
conn.expire(redislockKey, expireLock);
return identifierValue;
}
}
} catch (Exception e) {
e.printStackTrace();
if (conn != null) {
conn.close();
}
}
return null;
}
注意问题:
要为锁的value设置一个唯一的值,这样就避免了任意线程都能释放锁,因为如果业务时间小于锁的过期时间,锁被释放而业务没有执行完,另一线程获得锁,但会因第一个线程最后的释放锁而受到影响
conn.setnx和con.expire应该用lua脚本,保证其原子操作,上述代码就明显错误了,如果conn.setnx执行完后,redis服务器宕机了那么会导致锁永远无法释放
2.错误释放锁
/**
*
* @param identifierValue 锁的value
*/
public void unRedisLock(String identifierValue) {
Jedis conn = null;
conn = jedisPool.getResource();
try {
if (conn.get(redislockKey).equals(identifierValue)) {
System.out.println("释放锁" + Thread.currentThread().getName() + ",identifierValue: " + identifierValue);
conn.del(redislockKey);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
}
此处同样要用lua脚本来保证conn.get和conn.del的原子性操作,如果执行到conn.get后刚好锁过期了,而另一线程获得锁,但conn.del会把锁删掉,虽然判断了锁的value后再删除仍会出现一个线程删除了另一线程获得的锁
THE END.