延續Day28.使用單例模式實做線性分配器(1/2)的需求,
我們接下來要開始實作取號的方法。
由於必須使用單例模式(Singleton)來設計取號這個動作,在ASP.NET Core可以很方便的利用DI機制來建立一個Singleton instance。
方式為在Startup.cs的ConfigureServices
這個方法裡面注入我們的Singleton服務物件。
例如:
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddSingleton<IMySingleton>(provider => new MySingleton());
}
如此我們可直接在Controller的建構子帶入注入的的服務物件,並開始使用它。
[Route("api/[controller]")]
public class MyController : BaseController
{
private readonly IMySingleton _mySingleton = null;
public MyController(IMySingleton mySingleton)
{
this._mySingleton = mySingleton;
}
}
另外注入的類型分為:
Inject as | Lifetime |
---|---|
Transient | New instance is provided to every controller and every service. |
Scoped | Service are created once per request. |
Singleton | Single instance throughout the application, lazy singleton. |
Singleton Instance | Create instance when registered, eager singleton. |
可以參考我的這篇文章:[ASP.NET Core] Dependency Injection service lifetime
有了以上DI的基礎後,我們開始來實作取號的單例類別。
public interface IAllocatorGetValProvider
{
/// <summary>
/// 取號
/// </summary>
/// <returns></returns>
long GetNextVal(String key);
}
public sealed class AllocatorGetValProvider : IAllocatorGetValProvider
{
private DbContextFactory _dbFactory = null;
private string _key = String.Empty; //紀錄Key Name
private Int64 _minHiVal = 0;
private Int64 _maxHiVal = 0;
private static Int64 INTERVAL = 10; //minHi~maxHi
private static object block = new object();
public AllocatorGetValProvider(DbContextFactory dbFactory)
{
if (dbFactory != null)
this._dbFactory = dbFactory;
}
/// <summary>
/// 取號
/// </summary>
/// <returns></returns>
public Int64 GetNextVal(String key)
{
lock (block)
{
if (!key.Equals(this._key))
{
//當Singleton被重新建立時(例如AP重啟),強制跳號
this.setMinMaxHi(key: key, isForceReset: true);
this._key = key;
}
else
{
if (this._minHiVal < this._maxHiVal)
{
this._minHiVal++;
}
else
{
this.setMinMaxHi(key: key, isForceReset: true);
}
}
return this._minHiVal;
}
}
/// <summary>
/// 取得NEXT HI
/// </summary>
private void setMinMaxHi(string key, bool isForceReset=false)
{
try
{
//設定 TransactionScope的 Option
TransactionOptions transOptions = new TransactionOptions()
{
IsolationLevel = System.Transactions.IsolationLevel.Serializable,
Timeout = new TimeSpan(0, 0, 1) //timeout : 1 min
};
using (var dbContext = this._dbFactory.CreateDbContext())
using (var dbContextTransaction = dbContext.Database.BeginTransaction())
using (var hlService = new HiLoService<DAL.Models.HiLo>(dbContext))
{
Int64 dbNextHi = 0;
Int64 dbMaxVal = 0;
#region Get current HiLo from database
var hilo = hlService.Get(x => x.Key.Equals(key)).FirstOrDefault();
if (hilo != null)
{
dbNextHi = hilo.NextHi;
dbMaxVal = hilo.MaxValue;
}
else
{
throw new Exception("The key is not exist in HiLo master table!");
}
#endregion
#region 設定Singleton可用的minHi/maxHi value
//this.setMinMaxHiValStrategy(dbNextHi, dbMaxVal);
if (isForceReset || (this._minHiVal + 1) > dbMaxVal)
{
//重新設定新Range
this._minHiVal = dbNextHi + INTERVAL;
this._maxHiVal = dbMaxVal + INTERVAL;
hilo.NextHi = this._minHiVal;
hilo.MaxValue = this._maxHiVal;
hlService.Update(hilo);
}
else
{
this._maxHiVal = dbMaxVal;
}
#endregion
dbContextTransaction.Commit();
}
}
catch (Exception)
{
throw;
}
}
}
以上的程式碼重點在於:
INTERVAL
: 決定每次分配號碼範圍的大小,例如設定INTERVAL=10,表示每次只會分配10個號碼。_minHiVal
和_maxHiVal
並更新回資料庫。_minHiVal
和_maxHiVal
將不具有參考價值,而必須回到資料庫取得新的分配範圍。
例如:當INTERVAL100且已分配101-200,目前已取號到135,但是在AP重啟(等於Singleton物件被重新建立)及資料庫不記錄已用到哪一個號碼情況下,將直接重新分配201-300。 所以AP重啟後,Client拿到的第一個號碼是201。
public void ConfigureServices(IServiceCollection services)
{
#region Singleton HiLo-GetValue Provider
var dbFactory = new DbContextFactory(CurrentEnvironment.EnvironmentName);
services.AddSingleton<IAllocatorGetValProvider>(provider => new AllocatorGetValProvider(dbFactory));
#endregion
}
接著我們可以在API Controller使用這個注入的Singleton物件,並將之作為參數丟給分配器管理者AllocatorManager
的GetNextVal
方法 (這邊使用了策略模式!)。
[Route("api/[controller]")]
public class AllocatorController : BaseController
{
private readonly IHostingEnvironment _env = null;
private readonly IAllocatorGetValProvider getValProvider = null;
public AllocatorController(IHostingEnvironment env, IAllocatorGetValProvider getVal)
{
this._env = env;
this.getValProvider = getVal;
}
// GET api/hilo/keyName
[Route("GetNext/{key}")]
public async Task<Sequence> GetNext(String key)
{
if (String.IsNullOrEmpty(key))
{
throw new HttpRequestException("The key should not be NULL!");
}
else
{
using(var dbFactory = new DbContextFactory(this._env.EnvironmentName))
using(var allocatorMng = new AllocatorManager(dbFactory))
{
var seq = allocatorMng.GetNextVal(key, this.getValProvider);
return seq;
}
}
}
}
最後我們更新分配器管理者AllocatorManager
的GetNextVal
方法如下:
public class AllocatorManager : IDisposable
{
private DbContextFactory _dbFactory = null;
public AllocatorManager(DbContextFactory dbFactory)
{
this._dbFactory = dbFactory;
}
/// Create new HiLo instance in database
//Skip CreateHiLoInstance method here...
/// 取得Next value
public Domain.Models.Sequence GetNextVal(string key, IAllocatorGetValProvider getValProvider)
{
var seq = new Domain.Models.Sequence()
{
Key = key,
Value = getValProvider.GetNextVal(key)
};
return seq;
}
}
請確定如Day28文末的方式建立一組Key分別為"TMS","HR"的分配器。(下圖為資料表HiLos的Snapshot)
我們實際利用Postman來取號:
等等! 為什麼第一個號碼不是1呢?
原因在上面有提到因為我們是第一次啟動Web API(等於重啟),所以分配器會分配下一組範圍值給我們。
現在再重新查詢資料表的資料,其應被更新為:
另外可以在Postman將這個Request存到Collection並利用Collection Runner執行多次來觀察取號的情形。
如果要測試Concurrent requests推薦使用SuperBenchmarker。
我們用以下的指令來測試共1,000個requests,每次併發10個Cocurrent request。
sb -u http://localhost:5123/api/HiLo/GetNext/TMS -n 1000 -c 10 -m GET
其結果如以下LOG(只擷取最後面幾個Response logs),可以確認1,000次取號都是沒有重複的。
如果有興趣也可以同時開兩個CMD分別執行下面的sb指令來對不同的分配器作取號,可以觀察到送出request的Key有切換時,原本那一組分配的號碼範圍立即失效,分配器會重新指派下一組。
sb -u http://localhost:5123/api/Allocator/GetNext/TMS -n 1000 -c 10 -m GET -l C:\temp\log1.log
sb -u http://localhost:5123/api/Allocator/GetNext/HR -n 1000 -c 10 -m GET -l C:\temp\log2.log
用了兩天的時間來說明如何利用單例模式(Singleton)來實作線性分配器(Linear Allocator),主要是希望能藉由這篇文章讓大家能理解: