應用場景
在傳統(tǒng)單機部署的情況下,可以使用Java并發(fā)處理相關的API(如ReentrantLock或synchronized)進行互斥控制。這種Java提供的原生鎖機制可以保證在同一個Java虛擬機進程內(nèi)的多個線程同步執(zhí)行,避免出現(xiàn)無序現(xiàn)象。
但在互聯(lián)網(wǎng)場景,例如在商品秒殺過程中,隨著客戶業(yè)務量上升,整個系統(tǒng)并發(fā)飆升,需要多臺機器并發(fā)運行。例如當兩個用戶同時發(fā)起的請求分別落在兩個不同的機器上時,雖然這兩個請求可以同時執(zhí)行,但是因為兩個機器運行在兩個不同的Java虛擬機中,因此每個機器加的鎖不是同一個鎖,而不同的鎖只對屬于自己Java虛擬機中的線程有效,對其他Java虛擬機的線程無效。此時,Java提供的原生鎖機制在多機部署場景下就會失效,出現(xiàn)庫存超賣的現(xiàn)象。
解決方案
基于上述場景,需要保證兩臺機器加的鎖是同一個鎖,用加鎖的方式對某種資源進行順序訪問控制。這就需要分布式鎖登場了。
分布式鎖的思路是:在整個系統(tǒng)提供一個全局的、唯一的分配鎖的“東西”,當每個系統(tǒng)需要加鎖時,都向其獲取一把鎖,使不同的系統(tǒng)獲取到的內(nèi)容可以認為是同一把鎖。
當前分布式加鎖主要有三種方式:(磁盤) 數(shù)據(jù)庫 、緩存數(shù)據(jù)庫、Zookeeper。
使用DCS服務中Redis緩存實例實現(xiàn)分布式加鎖,有幾大優(yōu)勢:
- 加鎖操作簡單,使用SET、GET、DEL等幾條簡單命令即可實現(xiàn)鎖的獲取和釋放。
- 性能優(yōu)越,緩存數(shù)據(jù)的讀寫優(yōu)于磁盤數(shù)據(jù)庫與Zookeeper。
- 可靠性強,DCS有主備和集群實例類型,避免單點故障。
對分布式應用加鎖,能夠避免出現(xiàn)庫存超賣及無序訪問等現(xiàn)象。本實踐介紹如何使用Redis對分布式應用加鎖。
前提條件
- 已創(chuàng)建DCS緩存實例,且狀態(tài)為“運行中”。
- 客戶端所在服務器與DCS緩存實例網(wǎng)絡互通:
- 客戶端與Redis實例所在VPC為同一VPC
- 客戶端與Redis實例所在VPC為相同region下的不同VPC
如果客戶端與Redis實例不在相同VPC中,可以通過建立VPC對等連接方式連通網(wǎng)絡,具體請參考:緩存實例是否支持跨VPC訪問?。
- 客戶端與Redis實例所在VPC不在相同region
- 公網(wǎng)訪問
客戶端公網(wǎng)訪問Redis 4.0/5.0/6.0實例時,需要開啟實例公網(wǎng)訪問開關,具體請參考開啟Redis 4.0/5.0/6.0公網(wǎng)訪問并獲取公網(wǎng)訪問地址。
- 客戶端所在的服務器已安裝JDK1.8以上版本和開發(fā)工具(本文檔以安裝Eclipse為例),下載jedis客戶端(單擊此處直接下載jar包)。
本文檔下載的開發(fā)工具和客戶端僅為示例,您可以選擇其它類型的工具和客戶端。
實施步驟
- 在服務器上運行Eclipse,創(chuàng)建一個java工程,為示例代碼分別創(chuàng)建一個分布式鎖實現(xiàn)類DistributedLock.java和測試類CaseTest.java,并將jedis客戶端作為library引用到工程中。
創(chuàng)建的分布式鎖實現(xiàn)類DistributedLock.java內(nèi)容示例如下:
package dcsDemo01; import java.util.UUID; import redis.clients.jedis.Jedis; import redis.clients.jedis.params.SetParams; public class DistributedLock { // Redis實例連接地址和端口,需替換為實際獲取的值 private final String host = "192.168.0.220"; private final int port = 6379; private static final String SUC CES S = "OK"; public DistributedLock(){} /* * @param lockName 鎖名 * @param timeout 獲取鎖的超時時間 * @param lockTimeout 鎖的有效時間 * @return 鎖的標識 */ public String getLockWithTimeout(String lockName, long timeout, long lockTimeout) { String ret = null; Jedis jedisClient = new Jedis(host, port); try { // Redis實例連接密碼,需替換為實際獲取的值 String authMsg = jedisClient.auth("passwd"); if (!SUCCESS.equals(authMsg)) { System.out.println("AUTH FAILED: " + authMsg); } String identifier = UUID.randomUUID().toString(); String lockKey = "DLock:" + lockName; long end = System.currentTimeMillis() + timeout; SetParams setParams = new SetParams(); setParams.nx().px(lockTimeout); while(System.currentTimeMillis() < end) { String result = jedisClient.set(lockKey, identifier, setParams); if(SUCCESS.equals(result)) { ret = identifier; break; } try { Thread.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } catch (Exception e) { e.printStackTrace(); }finally { jedisClient.quit(); jedisClient.close(); } return ret; } /* * @param lockName 鎖名 * @param identifier 鎖的標識 */ public void releaseLock(String lockName, String identifier) { Jedis jedisClient = new Jedis(host, port); try { String authMsg = jedisClient.auth("passwd"); if (!SUCCESS.equals(authMsg)) { System.out.println("AUTH FAILED: " + authMsg); } String lockKey = "DLock:" + lockName; if(identifier.equals(jedisClient.get(lockKey))) { jedisClient.del(lockKey); } } catch (Exception e) { e.printStackTrace(); }finally { jedisClient.quit(); jedisClient.close(); } } }須知:
該代碼實現(xiàn)僅展示使用DCS服務進行加鎖訪問的便捷性。具體技術實現(xiàn)需要考慮死鎖、鎖的檢查等情況,這里不做詳細說明。
假設20個線程對10臺mate10手機進行搶購,創(chuàng)建的測試類CaseTest.java類內(nèi)容示例如下:package dcsDemo01; import java.util.UUID; public class CaseTest { public static void main(String[] args) { ServiceOrder service = new ServiceOrder(); for (int i = 0; i < 20; i++) { ThreadBuy client = new ThreadBuy(service); client.start(); } } } class ServiceOrder { private final int MAX = 10; DistributedLock DLock = new DistributedLock(); int n = 10; public void handleOder() { String userName = UUID.randomUUID().toString().substring(0,8) + Thread.currentThread().getName(); String identifier = DLock.getLockWithTimeout("Huawei Mate 10", 10000, 2000); System.out.println("正在為用戶:" + userName + " 處理訂單"); if(n > 0) { int num = MAX - n + 1; System.out.println("用戶:"+ userName + "購買第" + num + "臺,剩余" + (--n) + "臺"); }else { System.out.println("用戶:"+ userName + "無法購買,已售罄!"); } DLock.releaseLock("Huawei Mate 10", identifier); } } class ThreadBuy extends Thread { private ServiceOrder service; public ThreadBuy(ServiceOrder service) { this.service = service; } @Override public void run() { service.handleOder(); } } - 將DCS緩存實例的連接地址、端口以及連接密碼配置到分布式鎖實現(xiàn)類DistributedLock.java示例代碼文件中。
在DistributedLock.java中,host及port配置為實例的連接地址及端口號,在getLockWithTimeout、releaseLock方法中需配置passwd值為實例訪問密碼。
- 將測試類CaseTest中加鎖部分注釋掉,變成無鎖情況,示例如下:
//測試類中注釋兩行用于加鎖的代碼: public void handleOder() { String userName = UUID.randomUUID().toString().substring(0,8) + Thread.currentThread().getName(); //加鎖代碼 //String identifier = DLock.getLockWithTimeout("Huawei Mate 10", 10000, 2000); System.out.println("正在為用戶:" + userName + " 處理訂單"); if(n > 0) { int num = MAX - n + 1; System.out.println("用戶:"+ userName + "購買第" + num + "臺,剩余" + (--n) + "臺"); }else { System.out.println("用戶:"+ userName + "無法購買,已售罄!"); } //加鎖代碼 //DLock.releaseLock("Huawei Mate 10", identifier); } - 編譯及運行無鎖的類,運行結果是搶購無序的,如下:
正在為用戶:e04934ddThread-5 處理訂單 正在為用戶:a4554180Thread-0 處理訂單 用戶:a4554180Thread-0購買第2臺,剩余8臺 正在為用戶:b58eb811Thread-10 處理訂單 用戶:b58eb811Thread-10購買第3臺,剩余7臺 正在為用戶:e8391c0eThread-19 處理訂單 正在為用戶:21fd133aThread-13 處理訂單 正在為用戶:1dd04ff4Thread-6 處理訂單 用戶:1dd04ff4Thread-6購買第6臺,剩余4臺 正在為用戶:e5977112Thread-3 處理訂單 正在為用戶:4d7a8a2bThread-4 處理訂單 用戶:e5977112Thread-3購買第7臺,剩余3臺 正在為用戶:18967410Thread-15 處理訂單 用戶:18967410Thread-15購買第9臺,剩余1臺 正在為用戶:e4f51568Thread-14 處理訂單 用戶:21fd133aThread-13購買第5臺,剩余5臺 用戶:e8391c0eThread-19購買第4臺,剩余6臺 正在為用戶:d895d3f1Thread-12 處理訂單 用戶:d895d3f1Thread-12無法購買,已售罄! 正在為用戶:7b8d2526Thread-11 處理訂單 用戶:7b8d2526Thread-11無法購買,已售罄! 正在為用戶:d7ca1779Thread-8 處理訂單 用戶:d7ca1779Thread-8無法購買,已售罄! 正在為用戶:74fca0ecThread-1 處理訂單 用戶:74fca0ecThread-1無法購買,已售罄! 用戶:e04934ddThread-5購買第1臺,剩余9臺 用戶:e4f51568Thread-14購買第10臺,剩余0臺 正在為用戶:aae76a83Thread-7 處理訂單 用戶:aae76a83Thread-7無法購買,已售罄! 正在為用戶:c638d2cfThread-2 處理訂單 用戶:c638d2cfThread-2無法購買,已售罄! 正在為用戶:2de29a4eThread-17 處理訂單 用戶:2de29a4eThread-17無法購買,已售罄! 正在為用戶:40a46ba0Thread-18 處理訂單 用戶:40a46ba0Thread-18無法購買,已售罄! 正在為用戶:211fd9c7Thread-9 處理訂單 用戶:211fd9c7Thread-9無法購買,已售罄! 正在為用戶:911b83fcThread-16 處理訂單 用戶:911b83fcThread-16無法購買,已售罄! 用戶:4d7a8a2bThread-4購買第8臺,剩余2臺
- 取消測試類CaseTest中注釋的加鎖內(nèi)容,編譯并運行得到有序的搶購結果如下:
正在為用戶:eee56fb7Thread-16 處理訂單 用戶:eee56fb7Thread-16購買第1臺,剩余9臺 正在為用戶:d6521816Thread-2 處理訂單 用戶:d6521816Thread-2購買第2臺,剩余8臺 正在為用戶:d7b3b983Thread-19 處理訂單 用戶:d7b3b983Thread-19購買第3臺,剩余7臺 正在為用戶:36a6b97aThread-15 處理訂單 用戶:36a6b97aThread-15購買第4臺,剩余6臺 正在為用戶:9a973456Thread-1 處理訂單 用戶:9a973456Thread-1購買第5臺,剩余5臺 正在為用戶:03f1de9aThread-14 處理訂單 用戶:03f1de9aThread-14購買第6臺,剩余4臺 正在為用戶:2c315ee6Thread-11 處理訂單 用戶:2c315ee6Thread-11購買第7臺,剩余3臺 正在為用戶:2b03b7c0Thread-12 處理訂單 用戶:2b03b7c0Thread-12購買第8臺,剩余2臺 正在為用戶:75f25749Thread-0 處理訂單 用戶:75f25749Thread-0購買第9臺,剩余1臺 正在為用戶:26c71db5Thread-18 處理訂單 用戶:26c71db5Thread-18購買第10臺,剩余0臺 正在為用戶:c32654dbThread-17 處理訂單 用戶:c32654dbThread-17無法購買,已售罄! 正在為用戶:df94370aThread-7 處理訂單 用戶:df94370aThread-7無法購買,已售罄! 正在為用戶:0af94cddThread-5 處理訂單 用戶:0af94cddThread-5無法購買,已售罄! 正在為用戶:e52428a4Thread-13 處理訂單 用戶:e52428a4Thread-13無法購買,已售罄! 正在為用戶:46f91208Thread-10 處理訂單 用戶:46f91208Thread-10無法購買,已售罄! 正在為用戶:e0ca87bbThread-9 處理訂單 用戶:e0ca87bbThread-9無法購買,已售罄! 正在為用戶:f385af9aThread-8 處理訂單 用戶:f385af9aThread-8無法購買,已售罄! 正在為用戶:46c5f498Thread-6 處理訂單 用戶:46c5f498Thread-6無法購買,已售罄! 正在為用戶:935e0f50Thread-3 處理訂單 用戶:935e0f50Thread-3無法購買,已售罄! 正在為用戶:d3eaae29Thread-4 處理訂單 用戶:d3eaae29Thread-4無法購買,已售罄!