Java 实现基于Redis的分布式可重入锁
之前在 Java实现基于的Redis的分布式锁 这篇文章中,已经实现了加锁的逻辑,但是有个缺点,就是不可重入,任何重入锁的尝试都会导致死锁的发生,想了一下,这个问题可以解决。
Thinking
如何实现可重入?
首先锁信息(指redis中lockKey关联的value值) 必须得设计的能负载更多信息,之前non-reentrant时value直接就是一个超时时间,但是要实现可重入单超时时间是不够的,必须要标识锁是被谁持有的,也就是说要标识分布式环境中的线程,还要记录锁被入了多少次。
如何在分布式线程中标识唯一线程?
MAC地址 + jvm进程ID + 线程ID(或者线程地址都行),三者结合即可唯一分布式环境中的线程。
实现
代码框架还是和之前实现的非重入的差不多,重点是lock方法,代码已有非常详细的注释
- package cc.lixiaohui.lock.redis;
- import java.io.IOException;
- import java.net.SocketAddress;
- import java.util.concurrent.TimeUnit;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import redis.clients.jedis.Jedis;
- import cc.lixiaohui.lock.AbstractLock;
- import cc.lixiaohui.lock.Lock;
- import cc.lixiaohui.lock.time.nio.client.TimeClient;
- import cc.lixiaohui.lock.util.LockInfo;
- /**
- * 基于Redis的SETNX操作实现的分布式锁, 获取锁时最好用tryLock(long time, TimeUnit unit), 以免网路问题而导致线程一直阻塞.
- * <a href="http://redis.io/commands/setnx">SETNC操作参考资料.</a>
- *
- * <p><b>可重入实现关键:</b>
- * <ul>
- * <li>在分布式环境中如何确定一个线程? <i><b>mac地址 + jvm pid + threadId</b></i> (mac地址唯一, jvm
- * pid在单机内唯一, threadId在单jvm内唯一)</li>
- * <li>任何一个线程从redis拿到value值后都需要能确定 该锁是否被自己持有, 因此value值要有以下特性: 保存持有锁的主机(mac), jvm
- * pid, 持有锁的线程ID, 重复持有锁的次数</li>
- * </ul></p>
- * <p>
- * redis中value设计如下(in json):
- * <pre>
- * {
- * expires : expire time in long
- * mac : mac address of lock holder's machine
- * pid : jvm process id
- * threadId : lock holder thread id
- * count : hold count(for use of reentrancy)
- * }
- * 由{@link LockInfo LockInfo}表示.
- * </pre>
- *
- * <b>Usage Example:</b>
- * <pre>
- * {@link Lock} lock = new {@link ReentrantLock}(jedis, "lockKey", lockExpires, timeServerAddr);
- * if (lock.tryLock(3, TimeUnit.SECONDS)) {
- * try {
- * // do something
- * } catch (Exception e) {
- * lock.unlock();
- * }
- * }
- * </pre>
- * </p>
- *
- * @author lixiaohui
- * @date 2016年9月15日 下午2:52:38
- *
- */
- public class ReentrantLock extends AbstractLock {
- private Jedis jedis;
- private TimeClient timeClient;
- // 锁的名字
- protected String lockKey;
- // 锁的有效时长(毫秒)
- protected long lockExpires;
- private static final Logger logger = LoggerFactory.getLogger(ReentrantLock.class);
- public ReentrantLock(Jedis jedis, String lockKey, long lockExpires, SocketAddress timeServerAddr) throws IOException {
- this.jedis = jedis;
- this.lockKey = lockKey;
- this.lockExpires = lockExpires;
- timeClient = new TimeClient(timeServerAddr);
- }
- // 阻塞式获取锁的实现
- protected boolean lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) throws InterruptedException {
- if (interrupt) {
- checkInterruption();
- }
- // 超时控制 的时间可以从本地获取, 因为这个和锁超时没有关系, 只是一段时间区间的控制
- long start = localTimeMillis();
- long timeout = unit.toMillis(time); // if !useTimeout, then it's useless
- // walkthrough
- // 1. lockKey未关联value, 直接设置lockKey, 成功获取到锁, return true
- // 2. lock 已过期, 用getset设置lockKey, 判断返回的旧的LockInfo
- // 2.1 若仍是超时的, 则成功获取到锁, return true
- // 2.2 若不是超时的, 则进入下一次循环重新开始 步骤1
- // 3. lock没过期, 判断是否是当前线程持有
- // 3.1 是, 则计数加 1, return true
- // 3.2 否, 则进入下一次循环重新开始 步骤1
- // note: 每次进入循环都检查 : 1.是否超时, 若是则return false; 2.是否检查中断(interrupt)被中断,
- // 若需检查中断且被中断, 则抛InterruptedException
- while (useTimeout ? !isTimeout(start, timeout) : true) {
- if (interrupt) {
- checkInterruption();
- }
- long lockExpireTime = serverTimeMillis() + lockExpires + 1;// 锁超时时间
- String newLockInfoJson = LockInfo.newForCurrThread(lockExpireTime).toString();
- if (jedis.setnx(lockKey, newLockInfoJson) == 1) { // 条件能成立的唯一情况就是redis中lockKey还未关联value
- // TODO 成功获取到锁, 设置相关标识
- logger.debug("{} get lock(new), lockInfo: {}", Thread.currentThread().getName(), newLockInfoJson);
- locked = true;
- return true;
- }