|
|
package com.yohoufo.common.lock;
|
|
|
|
|
|
import com.google.common.collect.Lists;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
|
import org.springframework.data.redis.core.script.RedisScript;
|
|
|
|
|
|
import java.util.Random;
|
|
|
import java.util.UUID;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
/**
|
|
|
* Redis分布式锁
|
|
|
* 使用 SET resource-name anystring NX EX max-lock-time 实现
|
|
|
* <p>
|
|
|
* 该方案在 Redis 官方 SET 命令页有详细介绍。
|
|
|
* http://doc.redisfans.com/string/set.html
|
|
|
* <p>
|
|
|
* 在介绍该分布式锁设计之前,我们先来看一下在从 Redis 2.6.12 开始 SET 提供的新特性,
|
|
|
* 命令 SET key value [EX seconds] [PX milliseconds] [NX|XX],其中:
|
|
|
* <p>
|
|
|
* EX seconds — 以秒为单位设置 key 的过期时间;
|
|
|
* PX milliseconds — 以毫秒为单位设置 key 的过期时间;
|
|
|
* NX — 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
|
|
|
* XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。
|
|
|
* <p>
|
|
|
* 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
|
|
|
* <p>
|
|
|
* 客户端执行以上的命令:
|
|
|
* <p>
|
|
|
* 如果服务器返回 OK ,那么这个客户端获得锁。
|
|
|
* 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
|
|
|
*/
|
|
|
@Slf4j
|
|
|
class DefaultRedisLock implements RedisLock {
|
|
|
|
|
|
private static final String ACQUIRE_OK = "OK";
|
|
|
|
|
|
private static final Long RELEASE_OK = 1L;
|
|
|
|
|
|
private RedisTemplate<String, String> redisTemplate;
|
|
|
|
|
|
private String key;
|
|
|
|
|
|
/**
|
|
|
* 锁的有效时间(s)
|
|
|
*/
|
|
|
private long expireTimeMillis;
|
|
|
|
|
|
/**
|
|
|
* lua script
|
|
|
*/
|
|
|
private String lockValue;
|
|
|
|
|
|
/**
|
|
|
* 锁标记
|
|
|
*/
|
|
|
private boolean locked;
|
|
|
|
|
|
final Random random = new Random();
|
|
|
|
|
|
DefaultRedisLock(Builder builder) {
|
|
|
this.redisTemplate = builder.redisTemplate;
|
|
|
this.key = builder.key;
|
|
|
this.expireTimeMillis = builder.expireTimeMillis;
|
|
|
}
|
|
|
|
|
|
public boolean lock() {
|
|
|
updateLockValue();
|
|
|
while (true) {
|
|
|
if (acquire(lockValue, expireTimeMillis)) {
|
|
|
locked = true;
|
|
|
return true;
|
|
|
}
|
|
|
sleep(10, 50000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public boolean lock(long timeout, TimeUnit timeUnit) {
|
|
|
updateLockValue();
|
|
|
// 请求锁超时时间,纳秒
|
|
|
long timeoutNanos = timeUnit.toNanos(timeout);
|
|
|
// 系统当前时间,纳秒
|
|
|
long nowNanoTime = System.nanoTime();
|
|
|
while ((System.nanoTime() - nowNanoTime) < timeoutNanos) {
|
|
|
if (acquire(lockValue, expireTimeMillis)) {
|
|
|
locked = true;
|
|
|
// 上锁成功结束请求
|
|
|
return true;
|
|
|
}
|
|
|
// 每次请求等待一段时间
|
|
|
sleep(10, 50000);
|
|
|
}
|
|
|
return locked;
|
|
|
}
|
|
|
|
|
|
public boolean tryLock() {
|
|
|
updateLockValue();
|
|
|
locked = acquireOr(lockValue, expireTimeMillis, false);
|
|
|
return locked;
|
|
|
}
|
|
|
|
|
|
public boolean unlock() {
|
|
|
if (locked) {
|
|
|
if (releaseOr(lockValue, false)) {
|
|
|
locked = false;
|
|
|
return true;
|
|
|
} else {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
private boolean acquire(String value, long expireTimeMillis) {
|
|
|
String script = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";
|
|
|
RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
|
|
|
String result = redisTemplate.execute(redisScript, Lists.newArrayList(key), value, String.valueOf(expireTimeMillis));
|
|
|
if (ACQUIRE_OK.equals(result)) {
|
|
|
log.info("acquire lock ok, key is {} value is {}", key, value);
|
|
|
return true;
|
|
|
} else {
|
|
|
log.info("acquire lock ko, key is {} value is {}", key, value);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private boolean acquireOr(String value, long expireTimeMillis, boolean defaultValue) {
|
|
|
try {
|
|
|
return acquire(value, expireTimeMillis);
|
|
|
} catch (Exception e) {
|
|
|
log.info("release lock ko, key is {} value is {}", key, value, e);
|
|
|
return defaultValue;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private boolean release(String value) {
|
|
|
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
|
|
|
RedisScript<Long> redisScript = new DefaultRedisScript(script, Long.class);
|
|
|
Long result = redisTemplate.execute(redisScript, Lists.newArrayList(key), value);
|
|
|
if (RELEASE_OK.equals(result)) {
|
|
|
log.info("release lock ok, key is {} value is {}", key, value);
|
|
|
return true;
|
|
|
} else {
|
|
|
log.info("release lock ko, key is {} value is {}", key, value);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private boolean releaseOr(String value, boolean defaultValue) {
|
|
|
try {
|
|
|
return release(value);
|
|
|
} catch (Exception e) {
|
|
|
// try once
|
|
|
try {
|
|
|
return release(value);
|
|
|
} catch (Exception e2) {
|
|
|
log.info("release lock ko, key is {} value is {}", key, value, e2);
|
|
|
return defaultValue;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void updateLockValue() {
|
|
|
lockValue = UUID.randomUUID().toString();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 线程等待时间
|
|
|
*
|
|
|
* @param millis 毫秒
|
|
|
* @param nanos 纳秒
|
|
|
*/
|
|
|
private void sleep(long millis, int nanos) {
|
|
|
try {
|
|
|
Thread.sleep(millis, random.nextInt(nanos));
|
|
|
} catch (InterruptedException e) {
|
|
|
log.info("lock {} sleep interrupted ", key, e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
} |
|
|
\ No newline at end of file |
...
|
...
|
|