Redis和ZooKeeper的分佈式鎖實現

github地址: Redis和ZooKeeper對於分佈式鎖的實現

Redis分佈式鎖

客戶端在讀寫redis之前必須先從redis獲取鎖, 只有獲取到鎖的客戶端才能讀寫redis, 而其他沒有獲取到鎖的客戶端, 會以每秒一次的頻率不斷地去嘗試獲取鎖.

(1) 獲取鎖

SET my_lock 隨機值 PX 5000 NX

PX是設置過期時間, 單位毫秒. NX是僅當key不存在時才設置值.

(2) 刪除鎖

只有提供的value值相同才能刪除鎖, 因爲我們不能讓客戶端刪除別人的鎖. 因爲涉及到條件判斷, 爲了保證事務特性, 必須使用Lua腳本.

if redis.call("get",KEYS[1]) == ARGV[1] then
	return redis.call("del", KEYS[1])
else
    return 0
end

(3) Java實例

public class RedisDistributeLock {
	private static Logger logger = LoggerFactory.getLogger(RedisDistributeLock.class);

	private Jedis jedis;

	public RedisDistributeLock(String host) {
		JedisPool jedisPool = new JedisPool(host, 6379);
		jedis = jedisPool.getResource();
	}

	/**
	 * 獲取鎖
	 * @param key
	 * @param value
	 * @param expireTime 過期時間, 單位毫秒
	 */
	public void getLock(String key, String value, long expireTime) {
		try {
			SetParams params = new SetParams();
			params.px(expireTime);
			params.nx();
			while (true) {
				// 舊版本的Jedis使用命令: String result = jedis.set(key, value, "NX", "PX", 100);
				String result = jedis.set(key, value, params);
				if ("OK".equals(result)) {
					return;
				}
				Thread.sleep(100L);
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 釋放鎖
	 * @param key
	 * @param value
	 */
	public void releaseLock(String key, String value) {
		String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
	}

	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 5; i++) {
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					RedisDistributeLock redisDistributeLock = new RedisDistributeLock("xxx.xx.xx.xxx");

					//獲取鎖, 沒有獲取到鎖就繼續嘗試獲取鎖
					String key = "my_lock";
					String value = UUID.randomUUID().toString();
					redisDistributeLock.getLock(key, value, 200L);
					try {
						logger.info(Thread.currentThread().getName() + " 進行扣減庫存操作...");
					} catch (Exception e) {
						logger.error("處理業務邏輯報錯", e);
					}finally {
						//釋放鎖
						redisDistributeLock.releaseLock(key, value);
					}
				}
			});
		}
	}
}

ZooKeeper分佈式鎖

(1) 使用臨時節點實現

  • 所有客戶端都去/exclusive_lock節點下創建臨時子節點/exclusive_lock/myLock.
  • 只有一個客戶端能創建成功, 表示該客戶端拿到了鎖.
  • 其他沒有創建成功的客戶端在/exclusive_lock/myLock節點上註冊一個監聽器
  • 當獲取到鎖的客戶端宕機或正常完成業務邏輯後, 臨時節點/exclusive_lock/myLock會被刪除.
  • 其他客戶端都會收到通知, 重新去創建臨時節點/exclusive_lock/myLock.

這種實現有兩個問題: 羊羣效應和鎖公平性問題, 即每次當臨時節點被刪除後, 其他客戶端都會去獲取鎖, 且上一次獲取鎖的順序無效.

(2) 使用臨時順序節點實現

我們可以ZooKeeper的臨時順序節點來解決上面的兩個問題.

  • 所有客戶端都去/exclusive_lock節點下創建臨時順序子節點/exclusive_lock/myLock.
  • 然後再對這些臨時順序節點按字典序進行排序.
  • 排在第一個的臨時順序節點對應的客戶端獲取到鎖.
  • 其他客戶端在排自己前面的臨時順序節點上註冊一個監聽器.
  • 當獲取到鎖的客戶端的釋放鎖之後, ZK會通過監聽器通知下一個臨時順序節點對應的客戶端獲取到鎖.

下面我們來看下具體實現:

public class ZkDistributeLock {
	private static Logger logger = LoggerFactory.getLogger(ZkDistributeLock.class);

	/**
	 * 分佈式鎖的根節點路徑
	 */
	private String rootLockPath = "/exclusive_lock";
	/**
	 * 分佈式鎖節點路徑
	 */
	private String lockPath;
	/**
	 * 分佈式鎖名
	 */
	private String lockName;
	private ZooKeeper zk;

	/**
	 * 連接zk, 並創建分佈式鎖的根節點
	 * @param host zk服務地址
	 * @param lockName 分佈式鎖名
	 */
	public ZkDistributeLock(String host, String lockName) {
		try {
			CountDownLatch connectedSignal = new CountDownLatch(1);
			zk = new ZooKeeper(host, 5000, new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					if (event.getState() == Event.KeeperState.SyncConnected) {
						connectedSignal.countDown();
					}
				}
			});
			//因爲監聽器是異步操作, 要保證監聽器操作先完成, 即要確保先連接上ZooKeeper再返回實例.
			connectedSignal.await();

			//創建鎖的根節點(持久節點)
			if (zk.exists(rootLockPath, false) == null) {
				zk.create(rootLockPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			}

			//指定分佈式鎖節點路徑
			this.lockName = lockName;
		} catch (Exception e) {
			logger.error("connect zookeeper server error.", e);
		}
	}

	/**
	 * 獲取鎖
	 * 在業務中獲取到鎖後才能繼續往下執行, 否則堵塞, 直到獲取到鎖
	 */
	public void getLock() {
		try {
			//創建分佈式鎖的臨時順序節點
			lockPath = zk.create(rootLockPath + "/" + lockName, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

			//取出所有分佈式鎖的臨時順序節點, 然後排序
			List<String> children = zk.getChildren(rootLockPath, false);
			TreeSet<String> sortedChildren = new TreeSet<>();
			for (String child : children) {
				sortedChildren.add(rootLockPath + "/" + child);
			}

			//如果當前客戶端創建的順序節點是第一個, 則獲取到鎖
			String firstNode = sortedChildren.first();
			if (firstNode.equals(lockPath)) {
				return;
			}

			//如果當前客戶端沒有獲取到鎖, 則在前一個臨時順序節點上加一個監聽器
			String lowerNode = sortedChildren.lower(lockPath);
			CountDownLatch latch = new CountDownLatch(1);
			if (StringUtils.isBlank(lowerNode)) {
				return;
			}
			Stat stat = zk.exists(lowerNode, new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					//當前一個臨時順序節點被刪除後, 當前客戶端就獲取到鎖(這樣就保證了鎖的公平性)
					if (event.getType() == Event.EventType.NodeDeleted) {
						latch.countDown();
					}
				}
			});
			if (stat != null) {
				latch.await();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 釋放鎖
	 */
	public void releaseLock() {
		try {
			zk.delete(lockPath, -1);
		} catch (Exception e) {
			logger.error("release lock error.", e);
		}
	}

	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 5; i++) {
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					ZkDistributeLock zkDistributeLock = new ZkDistributeLock("xxx.xx.xx.xxx:2181", "myLock");

					//獲取鎖, 沒有獲取到鎖就一直等待
					zkDistributeLock.getLock();
					try {
						logger.info(Thread.currentThread().getName() + " 進行扣減庫存操作...");
					} catch (Exception e) {
						logger.error("處理業務邏輯報錯", e);
					}finally {
						//釋放鎖
						zkDistributeLock.releaseLock();
					}
				}
			});
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章