【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

回到顶部