Java鎖到底是個什麼東西

来源:https://www.cnblogs.com/cosimo/p/18009960
-Advertisement-
Play Games

一、java鎖存在的必要性 要認識java鎖,就必須對2個前置概念有一個深刻的理解:多線程和共用資源。 對於程式來說,數據就是資源。 在單個線程操作數據時,或快或慢不存在什麼問題,一個人你愛乾什麼乾什麼。 多個線程操作各自操作不同的數據,各乾各的,也不存在什麼問題。 多個線程對共用數據進行讀取操作, ...


一、java鎖存在的必要性

要認識java鎖,就必須對2個前置概念有一個深刻的理解:多線程共用資源

對於程式來說,數據就是資源。

在單個線程操作數據時,或快或慢不存在什麼問題,一個人你愛乾什麼乾什麼。

多個線程操作各自操作不同的數據,各乾各的,也不存在什麼問題。

多個線程對共用數據進行讀取操作,我就四處看看,什麼也不動,也不存在什麼問題。

但如果多個線程共用數據進行操作,問題就來了。

經典庫存問題:

mysql 記錄剩餘:1,redis 緩存記錄剩餘:1。

小明上網下單,後臺程式檢查 redis 記錄存貨剩 1 台,資料庫執行 -1,但小明網太卡了,資料庫剛執行完 -1,redis 沒來得及更新成0,小紅的華為5G直接下單,redis 剩1台,資料庫-1,redis -1,下單成功一氣呵成。結果就是2個人買了同一臺手機。

這種業務場景可以說比比皆是,所以要解決這種數據同步問題就要有對應的辦法,所以發明瞭java鎖這個工具來保證數據的一致性,舉個例子:

在一個不分男女的公共廁所中上一把鎖,有人進去,把門鎖住,上完出來,把鎖打開,以此類推。

二、2個重要的java鎖

synchronized關鍵字

synchronized關鍵字是java開發人員最常用的給共用資源上鎖的方式,也基本可以滿足一般的進程同步要求,使用 synchronized 無需手動執行加鎖和釋放鎖的操作,只需在需要同步的代碼塊、普通方法、靜態方法上加入該關鍵字即可,JVM 層面會幫我們自動的進行加鎖和釋放鎖的操作。

修飾普通方法

/**
 * synchronized 修飾普通方法
 */
public synchronized void method() {
    // ....
}

當 synchronized 修飾普通方法時,被修飾的方法被稱為同步方法,其作用範圍是整個方法,作用的對象是調用這個方法的對象。

修飾靜態方法

/**
 * synchronized 修飾靜態方法
 */
public static synchronized void staticMethod() {
    // .......
}

當 synchronized 修飾靜態方法時,其作用範圍是整個程式,這個鎖對於所有調用這個鎖的對象都是互斥的。

修飾普通方法 VS 修飾靜態方法

創建一個類,其中有synchronized修飾的普通方法和synchronized修飾的靜態方法。

public class SynchronizedUsage {
    /**
     * synchronized 修飾普通方法
     */
    public synchronized void method() {
        System.out.println("普通方法執行時間:" + LocalDateTime.now());
        try {
            // 休眠 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * synchronized 修飾靜態方法
     */
    public static synchronized void staticMethod() {
        System.out.println("靜態方法執行時間:" + LocalDateTime.now());
        try {
            // 休眠 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

測試

    public class Test01 {
        /**
         * 創建線程池同時執行任務
         */
        static ExecutorService threadPool = Executors.newFixedThreadPool(10);

        public static void main(String[] args) {

            // 執行兩次靜態方法
            threadPool.execute(() -> {
                SynchronizedUsage.staticMethod();
            });
            threadPool.execute(() -> {
                SynchronizedUsage.staticMethod();
            });

            // 執行兩次普通方法
            threadPool.execute(() -> {
                SynchronizedUsage usage = new SynchronizedUsage();
                usage.method();
            });
            threadPool.execute(() -> {
                SynchronizedUsage usage2 = new SynchronizedUsage();
                usage2.method();
            });
        }
    }

結果

說明:

普通方法的2次調用歸屬於不同的對象,也就是不同的鎖,所以執行的時候互不影響。

靜態方法的2次調用歸屬於同一個類,也就是相同的鎖,所以分先後執行,間隔3s。

修飾代碼塊

我們在日常開發中,最常用的是給代碼塊加鎖,而不是給方法加鎖,因為給方法加鎖,相當於給整個方法全部加鎖,這樣的話鎖的粒度就太大了,程式的執行性能就會受到影響,所以通常情況下,我們會使用 synchronized 給代碼塊加鎖,它的實現語法如下:

public void classMethod() throws InterruptedException {
    // 前置代碼...
    
    // 加鎖代碼
    synchronized (SynchronizedUsage.class) {
        // ......
    }
    
    // 後置代碼...
}

從上述代碼我們可以看出,相比於修飾方法,修飾代碼塊需要自己手動指定加鎖對象,加鎖的對象通常使用 this 或 xxx.class 這樣的形式來表示,比如以下代碼:

// 加鎖某個類
synchronized (SynchronizedUsage.class) {
    // ......
}

// 加鎖當前類對象
synchronized (this) {
    // ......
}

以上2中加鎖方式類似於上文中普通方法與靜態方法的區別,加鎖當前類對象this只作用於當前對象,對象不同則鎖不同,加鎖某個類則作用於該類,同屬於一個類的對象使用同一把鎖。

創建一個類

    public class SynchronizedUsageBlock {
        /**
         * synchronized(this) 加鎖
         */
        public void thisMethod() {
            synchronized (this) {
                System.out.println("synchronized(this) 加鎖:" + LocalDateTime.now());
                try {
                    // 休眠 3s
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * synchronized(xxx.class) 加鎖
         */
        public void classMethod() {
            synchronized (SynchronizedUsageBlock.class) {
                System.out.println("synchronized(xxx.class) 加鎖:" + LocalDateTime.now());
                try {
                    // 休眠 3s
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

測試

    public class Test02 {
        public static void main(String[] args) {
            // 創建線程池同時執行任務
            ExecutorService threadPool = Executors.newFixedThreadPool(10);

            // 執行兩次 synchronized(this)
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage = new SynchronizedUsageBlock();
                usage.thisMethod();
            });
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage2 = new SynchronizedUsageBlock();
                usage2.thisMethod();
            });

            // 執行兩次 synchronized(xxx.class)
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage3 = new SynchronizedUsageBlock();
                usage3.classMethod();
            });
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage4 = new SynchronizedUsageBlock();
                usage4.classMethod();
            });
        }
    }

結果

Lock介面

Lock介面及其相關的實現類是在JDK 1.8之後在併發包中新增的,最常用且常見的就是ReentrantLock。與synchronized不同的是,ReentrantLock在使用時需要顯式的獲取和釋放鎖。

雖然它缺少了隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。

Lock介面提供的synchronized不具備的特性

Lock介面中定義的方法

儘管java實現的鎖機制有很多種,並且有些鎖機制性能也比synchronized高,但還是強烈推薦在多線程應用程式中使用該關鍵字,因為實現方便,後續工作由jvm來完成,可靠性高。只有在確定鎖機制是當前多線程程式的性能瓶頸時,才考慮使用其他機制,如ReentrantLock等。

三、java鎖的核心分類

悲觀鎖

悲觀鎖總是假設最壞的情況,每次取數據時都認為其他線程會對數據進行修改,所以都會加鎖,當其他線程想要訪問數據時,都需要阻塞掛起。所以悲觀鎖總結為悲觀加鎖阻塞線程

  • • 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。

MySQL資料庫中的表鎖、行鎖、讀鎖、寫鎖等,Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。

樂觀鎖

而樂觀鎖認為自己在使用數據時不會有別的線程修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入。如果數據已經被其他線程更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)。總結為樂觀無鎖回滾重試

  • • 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。

  • • 樂觀鎖天生免疫死鎖。

樂觀鎖一般有2種實現方式:

CAS演算法

即compare and swap 或者 compare and set,涉及到三個操作數,數據所在的記憶體值,預期值,新值。當需要更新時,判斷當前記憶體值與之前取到的值是否相等,若相等,則用新值更新,若失敗則重試,一般情況下是一個自旋操作,即不斷的重試。

優點:

效率比較高,無阻塞,無等待,重試。

缺點:

ABA問題: 因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼CAS檢查時發現它的值沒有發生變化,但實際上發生了變化:A->B->A的過程。

迴圈時間長,開銷大: 自旋CAS如果長時間不成功,會給CPU帶來很大的執行開銷。

只能保證一個共用變數的原子操作: 當對一個共用變數操作時,我們可以採用CAS的方式來保證原子操作,但是對多個共用變數操作時,迴圈CAS就無法保證操作的原子性。

版本號機制

一般是在數據表中加上一個數據版本號version欄位,表示數據被修改的次數,當數據被修改時,version值會加1。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值為當前資料庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

    update table 
    set x = x + 1, version = version + 1 
    where id = #{id} and version = #{version};

MybaisPlus對樂觀鎖的實現

1)在資料庫中添加 version 欄位,作為樂觀鎖的版本號

    --在資料庫中的user表中添加一個version欄位,用於實現樂觀鎖
    ALTER TABLE `user` ADD COLUMN `version` INT

2)在對應的實體類中添加 version 屬性,並且在這個屬性上面添加 @Version 註解

    @Data
    public class User {
        @TableId(type = IdType.AUTO)//主鍵自動增長
        private Long id;
        private String name;
        private Integer age;
        private String email;
     
        @TableField(fill = FieldFill.INSERT)//INSERT的含義就是添加,也就是說在做添加操作時,下麵一行中的createTime會有值
        private Date createTime;
     
        @TableField(fill = FieldFill.INSERT_UPDATE)//INSERT_UPDATE的含義就是在做添加和修改時下麵一行中的updateTime都會有值,因為是第一次添加,還沒有做修改(一般都使用這個)
        private Date updateTime;
     
        @Version//版本號,用於實現樂觀鎖(這個一定要加)
        @TableField(fill = FieldFill.INSERT)//添加這個註解是為了在後面設置初始值,不加也可以
        private Integer version;
    }

3)寫一個配置類,配置樂觀鎖插件

    @Configuration
    @MapperScan("cn.hb.mapper")
    public class MpConfig {
        //樂觀鎖插件
        @Bean
        public OptimisticLockerInterceptor optimisticLockerInterceptor() {
            return new OptimisticLockerInterceptor();
        }
    }

4)設置版本號 version 的初始值為1

5)向表中添加一條數據,看 version 的值是否為1

6)測試樂觀鎖,看 version 的值是否加1

四、java鎖的其他分類

synchronized性能優化

鎖膨脹/鎖升級

JDK 6之前synchronized是一個獨占式的悲觀鎖、重量級鎖,效率偏低。JDK 6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。

所以目前鎖一共有4種狀態,級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態只能升級不能降級。(註意無鎖和偏向鎖是同一級別,鎖標誌位都是01,二者之間不存在膨脹關係,可以理解為無鎖狀態是輕量鎖的空閑狀態)

偏向鎖

在程式第一次執行到 synchronized 代碼塊的時候,鎖對象變成 偏向鎖 ,即偏向於第一個獲得它的線程的鎖。在程式第二次執行到改代碼塊時,線程會判斷此時持有鎖的線程是否就是它自己,如果是就繼續往下麵執行。值得註意的是,在第一次執行完同步代碼塊時,並不會釋放這個偏向鎖。從效率角度來看,如果第二次執行同步代碼塊的線程一直是一個,並不需要重新做加鎖操作,沒有額外開銷,效率極高。

輕量級鎖

當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。

這裡不同情況需值得註意:當第二個線程想要獲取鎖時,且這個鎖是偏向鎖時,會判斷當前持有鎖的線程是否仍然存活,如果該持有鎖的線程沒有存活,那麼偏向鎖並不會升級為輕量級鎖 。

重量級鎖

若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。

當其他線程再次嘗試獲取鎖的時候,發現現在的鎖是重量級鎖,此時線程都會進入阻塞狀態。

鎖消除

鎖消除即刪除不必要的加鎖操作。JVM在運行時,對一些“在代碼上要求同步,但是**被檢測到不可能存在共用數據競爭情況”的鎖進行消除。**根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼就可以認為這段代碼是線程安全的,無需加鎖。

鎖粗化

假設一系列的連續操作都會對同一個對象反覆加鎖及解鎖,甚至加鎖操作是出現在迴圈體中的,即使沒有出現線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。

如果JVM檢測到有一連串零碎的操作都是對同一對象的加鎖,將會擴大加鎖同步的範圍(即鎖粗化)到整個操作序列的外部。

自適應自旋鎖

自旋鎖

現在絕大多數的個人電腦和伺服器都是多路(核)處理器系統,如果物理機器有一個以上的處理器或者處理器核心,能讓兩個或以上的線程同時並行執行,就可以讓後面請求鎖的那個線程“稍等一會”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。自旋鎖優點在於它避免一些線程的掛起和恢復操作,因為掛起線程和恢複線程都需要從用戶態轉入內核態,這個過程是比較慢的,所以通過自旋的方式可以一定程度上避免線程掛起和恢復所造成的性能開銷。

自適應自旋鎖:

但是,如果長時間自旋還獲取不到鎖,那麼也會造成一定的資源浪費,所以我們通常會給自旋設置一個固定的值來避免一直自旋的性能開銷。然而對於 synchronized 關鍵字來說,它的自旋鎖更加的“智能”,synchronized 中的自旋鎖是自適應自旋鎖。

自適應自旋鎖是指,**線程自旋的次數不再是固定的值,而是一個動態改變的值,這個值會根據前一次自旋獲取鎖的狀態來決定此次自旋的次數。**比如上一次通過自旋成功獲取到了鎖,那麼這次通過自旋也有可能會獲取到鎖,所以這次自旋的次數就會增多一些,而如果上一次通過自旋沒有成功獲取到鎖,那麼這次自旋可能也獲取不到鎖,所以為了避免資源的浪費,就會少迴圈或者不迴圈,以提高程式的執行效率。簡單來說,如果線程自旋成功了,則下次自旋的次數會增多,如果失敗,下次自旋的次數會減少。

防止死鎖

• 不要寫嵌套鎖,容易死鎖

• 儘量少用同步代碼塊(Synchronized)

• 儘量使用ReentrantLock的tryLock方法設置超時時間,超時可以退出,防止死鎖

• 儘量降低鎖粒度,儘量不要幾個功能一把鎖

公平鎖與非公平鎖

當一個線程持有的鎖釋放時,其他線程按照先後順序,先申請的先得到鎖,那麼這個鎖就是公平鎖。反之,如果後申請的線程有可能先獲取到鎖,就是非公平鎖 。

Java 中的 ReentrantLock 可以通過其構造函數來指定是否是公平鎖,預設是非公平鎖。一般來說,使用非公平鎖可以獲得較大的吞吐量,所以推薦優先使用非公平鎖

synchronized 是一種非公平鎖。

可重入鎖和非可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。

    public class Widget {
        public synchronized void doSomething() {
            System.out.println("方法1執行...");
            doOthers();
        }

        public synchronized void doOthers() {
            System.out.println("方法2執行...");
        }
    }

在上面的代碼中,類中的兩個方法都是被內置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法。因為內置鎖是可重入的,所以同一個線程在調用doOthers()時可以直接獲得當前對象的鎖,進入doOthers()進行操作。

如果是一個不可重入鎖,那麼當前線程在調用doOthers()之前需要將執行doSomething()時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且無法釋放。所以此時會出現死鎖。

獨享鎖與共用鎖

獨占鎖是一種思想:只能有一個線程獲取鎖,以獨占的方式持有鎖。

Java中用到的獨占鎖:synchronized,ReentrantLock

共用鎖是一種思想:可以有多個線程獲取讀鎖,以共用的方式持有鎖。

Java中用到的共用鎖:ReentrantReadWriteLock。


往期推薦:

● 師爺,翻譯翻譯什麼叫AOP

終於搞懂動態代理了!

● 學會@ConfigurationProperties月薪過三千

● 0.o?讓我看看怎麼個事兒之SpringBoot自動配置

● 不是銀趴~是@Import!

● Java反射,看完就會用


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Java 迴圈 迴圈可以執行一個代碼塊,只要達到指定的條件。迴圈很方便,因為它們節省時間,減少錯誤,並使代碼更易讀。 Java While 迴圈 while 迴圈會迴圈執行一個代碼塊,只要指定的條件為真: 語法 while (condition) { // 要執行的代碼塊 } 在下麵的示例中,只要變 ...
  • 本篇文章將帶大家運行 Flink 最簡單的程式 WordCount。先實踐後理論,對其基本輸入輸出、編程代碼有初步瞭解,後續篇章再對 Flink 的各種概念和架構進行介紹。 下麵將從創建項目開始,介紹如何創建出一個 Flink 項目;然後從 DataStream 流處理和 FlinkSQL 執行兩種... ...
  • 背景 項目當中需要進行業務時間的校驗,如上午 9:00-下午 17:00,在 9:00 前或 17:00 後是不能處理相關業務的。因此在業務校驗的 Service 中定義了一個 checkBizTime() 方法。原本代碼如下: public void checkBizTime() { Date c ...
  • 本文介紹在Visual Studio軟體中配置、編譯C++環境下matplotlibcpp庫的詳細方法。 matplotlibcpp庫是一個C++環境下的繪圖工具,其通過調用Python介面,實現在C++代碼中通過matplotlib庫的命令繪製各類圖像。由於其需要調用Python介面,因此在配置m ...
  • 曾經有一位魔術師,他擅長將Spring Boot和Redis這兩個強大的工具結合成一種令人驚嘆的組合。他的魔法武器是Redis的Lua腳本。 今天,我們將揭開這個魔術師的秘密,探討如何在Spring Boot項目中使用Lua腳本,以解鎖新的可能性和提高性能。如果你一直在尋找提升你的應用程式的方法,那 ...
  • 如何在運行主方法的同時非同步運行另一個方法,我是用來更新緩存; 1. 工具類 public class ThreadPoolUtils { private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolUtils.clas ...
  • class_2 構造函數 構造函數是一種特殊的成員函數,用於創建和初始化類的對象。它的名稱與類的名稱相同,沒有返回值,也不需要顯式調用。在C++中,每個類都必須至少有一個構造函數。 當我們創建一個類的對象時,編譯器會自動調用構造函數來初始化該對象的成員變數。構造函數可以執行一些操作,如初始化成員變數 ...
  • Java Math Java 的 Math 類 擁有許多方法,允許您在數字上執行數學任務。 常用方法: Math.max(x, y): 找到 x 和 y 的最大值 Math.min(x, y): 找到 x 和 y 的最小值 Math.sqrt(x): 返回 x 的平方根 Math.abs(x): 返 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...