Reids相關的資料引用
- http://www.tuicool.com/articles/bURJRj [Reids各種數據類型的應用場景]
- https://github.com/antirez/redis [Github Reids]
- https://github.com/StackExchange/StackExchange.Redis [Github StackExchangeReids]
目標
- 在Redis的基礎上提供強類型的訪問入口
- 分頁支持
- 主鍵支持
幾個方案[數據類型]的選擇分析
為了實現上述目標,針對以下幾種類型進行了思考:
[基於字符串類型]
使用字符串類型來存儲集合對象。這種方式存在以下幾個問題:
- 每次更新操作涉及到整個集合對象
- 序列化/反序列化會導致性能瓶頸
- 無法支持分頁(僅支持內存分頁,每次應用服務器都需要加載所有數據)
[基於集合類型]
使用集合類型(LIST/SET)來存儲集合類型對象。相對於字符串而言,有如下改進:
- 每次更新操作不會影響到整個集合
- 序列化/反序列化不會導致性能瓶頸
- 支持分頁,分頁無需加載所有數據
但是仍然存在以下問題:
- 無法支持主鍵(無法根據Key來獲取數據)
- 每次更新的粗細粒度為整個數據"行"
[基於HashSet類型]
使用HashSet來存儲一個對象的每個FIELD,使用一個對應的KEY來訪問對象。這種方式解決了以下問題:
- 為數據訪問提供了鍵支持
- 可以根據指定字段來更新數據
但是無法提供集合支持。
[混合的方案]
使用一個SortedSet來記錄數據集合的所有的KEY,使用不同的KEY指向的HashSet存儲集合元素數據。這個方案滿足了上述所有的需求,是目前采取的方式。但是仍然有以下問題:
- 每次讀取一個對象就需要一次通信開銷(訪問一次HashSet)
KEY的設計
為了保證存儲在Redis的鍵值對邏輯上的唯一性,在實現上述方案的時候使用了較長的KEY。一個KEY由以下幾個部分組成:
- WellKnownReidsKeys,這是一個功能性的划分,表明這個key對應的值的用途
- TypeSpecifiedKey,這個部分反應了這個key對應的值被“結構化”之后的類型信息
- CustomizedKey,這個是一個自定義的Key,方便使用的時候擴展
在Redis中,一個KEY應該形如:[WellKnownReidsKeys][TypeSpecifiedKey][CustomizedKey]。其中,CustomizedKey可以將同類型的數據集合拆分成不同的區塊,獨立管理。
幾個性能問題
[強類型對象轉字典問題]
使用了運行時構造表達式目錄樹進行編譯的方式來減少反射開銷,代碼如下:
public Func<T, IDictionary<string, string>> Compile(string key)
{
var outType = typeof (Dictionary<string, string>);
var func = ConcurrentDic.GetOrAdd(key, k =>
{
var tType = typeof (T);
var properties = tType.GetProperties();
var expressions = new List<Expression>();
//public T xxx(IDataReader reader){
var param = Expression.Parameter(typeof (T));
//var instance = new T();
var newExp = Expression.New(outType);
var varExp = Expression.Variable(outType, "instance");v
var varAssExp = Expression.Assign(varExp, newExp);
expressions.Add(varAssExp);
var indexProp = typeof (IDictionary<string, string>).GetProperties().Last(p => p.Name == "Item");
var strConvertMethod = typeof (object).GetMethod("ToString");
foreach (var property in properties)
{
var propExp = Expression.PropertyOrField(param, property.Name);
Expression indexAccessExp = Expression.MakeIndex(varExp, indexProp,
new Expression[] {Expression.Constant(property.Name)});
var strConvertExp = Expression.Condition(Expression.Equal(Expression.Constant(null), Expression.Convert(propExp,typeof(object))),
Expression.Constant(string.Empty), Expression.Call(propExp, strConvertMethod));
var valueAssignExp = Expression.Assign(indexAccessExp, strConvertExp);
expressions.Add(valueAssignExp);
}
//return instance;
var retarget = Expression.Label(outType);
var returnExp = Expression.Return(retarget, varExp);
expressions.Add(returnExp);
//}
var relabel = Expression.Label(retarget, Expression.Default(outType));
expressions.Add(relabel);
var blockExp = Expression.Block(new[] {varExp}, expressions);
var expression = Expression.Lambda<Func<T, IDictionary<string, string>>>(blockExp, param);
return expression.Compile();
});
return func;
}
對於單次轉換,表達式的編譯結果根據類型信息和字典的KEY信息做了緩存,從而提升性能。對於集合轉換,對於每個集合的操作,每次使用的委托都是同一個從而減少了字典索引的開銷。以下是一個以硬編碼代碼為了測試基准的性能比對:
public void ModelStringDicTransfer()
{
var customer = new ExpressionFuncTest.Customer
{
Id = Guid.NewGuid(),
Name = "TestMap",
Age = 25,
Nick = "Test",
Sex = 1,
Address = "Hello World Street",
Tel = "15968131264"
};
const int RunCount = 10000000;
GetDicByExpression(customer);
var time = StopwatchHelper.Timing(() =>
{
int count = RunCount;
while (count-- > 0)
{
GetDicByExpression(customer);
}
});
var baseTime = StopwatchHelper.Timing(() =>
{
int count = RunCount;
while (count-- > 0)
{
GetDicByHardCode(customer);
}
});
Console.WriteLine("time:{0}\tbasetime:{1}", time, baseTime);
Assert.IsTrue(baseTime * 3 >= time);
}
private Func<ExpressionFuncTest.Customer, IDictionary<string, string>> _dicMapper;
private IDictionary<string, string> GetDicByExpression(ExpressionFuncTest.Customer customer)
{
_dicMapper = _dicMapper ?? ModelStringDicTransfer<ExpressionFuncTest.Customer>.Instance.Compile(
typeof(ExpressionFuncTest.Customer).FullName);
return _dicMapper(customer);
}
private Dictionary<string, string> GetDicByHardCode(ExpressionFuncTest.Customer customer)
{
var dic = new Dictionary<string, string>();
dic.Add("Name", customer.Name);
dic.Add("Address", customer.Address);
dic.Add("Nick", customer.Nick);
dic.Add("Tel", customer.Tel);
dic.Add("Id", customer.Id.ToString());
dic.Add("Age", customer.Age.ToString());
dic.Add("Sex", customer.Sex.ToString());
return dic;
}
對於10M的轉換量,硬編碼耗時6s左右,動態轉換耗時10s左右。
[整體的性能測試]
以下是一個針對已經完成的實現的測試:
public void PerformanceTest()
{
var amount = 1000000;
var key = "PerformanceTest";
Fill(amount, key);
PageGetFirst(1, key);
int i = 1;
while (i <= 100000)
{
var count = i;
var fTime = StopwatchHelper.Timing(() => PageGetFirst(count, key));
var lTime = StopwatchHelper.Timing(() => PageGetLast(count, key));
Console.WriteLine("{0}:第一頁耗時:{1}\t最后一頁耗時:{2}", count, fTime, lTime);
i = i*10;
}
}
private void Fill(int count,string partKey)
{
var codes = Enumerable.Range(1000, count).Select(i => i.ToString());
codes.Foreach(i =>
{
var customer = new Customer
{
Id = i == "1000" ? Guid.Empty : Guid.NewGuid(),
Name = "Customer" + i,
Code = i,
Address = string.Format("XX街{0}號", DateTime.Now.Millisecond),
Tel = "15968131264"
};
_pagableHashStore.UpdateOrInsertAsync(customer, customer.Code + "", partKey).Wait();
});
}
private void PageGetFirst(int count,string partKey)
{
var pageInfo = new PageInfo(count, 1);
_pagableHashStore.PageAsync(pageInfo, partKey).Result
.Foreach(i => i.Wait());
}
private void PageGetLast(int count, string partKey)
{
var pageInfo = new PageInfo(count, (100000 - 1)/count + 1);
_pagableHashStore.PageAsync(pageInfo, partKey).Result
.Foreach(i => i.Wait());
}
對於10M數據的分頁測試(默認的插入時間排序,不同的頁長)的結果(時間單位:毫秒):
- 1頁長 第一頁耗時:1, 最后一頁耗時:1
- 10頁長 第一頁耗時:0, 最后一頁耗時:0
- 100頁長 第一頁耗時:2, 最后一頁耗時:5
- 1000頁長 第一頁耗時:33, 最后一頁耗時:35
- 10000頁長 第一頁耗時:214, 最后一頁耗時:316
- 100000頁長 第一頁耗時:3251, 最后一頁耗時:3163
收獲
- 打開了腦洞
- 開始編寫單元測試
- 開始更新單元測試
