使用Redis構建簡單的ORM

Reids相關的資料引用

目標

  • 在Redis的基礎上提供強類型的訪問入口
  • 分頁支持
  • 主鍵支持

幾個方案[數據類型]的選擇分析

爲了實現上述目標,針對如下幾種類型進行了思考:git

[基於字符串類型]

使用字符串類型來存儲集合對象。這種方式存在如下幾個問題:github

  • 每次更新操做涉及到整個集合對象
  • 序列化/反序列化會致使性能瓶頸
  • 沒法支持分頁(僅支持內存分頁,每次應用服務器都須要加載全部數據)

[基於集合類型]

使用集合類型(LIST/SET)來存儲集合類型對象。相對於字符串而言,有以下改進:redis

  • 每次更新操做不會影響到整個集合
  • 序列化/反序列化不會致使性能瓶頸
  • 支持分頁,分頁無需加載全部數據

可是仍然存在如下問題:express

  • 沒法支持主鍵(沒法根據Key來獲取數據)
  • 每次更新的粗細粒度爲整個數據"行"

[基於HashSet類型]

使用HashSet來存儲一個對象的每一個FIELD,使用一個對應的KEY來訪問對象。這種方式解決了如下問題:緩存

  • 爲數據訪問提供了鍵支持
  • 能夠根據指定字段來更新數據

可是沒法提供集合支持。服務器

[混合的方案]

使用一個SortedSet來記錄數據集合的全部的KEY,使用不一樣的KEY指向的HashSet存儲集合元素數據。這個方案知足了上述全部的需求,是目前採起的方式。可是仍然有如下問題:app

  • 每次讀取一個對象就須要一次通訊開銷(訪問一次HashSet)

KEY的設計

爲了保證存儲在Redis的鍵值對邏輯上的惟一性,在實現上述方案的時候使用了較長的KEY。一個KEY由如下幾個部分組成:性能

  1. WellKnownReidsKeys,這是一個功能性的劃分,代表這個key對應的值的用途
  2. TypeSpecifiedKey,這個部分反應了這個key對應的值被「結構化」以後的類型信息
  3. 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

收穫

  • 打開了腦洞
  • 開始編寫單元測試
  • 開始更新單元測試

全部的源碼:

http://pan.baidu.com/s/1c2LQjSG

相關文章
相關標籤/搜索