【Java系列004】别小瞧了Redis分布式锁
Jedis为我们提供了便捷的分布式锁方法有setex和setnx,两者的区别在于setex可以设置超时时间(注意单位秒)。这里我们不用setex,而是用set自行指定nxxx和expx。释放锁,为了保证原子性操作,我们使用LUA命令。具体看下面2部分代码。
/** * NX-Only set the key if it does not already exist.
* XX -- Only set the key if it already exist.
*/
private static final String SET_IF_NOT_EXIST = new String("NX");
/**
* EX|PX, expire time units: EX = seconds; PX = milliseconds
*/
private static final String SET_WITH_EXPIRE_TIME = new String("PX");
private static final String LOCK_OK = new String("OK");
private static final Long RELEASE_SUCCESS = new Long(1);/**
* 尝试获得锁
*
* @param jedis
* @param lockKey
* @param value
* @param milliseconds(毫秒)
* @return
*/
private static Boolean tryGetLock(Jedis jedis, String lockKey, String value, int milliseconds) {
String result = jedis.set(lockKey, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, milliseconds);
if (LOCK_OK.equals(result)) {
log.info("获得锁,线程名称==" + Thread.currentThread().getName());
return true;
}
log.info("未获得锁,线程名称==" + Thread.currentThread().getName());
return false;
}
/**
* 释放锁
*
* @param jedis
* @param lockKey
* @param value
* @return
*/
private static Boolean releaseLock(Jedis jedis, String lockKey, String value) {
String script = "if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end";
Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
log.info("释放锁,线程名称==" + Thread.currentThread().getName());
return true;
}
log.info("未释放锁,线程名称==" + Thread.currentThread().getName());
return false;
}
下面我们模拟多线程并发获取锁资源。
public static void main(String[] args) { String lockKey = "LF-TEST:DISTRIBUTION:LOCK";
String value = "123";
int maxThread = 1000;
ExecutorService fixedCacheThreadPool = Executors.newFixedThreadPool(maxThread);
CountDownLatch cdLatch = new CountDownLatch(maxThread);
for (int i = 0; i < maxThread; i++) {
fixedCacheThreadPool.execute(() -> {
RedisUtil redisUtil = RedisUtil.getInstance();
Jedis jedis = redisUtil.getJedis();
try {
if(tryGetLock(jedis, lockKey, value, 200)){
TimeUnit.MILLISECONDS.sleep(100);
releaseLock(jedis, lockKey, value);
}
} catch (Exception ex) {
log.error("获取或释放锁有误:", ex);
} finally {
if (null != jedis) {
jedis.close();//归还连接
}
}
cdLatch.countDown();
});
}
try {
cdLatch.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("cdLatch 异常:", e);
}
}
写完用例,我们执行程序,把日志拿出来统计获得锁的是3个线程,释放锁只有2个线程。
难道还有一个线程没有释放锁?其实,并不是,这是个并发问题,由于每个线程的key和value是一样的,刚好线程A在准备释放锁的时候,刚好也超时(锁释放),这个时候线程B获得锁,就这样线程A把原本属于线程B的锁给释放了。所以就算你写了LUA脚本也不要以为妥妥的(没经过单元测试的程序就是耍流氓)。
那我们要怎么改进呢?目的是要达到线程间隔离,而且value也要不一致。目的明确了,我们自然想到可以用ThreadLocal和UUID来达到目的。我们改进代码后试试。
private static final ThreadLocal<String> thdLocalLockValue = new ThreadLocal<>();
public static void main(String[] args) {
String lockKey = "LF-TEST:DISTRIBUTION:LOCK";
int maxThread = 1000;
ExecutorService fixedCacheThreadPool = Executors.newFixedThreadPool(maxThread);
CountDownLatch cdLatch = new CountDownLatch(maxThread);
for (int i = 0; i < maxThread; i++) {
fixedCacheThreadPool.execute(() -> {
if(Objects.isNull(thdLocalLockValue.get())){
thdLocalLockValue.set(StringUtils.replace(UUID.randomUUID().toString(), "-", ""));
}
RedisUtil redisUtil = RedisUtil.getInstance();
Jedis jedis = redisUtil.getJedis();
try {
if(tryGetLock(jedis, lockKey, thdLocalLockValue.get(), 200)){
TimeUnit.MILLISECONDS.sleep(100);
releaseLock(jedis, lockKey, thdLocalLockValue.get());
}
} catch (Exception ex) {
log.error("获取或释放锁有误:", ex);
} finally {
if (null != jedis) {
jedis.close();//归还连接
}
thdLocalLockValue.remove();
}
cdLatch.countDown();
});
}
try {
cdLatch.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("cdLatch 异常:", e);
}
}
把输出日志统计后,我们发现获得锁的线程数和释放锁的线程数是一致,多尝试几次后也一样(由于篇幅问题就不截图了,有兴趣的朋友可以撸完代码试试)。
总结
今天我们一起学习了Redis分布式锁的实现,以及遇到的坑,这个坑就是刚好锁过期和释放锁两个线程并发导致。因此我们采用ThreadLocal来解决线程间隔离和每次锁的资源不一样。当然我们都知道除了Redis能实现分布式锁之外,zookeeper可以使用“临时顺序节点”实现分布式锁,其实对于单库而言也可以通过乐观锁和悲观锁实现。
思考和讨论
1、上面提到Zookeeper和数据库乐观锁、悲观锁都能实现分布式锁,你了解过它们间的区别吗?
2、代码中为何jedis.close(),注释写着是归还,而不是关闭连接呢?
3、我们用到了ThreadLocal,为何最后要显式remove()?不remove会带来什么问题?
欢迎留言与我分享和指正!也欢迎你把这篇文章分享给你的朋友或同事,一起交流。
感谢您的阅读,我们下节再见!
扫码关注我们,与君共进
以上是 【Java系列004】别小瞧了Redis分布式锁 的全部内容, 来源链接: utcz.com/z/515114.html