[C#] 不透過 Database 取得 auto-increment 的 Int32
今天分享一個有點奇怪的文,因為之前在寫一個取錢包系統,對於取 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 最後我用這方法修正
筆記一下。