基於redis的 / 基於zookeeper的分佈式鎖方案

分佈式系統中,要保證某個資源在一時間段內只能有一個進程訪問,需要使用分佈式鎖。

方案一:基於redis的分佈式鎖

我們首先介紹三個使用到的redis方法SETNX()、GET()、GETSET()。

setnx ( key, value ):SET if Not Exists,該方法是原子的。若 key 不存在,則設置當前 key 成功,返回 1;若 key 已存在,則設置當前 key 失敗,返回 0。

getset ( key,newValue ):該方法是原子的,對 key 設置 newValue 新值,返回 key 原來的舊值。若原來無值,則返回null。

使用步驟

1、setnx(lockkey, 當前時間+過期超時時間),如果返回 1,則獲取鎖成功;如果返回 0 則沒有獲取到鎖,轉向 2

2、get(lockkey) 獲取值 oldExpireTime ,並將這個 value 值與當前的系統時間進行比較,如果小於當前系統時間,則認爲這個鎖已經超時,可以允許別的請求重新獲取,轉向 3

3、計算 newExpireTime = 當前時間+過期超時時間,然後 getset(lockkey, newExpireTime) ,返回當前 lockkey 的(舊)值currentExpireTime。
判斷 currentExpireTime 與 oldExpireTime 是否相等,如果相等,則當前 getset 設置成功,獲取到了鎖;如果不相等,說明這個鎖又被別的請求獲取走了,那麼當前請求可以直接返回失敗,或者繼續重試。

4、在獲取到鎖之後,當前線程可以開始自己的業務處理,處理完畢後,比較自己的處理時間和對於鎖設置的超時時間,如果小於鎖設置的超時時間,則直接執行 delete 釋放鎖;如果大於鎖設置的超時時間,無需處理。

//redis分佈式鎖
public final class RedisLockUtil {

	private static final int defaultExpire = 60;

	private RedisLockUtil() {
		//
	}

	public static boolean lock(String key) {
		return lock2(key, defaultExpire);
	}

	/**
	 * 加鎖
	 * 
	 * @param key    redis key
	 * @param expire 過期時間,單位秒
	 * @return true:加鎖成功,false,加鎖失敗
	 */
	public static boolean lock2(String key, int expire) {

		RedisService redisService = SpringUtils.getBean(RedisService.class);

		long value = System.currentTimeMillis() + expire;
		long status = redisService.setnx(key, String.valueOf(value));

		if (status == 1) {
			return true;
		}
		// 獲取舊值
		long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
		if (oldExpireTime < System.currentTimeMillis()) {
			// 超時
			long newExpireTime = System.currentTimeMillis() + expire;
			// 當前舊值
			long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
			if (currentExpireTime == oldExpireTime) {
				return true;
			}
		}
		return false;
	}

	public static void unLock2(String key) {
		RedisService redisService = SpringUtils.getBean(RedisService.class);
		long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
		if (oldExpireTime > System.currentTimeMillis()) {
			redisService.del(key);
		}
	}

	
	
	
	//——————————————附:使用SETNX()、EXPIRE()做分佈式鎖——————————————————
	// 此種方案不完善。比如在expire()命令執行成功前,發生了宕機的現象,那就會出現死鎖的問題
	/**
	 * 加鎖
	 * 
	 * @param key    redis key
	 * @param expire 過期時間,單位秒
	 * @return true:加鎖成功,false,加鎖失敗
	 */
	public static boolean lock(String key, int expire) {

		RedisService redisService = SpringUtils.getBean(RedisService.class);
		long status = redisService.setnx(key, "1");

		if (status == 1) {
			redisService.expire(key, expire);
			return true;
		}

		return false;
	}

	public static void unLock(String key) {
		RedisService redisService = SpringUtils.getBean(RedisService.class);
		redisService.del(key);
	}
}

使用鎖:

public void drawRedPacket(long userId) {
    String key = "draw.redpacket.userid:" + userId;

    boolean lock = RedisLockUtil.lock2(key, 50);
    if(lock) {
        try {
            //領取操作
        } finally {
            //釋放鎖
            RedisLockUtil.unLock2(key);
        }
    } else {
        new RuntimeException("重複領取獎勵");
    }
}

優點: 性能高

缺點:失效時間設置多長時間爲好?如何設置的失效時間太短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。如果設置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間。

方案二:基於zookeeper的分佈式鎖

1.在 /lock 節點下創建一個有序臨時節點 (EPHEMERAL_SEQUENTIAL)。
2.判斷創建的節點序號是否最小,如果是最小則獲取鎖成功。不是則取鎖失敗,然後 watch 序號比本身小的前一個節點。
3.當取鎖失敗,設置 watch 後則等待 watch 事件到來後,再次判斷是否序號最小。
4.取鎖成功則執行代碼,最後釋放鎖(刪除該節點)。

public class DistributedLock implements Lock, Watcher {
	private ZooKeeper zk;
	private String root = "/locks";// 根
	private String lockName;// 競爭資源的標誌
	private String waitNode;// 等待前一個鎖
	private String myZnode;// 當前鎖
	private CountDownLatch latch;// 計數器
	private int sessionTimeout = 30000;
	private List<Exception> exception = new ArrayList<Exception>();

	/**
	 * 創建分佈式鎖,使用前請確認config配置的zookeeper服務可用
	 * 
	 * @param config   127.0.0.1:2181
	 * @param lockName 競爭資源標誌,lockName中不能包含單詞lock
	 */
	public DistributedLock(String config, String lockName) {
		this.lockName = lockName;
		// 創建一個與服務器的連接
		try {
			zk = new ZooKeeper(config, sessionTimeout, this);
			Stat stat = zk.exists(root, false);
			if (stat == null) {
				// 創建根節點
				zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			}
		} catch (IOException e) {
			exception.add(e);
		} catch (KeeperException e) {
			exception.add(e);
		} catch (InterruptedException e) {
			exception.add(e);
		}
	}

	/**
	 * zookeeper節點的監視器
	 */
	public void process(WatchedEvent event) {
		if (this.latch != null) {
			this.latch.countDown();
		}
	}

	public void lock() {
		if (exception.size() > 0) {
			throw new LockException(exception.get(0));
		}
		try {
			if (this.tryLock()) {
				System.out.println("Thread-" + Thread.currentThread().getName() + " " + myZnode + " get lock true");
				return;
			} else {
				waitForLock(waitNode, sessionTimeout);// 等待鎖
			}
		} catch (KeeperException e) {
			throw new LockException(e);
		} catch (InterruptedException e) {
			throw new LockException(e);
		}
	}

	public boolean tryLock() {
		try {
			String splitStr = "_lock_";
			if (lockName.contains(splitStr))
				throw new LockException("lockName can not contains \\u000B");
			// 創建臨時子節點
			myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
					CreateMode.EPHEMERAL_SEQUENTIAL);
			System.out.println(myZnode + " is created ");
			// 取出所有子節點
			List<String> subNodes = zk.getChildren(root, false);
			// 取出所有lockName的鎖
			List<String> lockObjNodes = new ArrayList<String>();
			for (String node : subNodes) {
				String _node = node.split(splitStr)[0];
				if (_node.equals(lockName)) {
					lockObjNodes.add(node);
				}
			}

			Collections.sort(lockObjNodes);

			if (myZnode.equals(root + "/" + lockObjNodes.get(0))) {
				// 如果是最小的節點,則表示取得鎖
				return true;
			}
			// 如果不是最小的節點,找到比自己小1的節點
			String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
			System.out.println("subMyZnode=" + subMyZnode);
			waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
			System.out.println("waitNode=" + waitNode);
		} catch (KeeperException e) {
			throw new LockException(e);
		} catch (InterruptedException e) {
			throw new LockException(e);
		}
		return false;
	}

	public boolean tryLock(long time, TimeUnit unit) {
		try {
			if (this.tryLock()) {
				return true;
			}
			return waitForLock(waitNode, time);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

	private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
		// 判斷比自己小一個數的節點是否存在,如果不存在則無需等待鎖,同時註冊監聽
		Stat stat = zk.exists(root + "/" + lower, true);
		if (stat != null) {
			System.out.println("Thread-" + Thread.currentThread().getName() + " waiting for " + root + "/" + lower);
			this.latch = new CountDownLatch(1);
			this.latch.await(waitTime, TimeUnit.MILLISECONDS);
			this.latch = null;
		}
		return true;
	}

	public void unlock() {
		try {
			System.out.println(Thread.currentThread().getName() + " unlock " + myZnode);
			zk.delete(myZnode, -1);
			myZnode = null;
			zk.close();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (KeeperException e) {
			e.printStackTrace();
		}
	}

	public void lockInterruptibly() throws InterruptedException {
		this.lock();
	}

	public Condition newCondition() {
		return null;
	}

	public class LockException extends RuntimeException {
		private static final long serialVersionUID = 1L;

		public LockException(String e) {
			super(e);
		}

		public LockException(Exception e) {
			super(e);
		}
	}

	public static void main(String[] args) {

		final DistributedLock client1 = new DistributedLock("127.0.0.1", "lName");
		final DistributedLock client2 = new DistributedLock("127.0.0.1", "lName");

		new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				client1.lock();
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				client1.unlock();

			}
		}, "t1").start();

		// 確保t1啓動
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}

		new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				client2.lock();
				System.out.println("do something");
				client2.unlock();
			}
		}, "t2").start();
	}

}

優點:
有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現起來較爲簡單。

缺點:
性能上可能並沒有緩存服務那麼高,因爲每次在創建鎖和釋放鎖的過程中,都要動態創建、銷燬臨時節點來實現鎖功能。ZK 中創建和刪除節點只能通過 Leader 服務器來執行,然後將數據同步到所有的 Follower 機器上。還需要對 ZK的原理有所瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章