Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class DistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String CLIENT_ID = UUID.randomUUID().toString();
private static final Map<String, RLock> REDIS_LOCK_MAP = new ConcurrentHashMap<>();
public RLock getLock(String lockKey) {
return getLock(lockKey, 60, TimeUnit.SECONDS);
}
public RLock getLock(String lockKey, long expireTime, TimeUnit expireTimeUnit) {
if (lockKey == null || lockKey.trim().isEmpty()) {
throw new RuntimeException("lockKey can not be empty!");
}
return REDIS_LOCK_MAP.computeIfAbsent(lockKey, key -> new RedisLock(key, expireTime, expireTimeUnit, CLIENT_ID));
}
private class RedisLock implements RLock {
/**
* obtain lock script
*/
private static final String OBTAIN_LOCK = "local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" +
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"else\n" +
" return false\n" +
"end";
/**
* remove key script
*/
private static final String REMOVE_LOCK = "local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('DEL', KEYS[1])\n" +
"end";
/**
* each machine has own clientId which is in order to avoid to release other client's lock
*/
private final String clientId;
private final String lockKey;
private final ReentrantLock lock;
private final long expireTime;
private final TimeUnit expireTimeUnit;
private RedisLock(String lockKey, long expireTime, TimeUnit expireTimeUnit, String clientId) {
this.lock = new ReentrantLock();
this.lockKey = lockKey;
this.expireTime = expireTime;
this.expireTimeUnit = expireTimeUnit;
this.clientId = clientId;
}
@Override
public void lock() {
try {
lock.lock();
if (checkReentrantLock(lockKey)) {
return;
}
while (!obtainLockFromRedis()) {
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException ignore) {
lock.unlock();
}
}
@Override
public boolean tryLock(long time, TimeUnit timeUnit) {
long now = System.currentTimeMillis();
try {
if (lock.tryLock(time, timeUnit)) {
if (checkReentrantLock(lockKey)) {
return true;
}
long expire = now + timeUnit.toMillis(time);
boolean acquired;
while (!(acquired = obtainLockFromRedis()) && System.currentTimeMillis() < expire) {
TimeUnit.MILLISECONDS.sleep(100);
}
return acquired;
}
} catch (InterruptedException e) {
lock.unlock();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit timeUnit, int retry) {
if (retry <= 0) {
return tryLock(time, timeUnit);
}
boolean acquired;
do {
acquired = tryLock(time, timeUnit);
} while (!acquired && retry-- > 0);
return acquired;
}
@Override
public void unlock() {
if (!lock.isHeldByCurrentThread()) {
throw new IllegalStateException("You do not own lock at " + this.lockKey);
}
if (lock.getHoldCount() > 1) {
lock.unlock();
return;
}
try {
removeLockFromRedis();
} finally {
lock.unlock();
}
}
private boolean checkReentrantLock(String lockKey) {
String id = redisTemplate.opsForValue().get(lockKey);
return Objects.equals(clientId, id);
}
private boolean obtainLockFromRedis() {
Boolean success = redisTemplate.execute(
new DefaultRedisScript<>(OBTAIN_LOCK, Boolean.class),
Collections.singletonList(lockKey),
clientId,
String.valueOf(expireTimeUnit.toMillis(expireTime))
);
return Boolean.TRUE.equals(success);
}
private void removeLockFromRedis() {
redisTemplate.execute(
new DefaultRedisScript<>(REMOVE_LOCK),
Collections.singletonList(lockKey),
clientId
);
}
}
public interface RLock {
void lock();
boolean tryLock(long time, TimeUnit timeUnit);
default boolean tryLock() {
return tryLock(0, TimeUnit.MILLISECONDS);
}
boolean tryLock(long time, TimeUnit timeUnit, int retry);
void unlock();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment