[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();
});
}
}
}
view raw startup.cs hosted with ❤ by GitHub
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>";
view raw testcode.cs hosted with ❤ by GitHub

下載整個專案來測試:

https://github.com/donma/SampleBufferFetchInt32


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

筆記一下。


當麻許的超技八 2014 | Donma Hsu Design.