基于Redis的分布式锁实现
背景
- 根据redis的setnx命令实现只有一个客户端可以拿到锁;
- RedissonLock的分布式锁实现使用了lua脚本,这里提供一种不适用脚本实现的方法;
基本实现
- 使用redis的setnx命令,再加上一个过期时间防止死锁
- 缺点:不支持重入,不支持wait,如果调用unlock的时间>leaseTime,则会删除之后获得的锁;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.concurrent.TimeUnit;
/**
* Created by jinshuan.li on 2016/12/1.
*/
public class DefaultDistrLock implements DistrLock {
private static JedisPool jedisPool = null;
private static Long DEFAULT_LEASE_TIME = 30000L;
private String lockKey;
public DefaultDistrLock(String lockKey) {
this.lockKey = lockKey;
}
/**
* 初始化
*
* @param jedisPool
*/
public static void init(JedisPool jedisPool) {
DefaultDistrLock.jedisPool = jedisPool;
}
@Override
public boolean tryLock() {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Long setnx = jedis.setnx(lockKey, String.valueOf(Thread.currentThread().getId()));
if (setnx.equals(1L)) {
jedis.pexpire(lockKey, DEFAULT_LEASE_TIME);
return true;
}
} finally {
closeJedis(jedis);
}
return false;
}
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Long setnx = jedis.setnx(lockKey, String.valueOf(Thread.currentThread().getId()));
if (setnx.equals(1L)) {
long toMillis = unit.toMillis(leaseTime);
jedis.pexpire(lockKey, toMillis);
return true;
}
} finally {
closeJedis(jedis);
}
return false;
}
@Override
public void unlock() {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(lockKey);
} finally {
closeJedis(jedis);
}
}
private void closeJedis(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
可重入的分布式锁实现
- 思路:在setnx失败之后,查看其value是否为特定值,如果是,则可以继续获取锁。同时加入holdCount记录锁的层数,unlock时也要进行相应处理
- 缺点:不可等待,每次竞争锁都需要访问redis
/**
* Created by jinshuan.li on 2016/12/1.
*/
public class DefaultDistrLock implements DistrLock {
private static JedisPool jedisPool = null;
private static Long DEFAULT_LEASE_TIME = 30000L;
private String lockKey;
private String uuid = UUID.randomUUID().toString();
private AtomicInteger holdCount = new AtomicInteger(0);
public DefaultDistrLock(String lockKey) {
this.lockKey = lockKey;
}
/**
* 初始化
*
* @param jedisPool
*/
public static void init(JedisPool jedisPool) {
DefaultDistrLock.jedisPool = jedisPool;
}
@Override
public boolean tryLock() {
Jedis jedis = null;
try {
String setValue=uuid + Thread.currentThread().getId();
jedis = jedisPool.getResource();
Long setnx = jedis.setnx(lockKey, setValue);
if (setnx.equals(1L)) {
jedis.pexpire(lockKey, DEFAULT_LEASE_TIME);
holdCount.incrementAndGet();
return true;
}
String value = jedis.get(lockKey);
if (StringUtils.equals(uuid + Thread.currentThread().getId(), value)) {
holdCount.incrementAndGet();
return true;
}
} finally {
closeJedis(jedis);
}
holdCount = new AtomicInteger(0);
return false;
}
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
Jedis jedis = null;
try {
String setValue=uuid + Thread.currentThread().getId();
jedis = jedisPool.getResource();
Long setnx = jedis.setnx(lockKey, setValue);
if (setnx.equals(1L)) {
long toMillis = unit.toMillis(leaseTime);
jedis.pexpire(lockKey, toMillis);
holdCount.incrementAndGet();
return true;
}
String value = jedis.get(lockKey);
if (StringUtils.equals(uuid + Thread.currentThread().getId(), value)) {
holdCount.incrementAndGet();
return true;
}
} finally {
closeJedis(jedis);
}
holdCount = new AtomicInteger(0);
return false;
}
@Override
public void unlock() {
final int countValue = holdCount.decrementAndGet();
if (countValue < 0) {
throw new IllegalStateException("this thread does not get lock");
}
if (countValue == 0) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(lockKey);
} finally {
closeJedis(jedis);
}
}
}
private void closeJedis(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
支持重入,等待的分布式锁
- 思路:在访问redis之前,先用一个innerLock拿到本地jvm的锁,原因是:如果在本地线程中都竞争不过,在分布式环境下则更竞争不过其他线程;
- 因此同时访问redis的并发只跟机器数目有关;
- 等待的实现,通过redis的pub、sub实现,同时使用countDownLatch来等待;在sub的通知中countDown以激活等待线程;
- 最终不断重试来获取锁;
import org.apache.commons.lang.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by jinshuan.li on 2016/12/1.
*/
public class DefaultDistrLock implements DistrLock {
private static JedisPool jedisPool = null;
private static Long DEFAULT_LEASE_TIME = 30000L;
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
private String lockKey;
private String uuid = UUID.randomUUID().toString();
private Lock innerLock = new ReentrantLock();
private CountDownLatch countDownLatch = new CountDownLatch(1);
JedisPubSub pubSub = new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
if (StringUtils.equals("DELETE", message)) {
this.unsubscribe();
}
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
countDownLatch.countDown();
}
};
private AtomicInteger holdCount = new AtomicInteger(0);
private long firstAccireTime = 0;
private long leaseTime = DEFAULT_LEASE_TIME;
public DefaultDistrLock(String lockKey) {
this.lockKey = lockKey;
}
/**
* 初始化
*
* @param jedisPool
*/
public static void init(JedisPool jedisPool) {
DefaultDistrLock.jedisPool = jedisPool;
}
@Override
public boolean tryLock() {
boolean getLocalLock = false;
Jedis jedis = null;
try {
getLocalLock = innerLock.tryLock();
if (!getLocalLock) {
return false;
}
jedis = jedisPool.getResource();
boolean remoteLock = getRemoteLock(jedis, DEFAULT_LEASE_TIME, TimeUnit.MILLISECONDS);
if (remoteLock) {
return true;
}
} finally {
closeJedis(jedis);
}
holdCount = new AtomicInteger(0);
return false;
}
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
Jedis jedis = null;
boolean getLocalLock = false;
long waitTimeMillis = unit.toMillis(waitTime);
try {
long startTime = System.currentTimeMillis();
getLocalLock = innerLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
if (!getLocalLock) {
return false;
}
jedis = jedisPool.getResource();
boolean remoteLock = getRemoteLock(jedis, leaseTime, unit);
if (remoteLock) {
return true;
}
while (true) {
long lastWaitTime = waitTimeMillis - (System.currentTimeMillis() - startTime);
if (lastWaitTime <= 0) {
break;
}
Long pttl = jedis.pttl(lockKey);
if (null == pttl) {
break;
}
long shouldWaitTime = pttl < lastWaitTime ? pttl : lastWaitTime;
if (shouldWaitTime != 0) {
if (countDownLatch.getCount() != 1) {
countDownLatch = new CountDownLatch(1);
}
subscribeDelete();
countDownLatch.await(shouldWaitTime, TimeUnit.MILLISECONDS);
}
lastWaitTime = waitTimeMillis - (System.currentTimeMillis() - startTime);
if (lastWaitTime > 0) {
remoteLock = getRemoteLock(jedis, leaseTime, unit);
if (remoteLock) {
return true;
}
} else {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
closeJedis(jedis);
}
holdCount = new AtomicInteger(0);
return false;
}
private JedisPubSub subscribeDelete() {
if (pubSub.isSubscribed()) {
return pubSub;
}
executorService.submit(new Runnable() {
@Override
public void run() {
Jedis jedis1 = null;
try {
jedis1 = jedisPool.getResource();
jedis1.subscribe(pubSub, lockKey + "-channel");
} finally {
closeJedis(jedis1);
}
}
});
return pubSub;
}
@Override
public void unlock() {
innerLock.unlock();
if (holdCount.get() > 0) {
final int countValue = holdCount.decrementAndGet();
if (countValue != 0) {
return;
}
if (System.currentTimeMillis() - firstAccireTime >= leaseTime) {
return;
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(lockKey);
jedis.publish(lockKey + "-channel", "DELETE");
} finally {
closeJedis(jedis);
}
}
}
private void closeJedis(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
/**
* 获取远程锁
*
* @param jedis
* @param leaseTime
* @param unit
* @return
*/
private boolean getRemoteLock(Jedis jedis, long leaseTime, TimeUnit unit) {
String setValue = uuid + Thread.currentThread().getId();
Long setnx = jedis.setnx(lockKey, setValue);
if (setnx.equals(1L)) {
long toMillis = unit.toMillis(leaseTime);
jedis.pexpire(lockKey, toMillis);
firstAccireTime = System.currentTimeMillis();
this.leaseTime = leaseTime;
holdCount.incrementAndGet();
return true;
}
String value = jedis.get(lockKey);
if (StringUtils.equals(setValue, value)) {
holdCount.incrementAndGet();
return true;
}
return false;
}
}