僅此一文讓你明白事務隔離級別、臟讀、不可重覆讀、幻讀

来源:https://www.cnblogs.com/yubaolee/archive/2019/02/25/10398633.html
-Advertisement-
Play Games

百度一下資料庫事務隔離,臟讀等,我想也是一堆。有些老學究扯一堆理論,有些通篇全是代碼,都讓人看的有種說不出蛋疼的感覺。本文用圖文並茂的方式,配上行雲流水般的代碼,非要擺清楚這個問題。本文代碼已提交至碼雲(點擊這裡下載)。 事務是現代關係型資料庫的核心之一。在多個事務併發操作資料庫(多線程、網路併發等 ...


百度一下資料庫事務隔離,臟讀等,我想也是一堆。有些老學究扯一堆理論,有些通篇全是代碼,都讓人看的有種說不出蛋疼的感覺。本文用圖文並茂的方式,配上行雲流水般的代碼,非要擺清楚這個問題。本文代碼已提交至碼雲(點擊這裡下載)。

事務是現代關係型資料庫的核心之一。在多個事務併發操作資料庫(多線程、網路併發等)的時候,如果沒有有效的避免機制,就會出現以下幾種問題:

第一類丟失更新(Lost Update)

在完全未隔離事務的情況下,兩個事務更新同一條數據資源,某一事務完成,另一事務異常終止,回滾造成第一個完成的更新也同時丟失 。這個問題現代關係型資料庫已經不會發生,就不在這裡占用篇幅,有興趣的可以自行百度。

臟讀(Dirty Read)

A事務執行過程中,B事務讀取了A事務的修改。但是由於某些原因,A事務可能沒有完成提交,發生RollBack了操作,則B事務所讀取的數據就會是不正確的。這個未提交數據就是臟讀(Dirty Read)。臟讀產生的流程如下:

微信截圖_20190223000611

可以用EF Core模擬此過程:

    class TestReadUncommitted :TestBase
    {
        private AutoResetEvent _autoResetEvent;

        [Test]
        public void ReadUncommitted()
        {
            using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
            {
                var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                Console.WriteLine($"初始用戶狀態:【{user.Status}】");
            }

            _autoResetEvent = new AutoResetEvent(false);
            ThreadPool.QueueUserWorkItem(data =>{
                Write();  //啟動線程寫
            });
            ThreadPool.QueueUserWorkItem(data =>{
                Read();  //啟動線程讀
            });

            Thread.Sleep(5000);

            using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
            {
                var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                Console.WriteLine($"最終用戶狀態:【{user.Status}】");
            }
        }

        private void Read()
        {
            _autoResetEvent.WaitOne();

            var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted };
            using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
            {
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:臟讀到的用戶狀態:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    //如果這時執行下麵的判斷
                    if (user.Status == 1)
                    {
                        Console.WriteLine("事務B:非正常數據,會產生意想不到的BUG");
                    }
                }
            }
        }
        private void Write()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted}))
            {
                Console.WriteLine($"事務A:修改--{DateTime.Now.ToString("HH:mm:ss fff")}");
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    user.Status = 1-user.Status;  //模擬修改
                    context.SaveChanges();
                }

                _autoResetEvent.Set();  //模擬多線程切換,這時切換到Read線程,復現臟讀

                Thread.Sleep(2000);  //模擬長事務
                Console.WriteLine($"事務A:改完,但沒提交--{DateTime.Now.ToString("HH:mm:ss fff")}");
            }
        }
    }
臟讀示例

對應的執行結果:

捕獲

不可重覆讀(Nonrepeatable Read)

B事務讀取了兩次數據,在這兩次的讀取過程中A事務修改了數據,B事務的這兩次讀取出來的數據不一樣。B事務這種讀取的結果,即為不可重覆讀(Nonrepeatable Read)。不可重覆讀的產生的流程如下:

微信截圖_20190223004632

模擬代碼如下:

public class TestReadCommitted : TestBase
    {
        private AutoResetEvent _toWriteEvent = new AutoResetEvent(false);
        private AutoResetEvent _toReadEvent = new AutoResetEvent(false);

        [Test]
        public void ReadCommitted()
        {
            ThreadPool.QueueUserWorkItem(data => {
                Read();  //啟動線程讀
            });
            ThreadPool.QueueUserWorkItem(data => {
                Write();  //啟動線程寫
            });

            Thread.Sleep(5000);

            using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
            {
                var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                Console.WriteLine($"最終用戶狀態:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
            }

        }

        private void Read()
        {
            using (var transactionScope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
            {
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:第一次讀取:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }

                _toWriteEvent.Set();  //模擬多線程切換,這時切換到寫線程,復現不可重覆讀
                _toReadEvent.WaitOne();

                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:第二次讀取:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }
            }

        }

        private void Write()
        {
            _toWriteEvent.WaitOne();

            using (var scope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
            {
                User user = null;
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務A:讀取為【{user?.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    user.Status = 1 - user.Status;
                    context.SaveChanges();
                }
                scope.Complete();
                Console.WriteLine($"事務A:已被更改為【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                _toReadEvent.Set();
            }
        }
    }
不可重覆讀示例

對應的執行結果:

捕獲

不可重覆讀有一種特殊情況,兩個事務更新同一條數據資源,後完成的事務會造成先完成的事務更新丟失。這種情況就是大名鼎鼎的第二類丟失更新。主流的資料庫已經預設屏蔽了第一類丟失更新問題(即:後做的事務撤銷,發生回滾造成已完成事務的更新丟失),但我們編程的時候仍需要特別註意第二類丟失更新。它產生的流程如下:

微信截圖_20190223114546

模擬代碼如下:

public class TestReadCommitted2 : TestBase
    {
        private AutoResetEvent _toWriteEvent = new AutoResetEvent(false);
        private AutoResetEvent _toReadEvent = new AutoResetEvent(false);

        [Test]
        public void ReadCommitted()
        {
            ThreadPool.QueueUserWorkItem(data => {
                Read();  //啟動線程讀
            });
            ThreadPool.QueueUserWorkItem(data => {
                Write();  //啟動線程寫
            });

            Thread.Sleep(5000);

            using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
            {
                var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                Console.WriteLine($"最終用戶狀態:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
            }
        }

        private void Read()
        {
            using (var transactionScope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
            {
                User user = null;
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:第一次讀取:【{user?.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }

                _toWriteEvent.Set();  //模擬多線程切換,這時切換到寫線程,復現不可重覆讀
                _toReadEvent.WaitOne();

                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:第二次讀取:【{user?.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    user.Status = 1 - user.Status;
                    context.SaveChanges();
                }
                transactionScope.Complete();
                Console.WriteLine($"事務B:已被更改為【{user?.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
            }

        }

        private void Write()
        {
            _toWriteEvent.WaitOne();

            using (var scope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
            {
                User user = null;
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務A:讀取為【{user?.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    user.Status = 1 - user.Status;
                    context.SaveChanges();
                }
                scope.Complete();
                Console.WriteLine($"事務A:已被更改為【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                _toReadEvent.Set();
            }
        }
    }
第二類更新丟失示例

對應的執行結果如下圖:

捕獲2

可以明顯看出事務A的更新被事務B所覆蓋,更新丟失。

幻讀(Phantom Read)

B事務讀取了兩次數據,在這兩次的讀取過程中A事務添加了數據,B事務的這兩次讀取出來的集合不一樣。幻讀產生的流程如下:

微信截圖_20190223102400

這個流程看起來和不可重覆讀差不多,但幻讀強調的集合的增減,而不是單獨一條數據的修改。

模擬代碼如下:

public class TestRepeat : TestBase
    {
        private AutoResetEvent _toWriteEvent = new AutoResetEvent(false);
        private AutoResetEvent _toReadEvent = new AutoResetEvent(false);

        [Test]
        public void Repeat()
        {
            ThreadPool.QueueUserWorkItem(data => {
                Read();  //啟動線程讀
            });
            ThreadPool.QueueUserWorkItem(data => {
                Write();  //啟動線程寫
            });

            Thread.Sleep(6000);

        }

        private void Read()
        {
            using (var transactionScope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
            {
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    Console.WriteLine($"事務B:第一次讀取:【{context.Users.Count()}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }

                _toWriteEvent.Set();  //模擬多線程切換,這時切換到寫線程,復現幻讀
                _toReadEvent.WaitOne();

                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    Console.WriteLine($"事務B:第二次讀取:【{context.Users.Count()}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }
            }

        }

        private void Write()
        {
            _toWriteEvent.WaitOne();

            using (var scope = new TransactionScope(TransactionScopeOption.Required,
                     new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
            {
                Console.WriteLine($"事務A:新增一條--{DateTime.Now.ToString("HH:mm:ss fff")}");
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var id = GenerateId.ShortStr();
                    context.Users.Add(new User { Id = id, Account = id, Status = 0, Name = id, CreateTime = DateTime.Now});
                    context.SaveChanges();
                }
                scope.Complete();
                Console.WriteLine($"事務A:完成新增--{DateTime.Now.ToString("HH:mm:ss fff")}");
                _toReadEvent.Set();
            }
        }
    }
幻讀示例

執行結果:

捕獲3

資料庫隔離級別

為瞭解決上面提及的併發問題,主流關係型資料庫都會提供四種事務隔離級別。

讀未提交(Read Uncommitted)

在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別是最低的隔離級別,雖然擁有超高的併發處理能力及很低的系統開銷,但很少用於實際應用。因為採用這種隔離級別隻能防止第一類更新丟失問題,不能解決臟讀,不可重覆讀及幻讀問題。

讀已提交(Read Committed)

這是大多數資料庫系統的預設隔離級別(但不是MySQL預設的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別可以防止臟讀問題,但會出現不可重覆讀及幻讀問題。

可重覆讀(Repeatable Read)

這是MySQL的預設事務隔離級別,它確保同一事務的多個實例在併發讀取數據時,會看到同樣的數據行。這種隔離級別可以防止除幻讀外的其他問題。

可串列化(Serializable)

這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀、第二類更新丟失問題。在這個級別,可以解決上面提到的所有併發問題,但可能導致大量的超時現象和鎖競爭,通常資料庫不會用這個隔離級別,我們需要其他的機制來解決這些問題:樂觀鎖和悲觀鎖。

這四種隔離級別會產生的問題如下(網上到處都有,懶得畫了):

20160319184334938

如何使用資料庫的隔離級別

很多文章博客在介紹完這些隔離級別以後,就沒有以後了。讀的人一般會覺得,嗯,是這麼回事,我知道了!

學習一個知識點,是需要實踐的。比如下麵這個常見而又異常嚴重的情況:

clipboard

圖中是典型的第二類丟失更新問題,後果異常嚴重。我們這裡就以讀已提交(Read Committed)及以下隔離級別中會出現不可重覆讀現象為例。從上面的表格可以看出,當事務隔離級別為可重覆讀(Repeatable Read)時可以避免。把TestReadCommitted中的Read線程事務級別調整一下:

////////////////////////////////////////////////////////////////////////////////////////////////////
// file:	Test\TestReadCommitted.cs
//
// summary:	讀已提交會出現“不可重覆讀”現象
//              把讀線程(事務B)的隔離級別調整到RepeatableRead,即可杜絕
////////////////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Linq;
using System.Threading;
using System.Transactions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using TestTransaction.Domain;

namespace TestTransaction.Test
{
    public class TestReadCommitted : TestBase
    {
        private AutoResetEvent _toWriteEvent = new AutoResetEvent(false);
        private AutoResetEvent _toReadEvent = new AutoResetEvent(false);

        [Test]
        public void ReadCommitted()
        {
            ThreadPool.QueueUserWorkItem(data => {
                Read();  //啟動線程讀
            });
            ThreadPool.QueueUserWorkItem(data => {
                Write();  //啟動線程寫
            });

            Thread.Sleep(60000);

            using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
            {
                var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                Console.WriteLine($"最終用戶狀態:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
            }

        }

        private void Read()
        {
            //讀線程(事務B)的隔離級別調整到RepeatableRead
            using (var transactionScope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead, Timeout = TimeSpan.FromSeconds(40) }))
            {
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:第一次讀取:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }

                _toWriteEvent.Set();  //模擬多線程切換,這時切換到寫線程,復現不可重覆讀
                _toReadEvent.WaitOne();

                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    var user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務B:第二次讀取:【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                }
            }

        }

        private void Write()
        {
            _toWriteEvent.WaitOne();

            using (var scope = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromSeconds(5) }))
            {
                User user = null;
                using (var context = _autofacServiceProvider.GetService<OpenAuthDBContext>())
                {
                    user = context.Users.SingleOrDefault(u => u.Account == "admin");
                    Console.WriteLine($"事務A:讀取為【{user?.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    user.Status = 1 - user.Status;
                    try
                    {
                        context.SaveChanges();
                        scope.Complete();

                        Console.WriteLine($"事務A:已被更改為【{user.Status}】--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    }
                    catch (DbUpdateException e)
                    {
                        Console.WriteLine($"事務A:異常,為了保證可重覆讀,你的修改提交失敗,請稍後重試--{DateTime.Now.ToString("HH:mm:ss fff")}");
                    }
                }
                _toReadEvent.Set();
            }
        }
    }
}

這時執行效果如下:

捕獲3

實際項目中,通過提示客戶端重做的方式,完美解決了不可重覆讀的問題。其他併發問題,也可以通過類似的方式解決。

最後,文中提到可串列化解決幻讀的問題,會在下篇文章詳細介紹,包含各種酷炫的樂觀鎖操作,敬請期待!本文唯一訪問地址:https://www.cnblogs.com/yubaolee/p/10398633.html


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

-Advertisement-
Play Games
更多相關文章
  • 導讀:Python貓是一隻喵星來客,它愛地球的一切,特別愛優雅而無所不能的 Python。我是它的人類朋友豌豆花下貓,被授權潤色與發表它的文章。如果你是第一次看到這個系列文章,那我強烈建議,請先看看它寫的前幾篇文章(鏈接見文末),相信你一定會愛上這隻神秘的哲學+極客貓的。不多說啦,一起來享用今天的“ ...
  • Win10構建Python全棧開發環境With WSL === [toc] 在學習Python全棧過程中,隨著後面地深入,進入實際項目地開發階段後,越發地發現,項目要使用的第三方軟體(redis git等)或者外界(支付寶開放平臺/微信開放平臺)聯繫越來越多,自己構建一個開發web服務,以及Pyth ...
  • 使用maven package打包項目時出現配置文件丟失的現象,此類問題解決辦法如下: 在web項目pom.xml 文件中添加如下: 在<build>標簽中添加如下配置: 說明:<include>標簽中配置自己項目中配置文件的尾碼名稱就闊以; 完畢! ...
  • 一、vector類型簡介 標準庫:集合或動態數組,我們可以放若幹對象放在裡面。 vector他能把其他對象裝進來,也被稱為容器 二、定義和初始化vector對象 (1)空的vector (2)元素拷貝的初始化方式 (3)C++11標準中,用列表初始化方法給值,用{}括起來 (4)創建指定數量的元素 ...
  • 原文地址:http://www.runoob.com/python/python-func-open.html ...
  • 一、前言 int,float,char,C++標準庫提供的類型:string,vector。 string:可變長字元串的處理;vector一種集合或者容器的概念。 二、string類型簡介 C++標準庫中的類型,代表一個可變長的字元串 char str[100] = “I Love China”; ...
  • 對二維數組指定的鍵名排序,首先大家想到的是array_multisort函數,關於array_multisort的用法我之前也寫了一篇廢話不多言,我們看個實例: 細心的朋友會看到,鍵名重置了,鍵名從0開始,顯然這可能不是我們想要的結果,那如何保持鍵名不變? 我們再看個示例: 這裡我們也可以精簡下ar ...
  • 一、前言 在非靜態頁面的項目開發中,必定會涉及到對於資料庫的訪問,最開始呢,我們使用 Ado.Net,通過編寫 SQL 幫助類幫我們實現對於資料庫的快速訪問,後來,ORM(Object Relational Mapping,對象關係映射)出現了,我們開始使用 EF、Dapper、NHibernate ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...