華為云計(jì)算 云知識(shí) 使用分布式緩存服務(wù)DCS實(shí)現(xiàn)熱點(diǎn)資源順序訪問(wèn)
使用分布式緩存服務(wù)DCS實(shí)現(xiàn)熱點(diǎn)資源順序訪問(wèn)

應(yīng)用場(chǎng)景

在傳統(tǒng)單機(jī)部署的情況下,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或synchronized)進(jìn)行互斥控制。這種Java提供的原生鎖機(jī)制可以保證在同一個(gè)Java虛擬機(jī)進(jìn)程內(nèi)的多個(gè)線程同步執(zhí)行,避免出現(xiàn)無(wú)序現(xiàn)象。

但在互聯(lián)網(wǎng)場(chǎng)景,例如在商品秒殺過(guò)程中,隨著客戶業(yè)務(wù)量上升,整個(gè)系統(tǒng)并發(fā)飆升,需要多臺(tái)機(jī)器并發(fā)運(yùn)行。例如當(dāng)兩個(gè)用戶同時(shí)發(fā)起的請(qǐng)求分別落在兩個(gè)不同的機(jī)器上時(shí),雖然這兩個(gè)請(qǐng)求可以同時(shí)執(zhí)行,但是因?yàn)閮蓚€(gè)機(jī)器運(yùn)行在兩個(gè)不同的Java虛擬機(jī)中,因此每個(gè)機(jī)器加的鎖不是同一個(gè)鎖,而不同的鎖只對(duì)屬于自己Java虛擬機(jī)中的線程有效,對(duì)其他Java虛擬機(jī)的線程無(wú)效。此時(shí),Java提供的原生鎖機(jī)制在多機(jī)部署場(chǎng)景下就會(huì)失效,出現(xiàn)庫(kù)存超賣(mài)的現(xiàn)象。

解決方案

基于上述場(chǎng)景,需要保證兩臺(tái)機(jī)器加的鎖是同一個(gè)鎖,用加鎖的方式對(duì)某種資源進(jìn)行順序訪問(wèn)控制。這就需要分布式鎖登場(chǎng)了。

分布式鎖的思路是:在整個(gè)系統(tǒng)提供一個(gè)全局的、唯一的分配鎖的“東西”,當(dāng)每個(gè)系統(tǒng)需要加鎖時(shí),都向其獲取一把鎖,使不同的系統(tǒng)獲取到的內(nèi)容可以認(rèn)為是同一把鎖。

當(dāng)前分布式加鎖主要有三種方式:(磁盤(pán)) 數(shù)據(jù)庫(kù) 、緩存數(shù)據(jù)庫(kù)、Zookeeper。

使用DCS服務(wù)中Redis緩存實(shí)例實(shí)現(xiàn)分布式加鎖,有幾大優(yōu)勢(shì):

  • 加鎖操作簡(jiǎn)單,使用SET、GET、DEL等幾條簡(jiǎn)單命令即可實(shí)現(xiàn)鎖的獲取和釋放。
  • 性能優(yōu)越,緩存數(shù)據(jù)的讀寫(xiě)優(yōu)于磁盤(pán)數(shù)據(jù)庫(kù)與Zookeeper。
  • 可靠性強(qiáng),DCS有主備和集群實(shí)例類型,避免單點(diǎn)故障。

對(duì)分布式應(yīng)用加鎖,能夠避免出現(xiàn)庫(kù)存超賣(mài)及無(wú)序訪問(wèn)等現(xiàn)象。本實(shí)踐介紹如何使用Redis對(duì)分布式應(yīng)用加鎖。

前提條件

  • 已創(chuàng)建DCS緩存實(shí)例,且狀態(tài)為“運(yùn)行中”。
  • 客戶端所在服務(wù)器與DCS緩存實(shí)例網(wǎng)絡(luò)互通:
    • 客戶端與Redis實(shí)例所在VPC為同一VPC

      同一VPC內(nèi)網(wǎng)絡(luò)默認(rèn)互通。

    • 客戶端與Redis實(shí)例所在VPC為相同region下的不同VPC

      如果客戶端與Redis實(shí)例不在相同VPC中,可以通過(guò)建立VPC對(duì)等連接方式連通網(wǎng)絡(luò),具體請(qǐng)參考:緩存實(shí)例是否支持跨VPC訪問(wèn)?。

    • 客戶端與Redis實(shí)例所在VPC不在相同region

      如果客戶端服務(wù)器和Redis實(shí)例不在同一region,僅支持通過(guò) 云專線 打通網(wǎng)絡(luò),請(qǐng)參考云專線。

    • 公網(wǎng)訪問(wèn)

      客戶端公網(wǎng)訪問(wèn)Redis 4.0/5.0/6.0實(shí)例時(shí),需要開(kāi)啟實(shí)例公網(wǎng)訪問(wèn)開(kāi)關(guān),具體請(qǐng)參考開(kāi)啟Redis 4.0/5.0/6.0公網(wǎng)訪問(wèn)并獲取公網(wǎng)訪問(wèn)地址

  • 客戶端所在的服務(wù)器已安裝JDK1.8以上版本和開(kāi)發(fā)工具(本文檔以安裝Eclipse為例),下載jedis客戶端(單擊此處直接下載jar包)。

    本文檔下載的開(kāi)發(fā)工具和客戶端僅為示例,您可以選擇其它類型的工具和客戶端。

實(shí)施步驟

  1. 在服務(wù)器上運(yùn)行Eclipse,創(chuàng)建一個(gè)java工程,為示例代碼分別創(chuàng)建一個(gè)分布式鎖實(shí)現(xiàn)類DistributedLock.java和測(cè)試類CaseTest.java,并將jedis客戶端作為library引用到工程中。

     

    創(chuàng)建的分布式鎖實(shí)現(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實(shí)例連接地址和端口,需替換為實(shí)際獲取的值
        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       獲取鎖的超時(shí)時(shí)間
         * @param lockTimeout   鎖的有效時(shí)間
         * @return              鎖的標(biāo)識(shí)
         */
        public String getLockWithTimeout(String lockName, long timeout, long lockTimeout) {
            String ret = null;
            Jedis jedisClient = new Jedis(host, port);
    
            try {
                // Redis實(shí)例連接密碼,需替換為實(shí)際獲取的值
                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    鎖的標(biāo)識(shí)
         */
        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();
            }
        }
    }

    須知:

    該代碼實(shí)現(xiàn)僅展示使用DCS服務(wù)進(jìn)行加鎖訪問(wèn)的便捷性。具體技術(shù)實(shí)現(xiàn)需要考慮死鎖、鎖的檢查等情況,這里不做詳細(xì)說(shuō)明。

    假設(shè)20個(gè)線程對(duì)10臺(tái)mate10手機(jī)進(jìn)行搶購(gòu),創(chuàng)建的測(cè)試類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 + "購(gòu)買(mǎi)第" + num + "臺(tái),剩余" + (--n) + "臺(tái)");
            }else {
                System.out.println("用戶:"+ userName + "無(wú)法購(gòu)買(mǎi),已售罄!");
            }
            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();
        }
    }
     
  2. 將DCS緩存實(shí)例的連接地址、端口以及連接密碼配置到分布式鎖實(shí)現(xiàn)類DistributedLock.java示例代碼文件中。

     

    在DistributedLock.java中,host及port配置為實(shí)例的連接地址及端口號(hào),在getLockWithTimeout、releaseLock方法中需配置passwd值為實(shí)例訪問(wèn)密碼。

     

  3. 將測(cè)試類CaseTest中加鎖部分注釋掉,變成無(wú)鎖情況,示例如下:

     

    //測(cè)試類中注釋兩行用于加鎖的代碼:
    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 + "購(gòu)買(mǎi)第" + num + "臺(tái),剩余" + (--n) + "臺(tái)");
        }else {
            System.out.println("用戶:"+ userName + "無(wú)法購(gòu)買(mǎi),已售罄!");
        }
        //加鎖代碼
        //DLock.releaseLock("Huawei Mate 10", identifier);
    }

  4. 編譯及運(yùn)行無(wú)鎖的類,運(yùn)行結(jié)果是搶購(gòu)無(wú)序的,如下:

     

    正在為用戶:e04934ddThread-5 處理訂單
    正在為用戶:a4554180Thread-0 處理訂單
    用戶:a4554180Thread-0購(gòu)買(mǎi)第2臺(tái),剩余8臺(tái)
    正在為用戶:b58eb811Thread-10 處理訂單
    用戶:b58eb811Thread-10購(gòu)買(mǎi)第3臺(tái),剩余7臺(tái)
    正在為用戶:e8391c0eThread-19 處理訂單
    正在為用戶:21fd133aThread-13 處理訂單
    正在為用戶:1dd04ff4Thread-6 處理訂單
    用戶:1dd04ff4Thread-6購(gòu)買(mǎi)第6臺(tái),剩余4臺(tái)
    正在為用戶:e5977112Thread-3 處理訂單
    正在為用戶:4d7a8a2bThread-4 處理訂單
    用戶:e5977112Thread-3購(gòu)買(mǎi)第7臺(tái),剩余3臺(tái)
    正在為用戶:18967410Thread-15 處理訂單
    用戶:18967410Thread-15購(gòu)買(mǎi)第9臺(tái),剩余1臺(tái)
    正在為用戶:e4f51568Thread-14 處理訂單
    用戶:21fd133aThread-13購(gòu)買(mǎi)第5臺(tái),剩余5臺(tái)
    用戶:e8391c0eThread-19購(gòu)買(mǎi)第4臺(tái),剩余6臺(tái)
    正在為用戶:d895d3f1Thread-12 處理訂單
    用戶:d895d3f1Thread-12無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:7b8d2526Thread-11 處理訂單
    用戶:7b8d2526Thread-11無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:d7ca1779Thread-8 處理訂單
    用戶:d7ca1779Thread-8無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:74fca0ecThread-1 處理訂單
    用戶:74fca0ecThread-1無(wú)法購(gòu)買(mǎi),已售罄!
    用戶:e04934ddThread-5購(gòu)買(mǎi)第1臺(tái),剩余9臺(tái)
    用戶:e4f51568Thread-14購(gòu)買(mǎi)第10臺(tái),剩余0臺(tái)
    正在為用戶:aae76a83Thread-7 處理訂單
    用戶:aae76a83Thread-7無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:c638d2cfThread-2 處理訂單
    用戶:c638d2cfThread-2無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:2de29a4eThread-17 處理訂單
    用戶:2de29a4eThread-17無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:40a46ba0Thread-18 處理訂單
    用戶:40a46ba0Thread-18無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:211fd9c7Thread-9 處理訂單
    用戶:211fd9c7Thread-9無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:911b83fcThread-16 處理訂單
    用戶:911b83fcThread-16無(wú)法購(gòu)買(mǎi),已售罄!
    用戶:4d7a8a2bThread-4購(gòu)買(mǎi)第8臺(tái),剩余2臺(tái)
  5. 取消測(cè)試類CaseTest中注釋的加鎖內(nèi)容,編譯并運(yùn)行得到有序的搶購(gòu)結(jié)果如下:

     

    正在為用戶:eee56fb7Thread-16 處理訂單
    用戶:eee56fb7Thread-16購(gòu)買(mǎi)第1臺(tái),剩余9臺(tái)
    正在為用戶:d6521816Thread-2 處理訂單
    用戶:d6521816Thread-2購(gòu)買(mǎi)第2臺(tái),剩余8臺(tái)
    正在為用戶:d7b3b983Thread-19 處理訂單
    用戶:d7b3b983Thread-19購(gòu)買(mǎi)第3臺(tái),剩余7臺(tái)
    正在為用戶:36a6b97aThread-15 處理訂單
    用戶:36a6b97aThread-15購(gòu)買(mǎi)第4臺(tái),剩余6臺(tái)
    正在為用戶:9a973456Thread-1 處理訂單
    用戶:9a973456Thread-1購(gòu)買(mǎi)第5臺(tái),剩余5臺(tái)
    正在為用戶:03f1de9aThread-14 處理訂單
    用戶:03f1de9aThread-14購(gòu)買(mǎi)第6臺(tái),剩余4臺(tái)
    正在為用戶:2c315ee6Thread-11 處理訂單
    用戶:2c315ee6Thread-11購(gòu)買(mǎi)第7臺(tái),剩余3臺(tái)
    正在為用戶:2b03b7c0Thread-12 處理訂單
    用戶:2b03b7c0Thread-12購(gòu)買(mǎi)第8臺(tái),剩余2臺(tái)
    正在為用戶:75f25749Thread-0 處理訂單
    用戶:75f25749Thread-0購(gòu)買(mǎi)第9臺(tái),剩余1臺(tái)
    正在為用戶:26c71db5Thread-18 處理訂單
    用戶:26c71db5Thread-18購(gòu)買(mǎi)第10臺(tái),剩余0臺(tái)
    正在為用戶:c32654dbThread-17 處理訂單
    用戶:c32654dbThread-17無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:df94370aThread-7 處理訂單
    用戶:df94370aThread-7無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:0af94cddThread-5 處理訂單
    用戶:0af94cddThread-5無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:e52428a4Thread-13 處理訂單
    用戶:e52428a4Thread-13無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:46f91208Thread-10 處理訂單
    用戶:46f91208Thread-10無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:e0ca87bbThread-9 處理訂單
    用戶:e0ca87bbThread-9無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:f385af9aThread-8 處理訂單
    用戶:f385af9aThread-8無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:46c5f498Thread-6 處理訂單
    用戶:46c5f498Thread-6無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:935e0f50Thread-3 處理訂單
    用戶:935e0f50Thread-3無(wú)法購(gòu)買(mǎi),已售罄!
    正在為用戶:d3eaae29Thread-4 處理訂單
    用戶:d3eaae29Thread-4無(wú)法購(gòu)買(mǎi),已售罄!