Curator實現分佈式鎖

簡介

分佈式鎖服務宕機, ZooKeeper 一般是以集羣部署, 如果出現 ZooKeeper 宕機, 那麼只要當前正常的服務器超過集羣的半數, 依然可以正常提供服務
持有鎖資源服務器宕機, 假如一臺服務器獲取鎖之後就宕機了, 那麼就會導致其他服務器無法再獲取該鎖. 就會造成 死鎖 問題, 在 Curator 中, 鎖的信息都是保存在臨時節點上, 如果持有鎖資源的服務器宕機, 那麼 ZooKeeper 就會移除它的信息, 這時其他服務器就能進行獲取鎖操作。

zookeper的實現主要是下面四個類:

InterProcessMutex:分佈式可重入排它鎖
InterProcessSemaphoreMutex:分佈式排它鎖
InterProcessReadWriteLock:分佈式讀寫鎖
InterProcessMultiLock:將多個鎖作爲單個實體管理的容器

1.共享鎖,不可重入-InterProcessSemaphoreMutex 

InterProcessSemaphoreMutex是一種不可重入的互斥鎖,也就意味着即使是同一個線程也無法在持有鎖的情況下再次獲得鎖,所以需要注意,不可重入的鎖很容易在一些情況導致死鎖。

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享鎖,不可重入---InterProcessSemaphoreMutex
     * InterProcessSemaphoreMutex是一種不可重入的互斥鎖,也就意味着即使是同一個線程也無法在持有鎖的情況下再次獲得鎖,所以需要注意,不可重入的鎖很容易在一些情況導致死鎖。
     * @date 2020/5/27 14:48
     **/
    @Test
    public void sharedLock() throws Exception {
        // 創建共享鎖
        final InterProcessLock lock = new InterProcessSemaphoreMutex(client, lockPath);
        // lock2 用於模擬其他客戶端
        final InterProcessLock lock2 = new InterProcessSemaphoreMutex(client2, lockPath);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock.acquire();
                System.out.println("1獲取鎖===============");
                // 測試鎖重入
                Thread.sleep(5 * 1000);
                lock.release();
                System.out.println("1釋放鎖===============");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock2.acquire();
                System.out.println("2獲取鎖===============");
                Thread.sleep(5 * 1000);
                lock2.release();
                System.out.println("2釋放鎖===============");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        Thread.sleep(20 * 1000);
    }

輸出結果

初始化資源...
1獲取鎖===============
1釋放鎖===============
2獲取鎖===============
2釋放鎖===============
釋放資源... 

2.共享可重入鎖-InterProcessMutex

此鎖可以重入,但是重入幾次需要釋放幾次。

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享可重入鎖-InterProcessMutex
     * 此鎖可以重入,但是重入幾次需要釋放幾次
     * @date 2020/5/27 14:55
     **/
    @Test
    public void sharedReentrantLock() throws Exception {
        // 創建共享鎖
        final InterProcessLock lock = new InterProcessMutex(client, lockPath);
        // lock2 用於模擬其他客戶端
        final InterProcessLock lock2 = new InterProcessMutex(client2, lockPath);

        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock.acquire();
                System.out.println("1獲取鎖===============");
                // 測試鎖重入
                lock.acquire();
                System.out.println("1再次獲取鎖===============");
                Thread.sleep(5 * 1000);
                lock.release();
                System.out.println("1釋放鎖===============");
                lock.release();
                System.out.println("1再次釋放鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock2.acquire();
                System.out.println("2獲取鎖===============");
                // 測試鎖重入
                lock2.acquire();
                System.out.println("2再次獲取鎖===============");
                Thread.sleep(5 * 1000);
                lock2.release();
                System.out.println("2釋放鎖===============");
                lock2.release();
                System.out.println("2再次釋放鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

輸出結果

初始化資源...
1獲取鎖===============
1再次獲取鎖===============
1釋放鎖===============
1再次釋放鎖===============
2獲取鎖===============
2再次獲取鎖===============
2釋放鎖===============
2再次釋放鎖===============
釋放資源... 

原理:

InterProcessMutex通過在zookeeper的某路徑節點下創建臨時序列節點來實現分佈式鎖,即每個線程(跨進程的線程)獲取同一把鎖前,都需要在同樣的路徑下創建一個節點,節點名字由uuid + 遞增序列組成。而通過對比自身的序列數是否在所有子節點的第一位,來判斷是否成功獲取到了鎖。當獲取鎖失敗時,它會添加watcher來監聽前一個節點的變動情況,然後進行等待狀態。直到watcher的事件生效將自己喚醒,或者超時時間異常返回。

3.共享可重入讀寫鎖-InterProcessMutex

讀鎖和讀鎖不互斥,只要有寫鎖就互斥。

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享可重入讀寫鎖-InterProcessReadWriteLock
     * 讀鎖和讀鎖不互斥,只要有寫鎖就互斥
     * @date 2020/5/27 14:57
     **/
    @Test
    public void sharedReentrantReadWriteLock() throws Exception {
        // 創建共享可重入讀寫鎖
        final InterProcessReadWriteLock locl1 = new InterProcessReadWriteLock(client, lockPath);
        // lock2 用於模擬其他客戶端
        final InterProcessReadWriteLock lock2 = new InterProcessReadWriteLock(client2, lockPath);

        // 獲取讀寫鎖(使用 InterProcessMutex 實現, 所以是可以重入的)
        final InterProcessLock readLock = locl1.readLock();
        final InterProcessLock readLockw = lock2.readLock();

        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                readLock.acquire();
                System.out.println("1獲取讀鎖===============");
                // 測試鎖重入
                readLock.acquire();
                System.out.println("1再次獲取讀鎖===============");
                Thread.sleep(5 * 1000);
                readLock.release();
                System.out.println("1釋放讀鎖===============");
                readLock.release();
                System.out.println("1再次釋放讀鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                Thread.sleep(500);
                readLockw.acquire();
                System.out.println("2獲取讀鎖===============");
                // 測試鎖重入
                readLockw.acquire();
                System.out.println("2再次獲取讀鎖==============");
                Thread.sleep(5 * 1000);
                readLockw.release();
                System.out.println("2釋放讀鎖===============");
                readLockw.release();
                System.out.println("2再次釋放讀鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

輸出結果:

初始化資源...
1獲取讀鎖===============
1再次獲取讀鎖===============
2獲取讀鎖===============
2再次獲取讀鎖==============
1釋放讀鎖===============
1再次釋放讀鎖===============
2釋放讀鎖===============
2再次釋放讀鎖===============
釋放資源... 

 4. 共享信號量

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享信號量-InterProcessSemaphoreV2
     * @date 2020/5/27 14:59
     **/
    @Test
    public void semaphore() throws Exception {
        // 創建一個信號量, Curator 以公平鎖的方式進行實現
        final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 1);
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取一個許可
                Lease lease = semaphore.acquire();
                log.info("1獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnLease(lease);
                log.info("1釋放讀信號量===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取一個許可
                Lease lease = semaphore.acquire();
                log.info("2獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnLease(lease);
                log.info("2釋放讀信號量===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

輸出結果:

初始化資源...
15:14:29.947 [Thread-2] INFO cn.com.sdd.study.DistributedLockTest - 2獲取讀信號量===============
15:14:34.976 [Thread-2] INFO cn.com.sdd.study.DistributedLockTest - 2釋放讀信號量===============
15:14:35.000 [Thread-1] INFO cn.com.sdd.study.DistributedLockTest - 1獲取讀信號量===============
15:14:40.129 [Thread-1] INFO cn.com.sdd.study.DistributedLockTest - 1釋放讀信號量===============
釋放資源...

一次獲取多個信號量:

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //一次獲取多個共享信號量
     * @date 2020/5/27 15:01
     **/
    @Test
    public void semaphore2() throws Exception {
        // 創建一個信號量, Curator 以公平鎖的方式進行實現
        final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 3);
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取2個許可
                Collection<Lease> acquire = semaphore.acquire(2);
                log.info("1獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnAll(acquire);
                log.info("1釋放讀信號量===============");
                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取1個許可
                Collection<Lease> acquire = semaphore.acquire(1);
                log.info("2獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnAll(acquire);
                log.info("2釋放讀信號量===============");
                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

 

輸出結果:

初始化資源...
15:16:54.914 [Thread-2] INFO cn.com.sdd.study.DistributedLockTest - 2獲取讀信號量===============
15:16:55.055 [Thread-1] INFO cn.com.sdd.study.DistributedLockTest - 1獲取讀信號量===============
15:17:00.128 [Thread-2] INFO cn.com.sdd.study.DistributedLockTest - 2釋放讀信號量===============
15:17:00.187 [Thread-1] INFO cn.com.sdd.study.DistributedLockTest - 1釋放讀信號量===============
釋放資源...

5.多重共享鎖-InterProcessMultiLock

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //多重共享鎖-InterProcessMultiLock
     * @date 2020/5/27 15:02
     **/
    @Test
    public void multiLock() throws Exception {
        // 可重入鎖
        final InterProcessLock interProcessLock1 = new InterProcessMutex(client, lockPath);
        // 不可重入鎖
        final InterProcessLock interProcessLock2 = new InterProcessSemaphoreMutex(client2, lockPath);
        // 創建多重鎖對象
        final InterProcessLock lock = new InterProcessMultiLock(Arrays.asList(interProcessLock1, interProcessLock2));

        final CountDownLatch countDownLatch = new CountDownLatch(1);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取參數集合中的所有鎖
                lock.acquire();
                // 因爲存在一個不可重入鎖, 所以整個 InterProcessMultiLock 不可重入
                System.out.println(lock.acquire(2, TimeUnit.SECONDS));
                // interProcessLock1 是可重入鎖, 所以可以繼續獲取鎖
                System.out.println(interProcessLock1.acquire(2, TimeUnit.SECONDS));
                // interProcessLock2 是不可重入鎖, 所以獲取鎖失敗
                System.out.println(interProcessLock2.acquire(2, TimeUnit.SECONDS));

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

輸出結果:

初始化資源...
false
true
false
釋放資源...

完整代碼

 

import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * @author suidd
 * @name DistributedLockDemo
 * @description Curator實現分佈式鎖的示例
 * @date 2020/5/27 14:26
 * Version 1.0
 **/
@Slf4j
@SpringBootTest
public class DistributedLockTest {
    // ZooKeeper 鎖節點路徑, 分佈式鎖的相關操作都是在這個節點上進行
    private final String lockPath = "/distributed-lock";

    // ZooKeeper 服務地址, 單機格式爲:(127.0.0.1:2181),
    // 集羣格式爲:(127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183)
    private String connectString;
    // Curator 客戶端重試策略
    private RetryPolicy retry;
    // Curator 客戶端對象
    private CuratorFramework client;
    // client2 用戶模擬其他客戶端
    private CuratorFramework client2;

    // 初始化資源
    @BeforeEach
    public void init() {
        // 設置 ZooKeeper 服務地址爲本機的 2181 端口
        connectString = "127.0.0.1:2181";
        // 重試策略
        // 初始休眠時間爲 1000ms, 最大重試次數爲 3
        retry = new ExponentialBackoffRetry(1000, 3);
        // 創建一個客戶端, 60000(ms)爲 session 超時時間, 15000(ms)爲鏈接超時時間
        client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
        client2 = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
        // 創建會話
        client.start();
        client2.start();
        System.out.println("初始化資源...");
    }

    // 釋放資源
    @AfterEach
    public void close() {
        CloseableUtils.closeQuietly(client);
        System.out.println("釋放資源...");
    }

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享鎖,不可重入-InterProcessSemaphoreMutex
     * InterProcessSemaphoreMutex是一種不可重入的互斥鎖,也就意味着即使是同一個線程也無法在持有鎖的情況下再次獲得鎖,所以需要注意,不可重入的鎖很容易在一些情況導致死鎖。
     * @date 2020/5/27 14:48
     **/
    @Test
    public void sharedLock() throws Exception {
        // 創建共享鎖
        final InterProcessLock lock = new InterProcessSemaphoreMutex(client, lockPath);
        // lock2 用於模擬其他客戶端
        final InterProcessLock lock2 = new InterProcessSemaphoreMutex(client2, lockPath);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock.acquire();
                System.out.println("1獲取鎖===============");
                // 測試鎖重入
                Thread.sleep(5 * 1000);
                lock.release();
                System.out.println("1釋放鎖===============");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock2.acquire();
                System.out.println("2獲取鎖===============");
                Thread.sleep(5 * 1000);
                lock2.release();
                System.out.println("2釋放鎖===============");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        Thread.sleep(20 * 1000);
    }

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享可重入鎖-InterProcessMutex
     * 此鎖可以重入,但是重入幾次需要釋放幾次
     * @date 2020/5/27 14:55
     **/
    @Test
    public void sharedReentrantLock() throws Exception {
        // 創建共享鎖
        final InterProcessLock lock = new InterProcessMutex(client, lockPath);
        // lock2 用於模擬其他客戶端
        final InterProcessLock lock2 = new InterProcessMutex(client2, lockPath);

        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock.acquire();
                System.out.println("1獲取鎖===============");
                // 測試鎖重入
                lock.acquire();
                System.out.println("1再次獲取鎖===============");
                Thread.sleep(5 * 1000);
                lock.release();
                System.out.println("1釋放鎖===============");
                lock.release();
                System.out.println("1再次釋放鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                lock2.acquire();
                System.out.println("2獲取鎖===============");
                // 測試鎖重入
                lock2.acquire();
                System.out.println("2再次獲取鎖===============");
                Thread.sleep(5 * 1000);
                lock2.release();
                System.out.println("2釋放鎖===============");
                lock2.release();
                System.out.println("2再次釋放鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享可重入讀寫鎖-InterProcessReadWriteLock
     * 讀鎖和讀鎖不互斥,只要有寫鎖就互斥
     * @date 2020/5/27 14:57
     **/
    @Test
    public void sharedReentrantReadWriteLock() throws Exception {
        // 創建共享可重入讀寫鎖
        final InterProcessReadWriteLock locl1 = new InterProcessReadWriteLock(client, lockPath);
        // lock2 用於模擬其他客戶端
        final InterProcessReadWriteLock lock2 = new InterProcessReadWriteLock(client2, lockPath);

        // 獲取讀寫鎖(使用 InterProcessMutex 實現, 所以是可以重入的)
        final InterProcessLock readLock = locl1.readLock();
        final InterProcessLock readLockw = lock2.readLock();

        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                readLock.acquire();
                System.out.println("1獲取讀鎖===============");
                // 測試鎖重入
                readLock.acquire();
                System.out.println("1再次獲取讀鎖===============");
                Thread.sleep(5 * 1000);
                readLock.release();
                System.out.println("1釋放讀鎖===============");
                readLock.release();
                System.out.println("1再次釋放讀鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                Thread.sleep(500);
                readLockw.acquire();
                System.out.println("2獲取讀鎖===============");
                // 測試鎖重入
                readLockw.acquire();
                System.out.println("2再次獲取讀鎖==============");
                Thread.sleep(5 * 1000);
                readLockw.release();
                System.out.println("2釋放讀鎖===============");
                readLockw.release();
                System.out.println("2再次釋放讀鎖===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //共享信號量-InterProcessSemaphoreV2
     * @date 2020/5/27 14:59
     **/
    @Test
    public void semaphore() throws Exception {
        // 創建一個信號量, Curator 以公平鎖的方式進行實現
        final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 1);
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取一個許可
                Lease lease = semaphore.acquire();
                log.info("1獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnLease(lease);
                log.info("1釋放讀信號量===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取一個許可
                Lease lease = semaphore.acquire();
                log.info("2獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnLease(lease);
                log.info("2釋放讀信號量===============");

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //一次獲取多個共享信號量
     * @date 2020/5/27 15:01
     **/
    @Test
    public void semaphore2() throws Exception {
        // 創建一個信號量, Curator 以公平鎖的方式進行實現
        final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 3);
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取2個許可
                Collection<Lease> acquire = semaphore.acquire(2);
                log.info("1獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnAll(acquire);
                log.info("1釋放讀信號量===============");
                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取1個許可
                Collection<Lease> acquire = semaphore.acquire(1);
                log.info("2獲取讀信號量===============");
                Thread.sleep(5 * 1000);
                semaphore.returnAll(acquire);
                log.info("2釋放讀信號量===============");
                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }

    /**
     * @param
     * @return change notes
     * @author suidd
     * @description //多重共享鎖-InterProcessMultiLock
     * @date 2020/5/27 15:02
     **/
    @Test
    public void multiLock() throws Exception {
        // 可重入鎖
        final InterProcessLock interProcessLock1 = new InterProcessMutex(client, lockPath);
        // 不可重入鎖
        final InterProcessLock interProcessLock2 = new InterProcessSemaphoreMutex(client2, lockPath);
        // 創建多重鎖對象
        final InterProcessLock lock = new InterProcessMultiLock(Arrays.asList(interProcessLock1, interProcessLock2));

        final CountDownLatch countDownLatch = new CountDownLatch(1);

        new Thread(() -> {
            // 獲取鎖對象
            try {
                // 獲取參數集合中的所有鎖
                lock.acquire();
                // 因爲存在一個不可重入鎖, 所以整個 InterProcessMultiLock 不可重入
                System.out.println(lock.acquire(2, TimeUnit.SECONDS));
                // interProcessLock1 是可重入鎖, 所以可以繼續獲取鎖
                System.out.println(interProcessLock1.acquire(2, TimeUnit.SECONDS));
                // interProcessLock2 是不可重入鎖, 所以獲取鎖失敗
                System.out.println(interProcessLock2.acquire(2, TimeUnit.SECONDS));

                countDownLatch.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        countDownLatch.await();
    }
}

參考鏈接:https://www.cnblogs.com/qlqwjy/p/10518900.html 

 

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