分布式鎖
分布式鎖,這個主要得益於 ZooKeeper 為我們保證了數據的強一致性。鎖服務可以分為兩類,一個是 保持獨占,另一個是 控制時序。
1. 所謂保持獨占,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把 zk 上的一個 znode 看作是一把鎖,通過 create znode 的方式來實現。所有客戶端都去創建 /distribute_lock 節點,最終成功創建的那個客戶端也即擁有了這把鎖。
2. 控制時序,就是所有視圖來獲取這個鎖的客戶端,最終都是會被安排執行,只是有個全局時序了。做法和上面基本類似,只是這里 /distributelock 已經預先存在,客戶端在它下面創建臨時有序節點(這個可以通過節點的屬性控制:CreateMode.EPHEMERALSEQUENTIAL 來指定)。Zk 的父節點(/distribute_lock)維持一份 sequence, 保證子節點創建的時序性,從而也形成了每個客戶端的全局時序。
分布式鎖 單純的Lock鎖或者synchronize只能解決單個jvm線程安全問題
分布式 Session 一致性問題
分布式全局id(也可以使用分布式鎖)
分布式鎖,產生的原因是 集群
在單台服務器上 如何生成訂單號(保證唯一),方案 UUid+時間戳方式, redis方式
生成訂單號, 秒殺搶購時候,
首先預測100w訂單號,生成放在redis。客戶端下單,直接redis去獲取即可。因為redis單線程的,多個線程去獲取時候,安全呀。
實際150w用戶。當redis剩下50w訂單號時候,繼續生成補充之。
如果在集群情況,UUid+時間戳。不能保證唯一性!,原因:
如果單台:
uuid+時間戳,生成的代碼邏輯:
package com.toov5.Lock; import java.text.SimpleDateFormat; import java.util.Date; //生成訂單號 時間戳 public class OrderNumGenerator { //區分不同的訂單號 private static int count = 0; //單台服務器,多個線程 同事生成訂單號 public String getNumber(){ try { Thread.sleep(300); } catch (Exception e) { // TODO: handle exception } SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); return simpt.format(new Date()) + "-" + ++count; //時間戳后面加了 count } }
開啟100個線程調用之:
package com.toov5.Lock; public class OrderService implements Runnable { private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); //定義成全局的 public void run() { getNumber(); } public void getNumber(){ String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName()+"num"+number); } public static void main(String[] args) { OrderService orderService = new OrderService(); for (int i = 0; i <100; i++) { //開啟100個線程 new Thread(orderService).start(); } } }
結果:
多個線程共享區同一個全局變量,線程安全問題!
解決方案就是加鎖嘛!
或者使用 lock鎖也可以
public class OrderService implements Runnable { private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); // 使用lock鎖 private java.util.concurrent.locks.Lock lock = new ReentrantLock(); public void run() { getNumber(); } public void getNumber() { try { // synchronized (this) { lock.lock(); String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number); // } } catch (Exception e) { } finally { lock.unlock(); } } public static void main(String[] args) { System.out.println("####生成唯一訂單號###"); OrderService orderService = new OrderService(); for (int i = 0; i < 100; i++) { new Thread(orderService).start(); } } }
如果是集群環境下:
每台jvm都有一個 count 都有 自增的代碼 操作這個 count 三個不同的jvm 獨立的 用戶請求 過來 映射到哪個 就操作哪個
這時候就產生分布式鎖的問題
這時候需要分布式鎖:共享一個count
jvm1 操作時候 其他的jvm2 和 jvm3 不可以操作他!
分布式鎖 保證分布式領域中共享數據安全問題
1、數據庫實現(效率低,不推薦)
2、redis實現(使用redission實現,但是需要考慮思索,釋放問題。繁瑣一些)
3、Zookeeper實現 (使用臨時節點,效率高,失效時間可以控制)
4、Spring Cloud 實現全局鎖(內置的)
業務場景
在分布式情況,生成全局訂單號ID
產生問題
在分布式(集群)環境下,每台JVM不能實現同步,在分布式場景下使用時間戳生成訂單號可能會重復
分布式情況下,怎么解決訂單號生成不重復
- 使用分布式鎖
- 提前生成好,訂單號,存放在redis取。獲取訂單號,直接從redis中取。
使用分布式鎖生成訂單號技術
1.使用數據庫實現分布式鎖
缺點:性能差、線程出現異常時,容易出現死鎖
2.使用redis實現分布式鎖
缺點:鎖的失效時間難控制、容易產生死鎖、非阻塞式、不可重入
3.使用zookeeper實現分布式鎖
實現相對簡單、可靠性強、使用臨時節點,失效時間容易控制
什么是分布式鎖
分布式鎖一般用在分布式系統或者多個應用中,用來控制同一任務是否執行或者任務的執行順序。在項目中,部署了多個tomcat應用,在執行定時任務時就會遇到同一任務可能執行多次的情況,我們可以借助分布式鎖,保證在同一時間只有一個tomcat應用執行了定時任務
使用Zookeeper實現分布式鎖
Zookeeper實現分布式鎖原理
使用zookeeper創建臨時序列節點來實現分布式鎖,適用於順序執行的程序,大體思路就是創建臨時序列節點,找出最小的序列節點,獲取分布式鎖,程序執行完成之后此序列節點消失,通過watch來監控節點的變化,從剩下的節點的找到最小的序列節點,獲取分布式鎖,執行相應處理,依次類推……
如何使用zk實現分布式鎖?
臨時節點
持久節點
分布式鎖使用 臨時節點,實現:
實現步驟:
多個Jvm同時在Zookeeper上創建同一個相同的節點( /Lock)
zk節點唯一的! 不能重復!節點類型為臨時節點, jvm1創建成功時候,jvm2和jvm3創建節點時候會報錯,該節點已經存在。這時候 jvm2和jvm3進行等待。
jvm1的程序現在執行完畢,執行釋放鎖。關閉當前會話。臨時節點不復存在了並且事件通知Watcher,jvm2和jvm3繼續創建。
ps:zk強制關閉時候,通知會有延遲。但是close()方法關閉時候,延遲小
如果程序一直不處理完,可能導致思索(其他的一直等待)。設置有效期~ 直接close()掉 其實連接也是有有效期設置的 大家可以找下相關資料看下哦
上代碼!
引入Jar包:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.toov5.FbsLock</groupId> <artifactId>FbsLock</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.10</version> </dependency> </dependencies> </project>
創建鎖的接口
package com.toov5.Lock; public interface ExtLock { //ExtLock基於zk實現分布式鎖 public void getLock(); //釋放鎖 public void unLock(); }
模板方法模式
package com.toov5.Lock; import org.I0Itec.zkclient.ZkClient; //將重復代碼抽象到子類中(模板方法設計模式) public abstract class ZookeeperAbstractLock implements ExtLock { private static final String CONNECTION="192.168.91.5:2181"; protected ZkClient zkClient = new ZkClient(CONNECTION); private String lockPath="/lockPath"; //獲取鎖 public void getLock() { //1、連接zkClient 創建一個/lock的臨時節點 // 2、 如果節點創建成果,直接執行業務邏輯,如果節點創建失敗,進行等待 if (tryLock()) { System.out.println("#####成功獲取鎖######"); }else { //進行等待 waitLock(); } //3、使用事件通知監聽該節點是否被刪除 ,如果是,重新進入獲取鎖的資源 } //創建失敗 進行等待 abstract void waitLock(); abstract boolean tryLock(); //釋放鎖 public void unLock() { //執行完畢 直接連接 if (zkClient != null) { zkClient.close(); System.out.println("######釋放鎖完畢######"); } } }
創建子類實現上面的 抽象方法
package com.toov5.Lock; import java.util.concurrent.CountDownLatch; import org.I0Itec.zkclient.IZkDataListener; public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock { @Override boolean tryLock() { try { zkClient.createEphemeral(lockPath); // System.out.println("#########獲取鎖######"); return true; } catch (Exception e) { // 如果失敗 直接catch return false; } } @Override void waitLock() { IZkDataListener iZkDataListener = new IZkDataListener() { // 節點被刪除 public void handleDataDeleted(String arg0) throws Exception { if (countDownLatch != null) { countDownLatch.countDown(); // 計數器為0的情況,await 后面的繼續執行 } } // 節點被修改 public void handleDataChange(String arg0, Object arg1) throws Exception { } }; // 監聽事件通知 zkClient.subscribeDataChanges(lockPath, iZkDataListener); // 控制程序的等待 if (zkClient.exists(lockPath)) { //如果 檢查出 已經被創建了 就new 然后進行等待 countDownLatch = new CountDownLatch(1); try { countDownLatch.wait(); //等待時候 就不往下走了 當為0 時候 后面的繼續執行 } catch (Exception e) { // TODO: handle exception } } //后面代碼繼續執行 //為了不影響程序的執行 建議刪除該事件監聽 監聽完了就刪除掉 zkClient.unsubscribeDataChanges(lockPath, iZkDataListener); } }
生產訂單號:
package com.toov5.Lock; import java.text.SimpleDateFormat; import java.util.Date; //生成訂單號 時間戳 public class OrderNumGenerator { //區分不同的訂單號 private static int count = 0; //單台服務器,多個線程 同事生成訂單號 public String getNumber(){ try { Thread.sleep(500); } catch (Exception e) { } SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); return simpt.format(new Date()) + "-" + ++count; //時間戳后面加了 count } }
運行方法:
package com.toov5.Lock; public class OrderService implements Runnable { private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); // 定義成全局的 private ExtLock lock = new ZookeeperDistrbuteLock(); public void run() { getNumber(); } public synchronized void getNumber() { // 加鎖 保證線程安全問題 讓一個線程操作 try { lock.getLock(); String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName() + ",number" + number); } catch (Exception e) { } finally { lock.unLock(); } } public static void main(String[] args) { // OrderService orderService = new OrderService(); for (int i = 0; i < 100; i++) { // 開啟100個線程 //模擬分布式鎖的場景 new Thread(new OrderService()).start(); } } }
運行結果:
代碼欣賞: