1. 理解 redis SETNX命令特性
- 首先 SETNX 是 SET if Not eXists的简写 , 将 key 的值设为 value,当 key不存在的时候才能设置成功返回值 为 “1”,否则返回“0”,表示设置失败。
2. 使用SETNX实现分布式锁
- SETNX lock.foo <current Unix time + lock timeout + 1>
- 如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
- 如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SET NX 操作,以获得锁。
3. 解决死锁问题
- 设想一个问题? 一个进程在操作过程中获取锁了,因其他因素与redis 进行断开 ,无法对已经获取的锁进行释放,那么将会出现其他进程一直等待的过程,我们称之为“死锁”。
- 我们可以利用 redis 设置key的有效时间解决此问题,当时间超时,则其他进程继续竞争锁资源。
4. 基于jedis获取锁代码如下
- 此代码需要优化,未能及时处理已获取锁的进程出现自己挂掉的情况,只能等到key有效期失效,别的进程才能参与锁的竞争
public String acquireLock(String lockName, long acquireTimeout, long lockTimeout){
//保证释放锁的时候是同一个持有锁的人
String identifier = UUID.randomUUID().toString();
String lockKey = "lock:" + lockName;
int lockExpire = (int) (lockTimeout / 1000);
Jedis jedis = null;
try {
try {
jedis = JedisClient.getJedis();
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis() + acquireTimeout;
//获取锁的限定时间
while (System.currentTimeMillis() < end) {
//设置值成功
if (jedis.setnx(lockKey, identifier) == 1) {
//设置超时时间
jedis.expire(lockKey, lockExpire);
//获得锁成功
return identifier;
}
//设置超时时间
if (jedis.ttl(lockKey) == -1) {
jedis.expire(lockKey, lockExpire);
}
try {
//等待片刻后进行获取锁的重试
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
jedis.close(); //回收
}
return null;
}
5. 基于jedis释放锁代码如下
public boolean releaseLock(String lockName, String identifier) {
System.out.println(lockName + "开始释放锁:" + identifier);
String lockKey = "lock:" + lockName;
Jedis jedis = null;
boolean isRelease = false;
try {
jedis = JedisClient.getJedis();
while (true) {
jedis.watch(lockKey);
//判断是否为同一把锁
if (identifier.equals(jedis.get(lockKey))) {
Transaction transaction = jedis.multi();
transaction.del(lockKey);
if (transaction.exec().isEmpty()) {
continue;
}
isRelease = true;
}
//TODO 异常
jedis.unwatch();
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
return isRelease;
}
6. Redision客户端 获取锁的核心代码
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
7. Redision客户端 释放锁的核心代码
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}