/* * Copyright (c) 2017 Baidu, Inc. All Rights Reserve. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.baidu.fsg.dlock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.LockSupport; import com.baidu.fsg.dlock.domain.DLockConfig; import com.baidu.fsg.dlock.domain.DLockEntity; import com.baidu.fsg.dlock.domain.DLockStatus; import com.baidu.fsg.dlock.exception.DLockProcessException; import com.baidu.fsg.dlock.exception.OptimisticLockingException; import com.baidu.fsg.dlock.processor.DLockProcessor; import com.baidu.fsg.dlock.utils.NetUtils; /** * DistributedReentrantLock implements the lock,tryLock syntax of {@link Lock} by different mechanisms:<br> * <li>database</li> * The database synchronization primitives(line lock with conditional "UPDATE" statement). * * <li>redis</li> * The Atomic redis command & Lua script, guaranteed the atomic operations.<br> * The expire mechanisms of redis, guaranteed the lock will be released without expanding lease request, * so that the other competitor can try to lock.<p> * * We use a variant of CLH lock queue for the competitor threads, provides an unfair implement to make high * throughput. * * @author chenguoqing * @author yutianbao */ public class DistributedReentrantLock implements Lock { /** * Lock configuration */ private final DLockConfig lockConfig; /** * Lock processor */ private final DLockProcessor lockProcessor; /** * Head of the wait queue, lazily initialized. Except for initialization, it is modified only via method setHead. * Note: If head exists, its waitStatus is guaranteed not to be CANCELLED. */ private final AtomicReference<Node> head = new AtomicReference<>(); /** * Tail of the wait queue, lazily initialized. Modified only via method enq to add new wait node. */ private final AtomicReference<Node> tail = new AtomicReference<>(); /** * The current owner of exclusive mode synchronization. */ private final AtomicReference<Thread> exclusiveOwnerThread = new AtomicReference<>(); /** * Retry thread reference */ private final AtomicReference<RetryLockThread> retryLockRef = new AtomicReference<>(); /** * Expand lease thread reference */ private final AtomicReference<ExpandLockLeaseThread> expandLockRef = new AtomicReference<>(); /** * Once a thread hold this lock, the thread can reentrant the lock. * This value represents the count of holding this lock. Default as 0 */ private final AtomicInteger holdCount = new AtomicInteger(0); /** * CLH Queue Node for holds all parked thread */ static class Node { final AtomicReference<Node> prev = new AtomicReference<>(); final AtomicReference<Node> next = new AtomicReference<>(); final Thread t; Node() { this(null); } Node(Thread t) { this.t = t; } } /** * Constructor with lock configuration and lock processor */ public DistributedReentrantLock(DLockConfig lockConfig, DLockProcessor lockProcessor) { this.lockConfig = lockConfig; this.lockProcessor = lockProcessor; } @Override public Condition newCondition() { throw new UnsupportedOperationException(); } @Override public void lockInterruptibly() throws InterruptedException { throw new UnsupportedOperationException(); } @Override public void lock() { // lock db record if (!tryLock()) { acquireQueued(addWaiter()); } } final void acquireQueued(final Node node) { for (;;) { final Node p = node.prev.get(); if (p == head.get() && tryLock()) { head.set(node); p.next.set(null); // help GC break; } // if need, start retry thread if (exclusiveOwnerThread.get() == null) { startRetryThread(); } // park current thread LockSupport.park(this); } } private Node addWaiter() { Node node = new Node(Thread.currentThread()); // Try the fast path of enq; backup to full enq on failure Node pred = tail.get(); if (pred != null) { node.prev.set(pred); if (tail.compareAndSet(pred, node)) { pred.next.set(node); return node; } } enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail.get(); if (t == null) { // Must initialize Node h = new Node(); // Dummy header h.next.set(node); node.prev.set(h); if (head.compareAndSet(null, h)) { tail.set(node); return h; } } else { node.prev.set(t); if (tail.compareAndSet(t, node)) { t.next.set(node); return t; } } } } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { throw new UnsupportedOperationException(); } /** * Lock redis record through the atomic command Set(key, value, NX, PX, expireTime), only one request will success * while multiple concurrently requesting. */ @Override public boolean tryLock() { // current thread can reentrant, and locked times add once if (Thread.currentThread() == this.exclusiveOwnerThread.get()) { this.holdCount.incrementAndGet(); return true; } DLockEntity newLock = new DLockEntity(); newLock.setLockTime(System.currentTimeMillis()); newLock.setLocker(generateLocker()); newLock.setLockStatus(DLockStatus.PROCESSING); boolean locked = false; try { // get lock directly lockProcessor.updateForLock(newLock, lockConfig); locked = true; } catch (OptimisticLockingException | DLockProcessException e) { // NOPE. Retry in the next round. } if (locked) { // set exclusive thread this.exclusiveOwnerThread.set(Thread.currentThread()); // locked times reset to one this.holdCount.set(1); // shutdown retry thread shutdownRetryThread(); // start the timer for expand lease time startExpandLockLeaseThread(newLock); } return locked; } /** * Attempts to release this lock.<p> * * If the current thread is the holder of this lock then the hold * count is decremented. If the hold count is now zero then the lock * is released. If the current thread is not the holder of this * lock then {@link IllegalMonitorStateException} is thrown. * * @throws IllegalMonitorStateException if the current thread does not * hold this lock */ @Override public void unlock() throws IllegalMonitorStateException { // lock must be hold by current thread if (Thread.currentThread() != this.exclusiveOwnerThread.get()) { throw new IllegalMonitorStateException(); } // lock is still be hold if (holdCount.decrementAndGet() > 0) { return; } // clear remote lock DLockEntity currentLock = new DLockEntity(); currentLock.setLocker(generateLocker()); currentLock.setLockStatus(DLockStatus.PROCESSING); try { // release remote lock lockProcessor.updateForUnlock(currentLock, lockConfig); } catch (OptimisticLockingException | DLockProcessException e) { // NOPE. Lock will deleted automatic after the expire time. } finally { // Release exclusive owner this.exclusiveOwnerThread.compareAndSet(Thread.currentThread(), null); // Shutdown expand thread shutdownExpandThread(); // wake up the head node for compete lock unparkQueuedNode(); } } /** * wake up the head node for compete lock */ private void unparkQueuedNode() { // wake up the head node for compete lock Node h = head.get(); if (h != null && h.next.get() != null) { LockSupport.unpark(h.next.get().t); } } /** * Generate current locker. IP_Thread ID */ private String generateLocker() { return NetUtils.getLocalAddress() + "-" + Thread.currentThread().getId(); } /** * Task for expanding the lock lease */ abstract class LockThread extends Thread { /** * Synchronizes */ final Object sync = new Object(); /** * Delay time for start(ms) */ final int delay; /** * Retry interval(ms) */ final int retryInterval; final AtomicInteger startState = new AtomicInteger(0); /** * Control variable for shutdown */ private boolean shouldShutdown = false; /** * Is first running */ private boolean firstRunning = true; LockThread(String name, int delay, int retryInterval) { setDaemon(true); this.delay = delay; this.retryInterval = retryInterval; setName(name + "-" + getId()); } @Override public void run() { while (!shouldShutdown) { synchronized (sync) { try { // first running, delay if (firstRunning && delay > 0) { firstRunning = false; sync.wait(delay); } // execute task execute(); // wait for interval sync.wait(retryInterval); } catch (InterruptedException e) { shouldShutdown = true; } } } // clear associated resources for implementations beforeShutdown(); } abstract void execute() throws InterruptedException; void beforeShutdown() { } } /** * Task for expanding the lock lease */ private class ExpandLockLeaseThread extends LockThread { final DLockEntity lock; ExpandLockLeaseThread(DLockEntity lock, int delay, int retryInterval) { super("ExpandLockLeaseThread", delay, retryInterval); this.lock = lock; } @Override void execute() throws InterruptedException { try { // set lock time lock.setLockTime(System.currentTimeMillis()); // update lock lockProcessor.expandLockExpire(lock, lockConfig); } catch (OptimisticLockingException e) { // if lock has been released, kill current thread throw new InterruptedException("Lock released."); } catch (DLockProcessException e) { // retry } } @Override void beforeShutdown() { expandLockRef.compareAndSet(this, null); } } private void startExpandLockLeaseThread(DLockEntity lock) { ExpandLockLeaseThread t = expandLockRef.get(); while (t == null || t.getState() == Thread.State.TERMINATED) { // set new expand lock thread int retryInterval = (int) (lockConfig.getMillisLease() * 0.75); expandLockRef.compareAndSet(t, new ExpandLockLeaseThread(lock, 1, retryInterval)); // retrieve the new expand thread instance t = expandLockRef.get(); } if (t.startState.compareAndSet(0, 1)) { t.start(); } } private void shutdownExpandThread() { ExpandLockLeaseThread t = expandLockRef.get(); if (t != null && t.isAlive()) { t.interrupt(); } } /** * Start when: (1) no threads hold lock; (2) CLH has waiting thread(s). And shutdown when one thread * posses the lock, because it does not has necessary to start retry thread. */ private class RetryLockThread extends LockThread { RetryLockThread(int delay, int retryInterval) { super("RetryLockThread", delay, retryInterval); } @Override void execute() throws InterruptedException { // if existing running thread, kill self if (exclusiveOwnerThread.get() != null) { throw new InterruptedException("Has running thread."); } Node h = head.get(); // no thread for lock, kill self if (h == null) { throw new InterruptedException("No waiting thread."); } boolean needRetry = false; try { needRetry = lockProcessor.isLockFree(lockConfig.getLockUniqueKey()); } catch (DLockProcessException e) { needRetry = true; } // if the lock has been releases or expired, re-competition if (needRetry) { // wake up the head node for compete lock unparkQueuedNode(); } } @Override void beforeShutdown() { retryLockRef.compareAndSet(this, null); } } /** * Start the retry thread */ private void startRetryThread() { RetryLockThread t = retryLockRef.get(); while (t == null || t.getState() == Thread.State.TERMINATED) { retryLockRef.compareAndSet(t, new RetryLockThread((int) (lockConfig.getMillisLease() / 10), (int) (lockConfig.getMillisLease() / 6))); t = retryLockRef.get(); } if (t.startState.compareAndSet(0, 1)) { t.start(); } } /** * Shutdown retry thread */ private void shutdownRetryThread() { RetryLockThread t = retryLockRef.get(); if (t != null && t.isAlive()) { t.interrupt(); } } }