[C#] 不透過 Database 取得 auto-increment 的 Int32

2022-03-01

今天分享一個有點奇怪的文,因為之前在寫一個取錢包系統,對於取 HD Wallet 來說,要拿到一個獨一無二的 index 就變得很重要

相關文章可以參考 產生 TRX 錢包,使用 ETH 錢包轉換 透過 Nethereum ,基本上如果沒意外可以取到 Int32  的 Max Value

也就是  2,147,483,647  ,大概二十一億左右。

因為我取錢包的系統是用 Azure Table Storage ,沒有像是 SQL Server 可以開  IDENTITY to perform an auto-increment (自動編號)

所以我得自己掌管不會重複存取到一樣的數值,但是會遇到一次大量進線取用的問題,這時候問題來了

要如何不會被重複取到不透過資料庫。 這是 base on .netcore 3.1


1. 首先我得先規劃一個 static 的 ConcurrentQueue<Int32> 來做 Buffer ,在初始化的時候我會從一個本地檔案讀取最後的數值並且

先預先載入一個數量(程式範例中我暫存 10 個),之後我要取用 都統一到 那個 ConcurrentQueue<Int32> 取用


2. 之後有設計一個 timer 定期將現在  ConcurrentQueue<Int32>  寫回檔案之中


3. 補充一個概念 假設 現在 ConcurrentQueue<Int32> 中 擁有 1,2,3,4,5,6,7,8,9,10 十個數字,使用 Parallel For 模擬高量同時取時

假設我拿到1, 我會將 1+10 後再塞回去,但是你在塞回去的時候 就不保證按照每個執行緒會按照順序塞回去,所以也有可能第二 round

有可能會變成這樣 20,19,17,16,18,15,14,13,11,12 ,所以我們得假設 剛剛那一串數列 在 Timer 中 將17 的時候機器當掉了

所以重啟之後都一慮使用該數值+10 ,之後 Queue 進 ConcurrentQueue<Int32> 等於是有可能中間有一批我沒有用到就直接放棄

所以有可能會有斷的不連續,不過這不影響我的使用,畢竟不重複對我來說才是關鍵

大概是這樣這邊紀錄一下我的  code.


程式碼


using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Timers; namespace SampleBufferFetchInt32 { public class Startup { //Buffer 使用 private static System.Collections.Concurrent.ConcurrentQueue<Int32> _IndexPointerSwap; private static int _IndexPointer; /// <summary> /// 檢查目前走到的 Pointer 然後做儲存 /// </summary> private static Timer _PointerChecker { get; set; } /// <summary> /// Q 暫存數量 /// </summary> private readonly static int _SwapCount = 10; /// <summary> /// 初始化起始數值 /// </summary> private void InitPointerFromFile() { if (!File.Exists(AppDomain.CurrentDomain.BaseDirectory + "COUNT")) { File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "COUNT", "1000"); _IndexPointer = 1000; } else { var tmp = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "COUNT"); _IndexPointer = int.Parse(tmp); } } /// <summary> /// 是否正在執行檢查 /// 如果正在執行跳開 /// </summary> private static bool IsRunningChecker { get; set; } /// <summary> /// Peek Now Pointer. /// </summary> /// <returns></returns> public static int PeekCurrentPointer() { int tmp = 0; while (_IndexPointerSwap.TryPeek(out tmp)) { break; } return tmp; } /// <summary> /// 重新啟動檢查的 Timer /// </summary> public static void RestartTimerChecker() { _PointerChecker = new Timer(); _PointerChecker.Elapsed += (sender, args) => { if (IsRunningChecker) return; //simple lock for recyle. IsRunningChecker = true; int tmp = -1; //檢查目前走到哪裡並且抄寫回 file. if (_IndexPointerSwap.TryPeek(out tmp)) { File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "COUNT", tmp.ToString()); } IsRunningChecker = false; }; _PointerChecker.Interval = 500; _PointerChecker.Start(); } /// <summary> /// 初始化填入多少的buffer pointer. /// </summary> /// <param name="num"></param> private void FillPointer(int num) { //為了避免中間可能被之前用過 //所以必須要用 中間的 buffer 數往後加入 var nP = _IndexPointer + 1 + _SwapCount; for (var i = (nP); i < (nP + num); i++) { _IndexPointerSwap.Enqueue(i); } } /// <summary> /// 取得一個數值 /// </summary> /// <returns></returns> public static int GetOneValue() { int res = -1; while (!_IndexPointerSwap.TryDequeue(out res)) { } _IndexPointerSwap.Enqueue(res + _SwapCount); return res; } public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { //SetPointer From File InitPointerFromFile(); //Fille data to quqeue. _IndexPointerSwap = new System.Collections.Concurrent.ConcurrentQueue<int>(); //Fill 100 to _IndexPointerSwap FillPointer(_SwapCount); //Start Timer RestartTimerChecker(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); } } } Stopwatch st = new Stopwatch(); st.Start(); ConcurrentBag<string> _tmp = new ConcurrentBag<string>(); Parallel.For(0, 1_000, i => { var ts = Startup.GetOneValue(); _tmp.Add(ts.ToString()); }); st.Stop(); //檢查有沒有重複 _checkDup = new HashSet<string>(); foreach (var c in _tmp) { _checkDup.Add(c); } Context += _checkDup.Count + "," + _tmp.Count + "," +Startup.PeekCurrentPointer()+ "," + st.Elapsed + "<br>";

下載整個專案來測試:

https://github.com/donma/SampleBufferFetchInt32


最後說一下,會弄這個是紀念我去年犯了一個錯,導致重複取到 wallet 最後我用這方法修正

筆記一下。


當麻許的碎念筆記 2014 | Donma Hsu Design.