C#客戶端Redis伺服器的分散式快取

2015-08-16    分類:.NET開發、程式設計開發、首頁精華2人評論發表於2015-08-16

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

介紹

在這篇文章中,我想介紹我知道的一種最緊湊的安裝和配置Redis伺服器的方式。另外,我想簡短地概述一下在.NET / C#客戶端下Redis hash(雜湊型別)和list(連結串列)的使用。

在這篇文章主要講到:

  • 安裝Redis伺服器(附完整的應用程式檔案設定
  • Redis伺服器保護(配置身份驗證)
  • 配置伺服器複製
  • 從C#應用程式訪問快取
  • 使用Redis ASP.NET會話狀態
  • Redis 集合(Set)、列表(List)和事務處理用法示例
  • 說明附加的源(Redis Funq LoC MVC專案:舉例)
  • 快取的優化思路

背景

Redis是最快也是功能最豐富的記憶體Key-Value資料儲存系統之一。

缺點

  • 沒有本地資料快取(如在Azure快取同步本地資料快取)
  • 沒有完全叢集化的支援(不過,可能今年年底會實現)

優點

  • 易於配置
  • 使用簡單
  • 高效能
  • 支援不同的資料型別(如hash(雜湊型別)、list(連結串列)、set(集合)、sorted set(有序集))
  • ASP.NET會話整合
  • Web UI用於瀏覽快取內容

下面我將簡單說明如何在伺服器上安裝和配置Redis,並用C#使用它。

Redis的安裝

https://github.com/dmajkic/redis/downloads<wbr><wbr><wbr>(win32 win64直接連結)下載二進位制檔案,解包檔案到應用程式目錄(如C:\Program Files\Redis)

下載從https://github.com/kcherenkov/redis-windows-service/downloads<wbr><wbr><wbr><wbr><wbr>編譯的Redis服務,然後複製到程式資料夾(如C:\Program Files\Redis)。如果配置檔案丟失,也可以下載複製到應用程式目錄。有效的Redis配置檔案的範例在https://raw.github.com/antirez/redis/2.6/redis.conf。<wbr><wbr><wbr>

Redis應用程式的完整檔案也可以從壓縮檔案(x64)得到。

當你擁有了全套的應用程式檔案(如下圖所示),

redis application folder conten

導航到應用程式目錄,然後執行以下命令:

sc create %name% binpath= "\"%binpath%\" %configpath%" start= "auto" DisplayName= "Redis"

其中:

  • %name%——服務例項的名稱,例如:redis-instance;
  • %binpath%——到專案exe檔案的路徑,例如:C:\Program Files\Redis\RedisService_1.1.exe;
  • %configpath%——到Redis配置檔案的路徑,例如:C:\Program Files\Redis\redis.conf;

舉例:

sc create Redis start= auto DisplayName= Redis binpath= "\"C:\Program Files\Redis\RedisService_1.1.exe\
" \"C:\Program Files\Redis\redis.conf\""

即應該是這樣的:

請確保有足夠的許可權啟動該服務。安裝完畢後,請檢查該服務是否建立成功,當前是否正在執行:

或者,你可以使用安裝程式(我沒試過):https://github.com/rgl/redis/downloads<wbr><wbr><wbr>

Redis伺服器保護:密碼,IP過濾

保護Redis伺服器的主要方式是使用Windows防火牆或活躍的網路連線屬性設定IP過濾。此外,還可以使用Redis密碼設定額外保護。這需要用下面的方式更新Redis配置檔案(redis.conf):

首先,找到這行:

# requirepass foobared

刪除開頭的#符號,用新密碼替換foobared:

requirepass foobared

然後,重新啟動Redis Windows服務!

當具體使用客戶端的時候,使用帶密碼的建構函式:

RedisClient client = new RedisClient(serverHost, port, redisPassword);

Redis伺服器複製(主—從配置)

Redis支援主從同步,即,每次主伺服器修改,從伺服器得到通知,並自動同步。大多複製用於讀取(但不能寫)擴充套件和資料冗餘和伺服器故障轉移。設定兩個Redis例項(在相同或不同伺服器上的兩個服務),然後配置其中之一作為從站。為了讓Redis伺服器例項是另一臺伺服器的從屬,可以這樣更改配置檔案:

找到以下程式碼:

# slaveof <masterip> <masterport>

替換為:

slaveof 192.168.1.1 6379

(可以自定義指定主伺服器的真實IP和埠)。如果主伺服器配置為需要密碼(驗證),可以如下所示改變redis.conf,找到這一行程式碼:

# masterauth <master-password>

刪除開頭的#符號,用主伺服器的密碼替換<master-password>,即:

masterauth mastpassword

現在這個Redis例項可以被用來作為主伺服器的只讀同步副本。

用C#程式碼使用Redis快取

用C#程式碼使用Redis執行Manage NuGet包外掛,找到ServiceStack.Redis包,並進行安裝。

直接從例項化客戶端使用Set/Get方法示例:

string host = "localhost";
string elementKey = "testKeyRedis";

using (RedisClient redisClient = new RedisClient(host))
{
      if (redisClient.Get<string>(elementKey) == null)
      {
           // adding delay to see the difference
           Thread.Sleep(5000); 
           // save value in cache
           redisClient.Set(elementKey, "some cached value");
      }
      // get value from the cache by key
      message = "Item value is: " + redisClient.Get<string>("some cached value");
 }

型別化實體集更有意思和更實用,這是因為它們操作的是確切型別的物件。在下面的程式碼示例中,有兩個類分別定義為Phone和Person——phone的主人。每個phone例項引用它的主人。下面的程式碼演示我們如何通過標準新增、刪除和發現快取項:

public class Phone
{
   public int Id { get; set; }
   public string Model { get; set; }
   public string Manufacturer { get; set; }
   public Person Owner { get; set; }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
    public int Age { get; set; }
    public string Profession { get; set; }
}

using (RedisClient redisClient = new RedisClient(host))
{
     IRedisTypedClient<phone> phones = redisClient.As<phone>();
     Phone phoneFive = phones.GetValue("5");
     if (phoneFive == null)
     {
          // make a small delay
          Thread.Sleep(5000);
          // creating a new Phone entry
          phoneFive = new Phone
          {
               Id = 5,
               Manufacturer = "Motorolla",
               Model = "xxxxx",
               Owner = new Person
               {
                    Id = 1,
                    Age = 90,
                    Name = "OldOne",
                    Profession = "sportsmen",
                    Surname = "OldManSurname"
               }
          };
          // adding Entry to the typed entity set
          phones.SetEntry(phoneFive.Id.ToString(), phoneFive);
     }
     message = "Phone model is " + phoneFive.Manufacturer;
     message += "Phone Owner Name is: " + phoneFive.Owner.Name;
}

在上面的例子中,我們例項化了輸入端IRedisTypedClient,它與快取物件的特定型別——Phone型別一起工作。

Redis ASP.NET會話狀態

要用Redis提供商配置ASP.NET會話狀態,新增新檔案到你的Web專案,命名為RedisSessionStateProvider.cs,可以從https://github.com/chadman/redis-service-provider/raw/master/RedisProvider/SessionProvider/RedisSessionProvider.cs<wbr><wbr><wbr><wbr><wbr><wbr><wbr><wbr><wbr>複製程式碼,然後新增或更改配置檔案中的以下部分(sessionState標籤已經內建於system.web標籤),或者你也可以下載附加來源和複製程式碼。

<sessionstate timeout="1" mode="Custom" 
customprovider="RedisSessionStateProvider" cookieless="false">
      <providers>
        <add name="RedisSessionStateProvider" writeexceptionstoeventlog="false" 
        type="RedisProvider.SessionProvider.CustomServiceProvider" 
        server="localhost" port="6379" password="pasword">
      </add> </providers>
</sessionstate>

注意,此密碼是可以選擇的,看伺服器是否需要認證。它必須被真實的值替換或刪除,如果Redis伺服器不需要身份驗證,那麼伺服器屬性和埠得由具體的數值代替(預設埠為6379)。然後在專案中,你才可以使用會話狀態:

// in the Global.asax
public class MvcApplication1 : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        //....
    }

    protected void Session_Start()
    {
        Session["testRedisSession"] = "Message from the redis ression";
    }
}

在Home controller(主控制器):

public class HomeController : Controller
{
    public ActionResult Index()
    {
       //...
       ViewBag.Message = Session["testRedisSession"];
       return View();
    }
//...
}

結果:

ASP.NET輸出快取提供者,並且Redis可以用類似的方式進行配置。

Redis Set(集合)和List(列表)

主要要注意的是,Redis列表實現IList<T>,而Redis集合實現ICollection<T>。下面來說說如何使用它們。

當需要區分相同型別的不同分類物件時,使用列表。例如,我們有“mostSelling(熱銷手機)”和“oldCollection(回收手機)”兩個列表:

string host = "localhost";
using (var redisClient = new RedisClient(host))
{
    //Create a 'strongly-typed' API that makes all Redis Value operations to apply against Phones
    IRedisTypedClient<phone> redis = redisClient.As<phone>();

    IRedisList<phone> mostSelling = redis.Lists["urn:phones:mostselling"];
    IRedisList<phone> oldCollection = redis.Lists["urn:phones:oldcollection"];

    Person phonesOwner = new Person
        {
            Id = 7,
            Age = 90,
            Name = "OldOne",
            Profession = "sportsmen",
            Surname = "OldManSurname"
        };

    // adding new items to the list
    mostSelling.Add(new Phone
            {
                Id = 5,
                Manufacturer = "Sony",
                Model = "768564564566",
                Owner = phonesOwner
            });

    oldCollection.Add(new Phone
            {
                Id = 8,
                Manufacturer = "Motorolla",
                Model = "324557546754",
                Owner = phonesOwner
            });

    var upgradedPhone  = new Phone
    {
        Id = 3,
        Manufacturer = "LG",
        Model = "634563456",
        Owner = phonesOwner
    };

    mostSelling.Add(upgradedPhone);

    // remove item from the list
    oldCollection.Remove(upgradedPhone);

    // find objects in the cache
    IEnumerable<phone> LGPhones = mostSelling.Where(ph => ph.Manufacturer == "LG");

    // find specific
    Phone singleElement = mostSelling.FirstOrDefault(ph => ph.Id == 8);

    //reset sequence and delete all lists
    redis.SetSequence(0);
    redisClient.Remove("urn:phones:mostselling");
    redisClient.Remove("urn:phones:oldcollection");
}

當需要儲存相關的資料集和收集統計資訊,例如answer -> queustion給答案或問題投票時,Redis集合就非常好使。假設我們有很多的問題(queustion)和答案(answer ),需要將它們儲存在快取中。使用Redis,我們可以這麼做:

/// <summary>
/// Gets or sets the Redis Manager. The built-in IoC used with ServiceStack autowires this property.
/// </summary>
IRedisClientsManager RedisManager { get; set; }
/// <summary>
/// Delete question by performing compensating actions to 
/// StoreQuestion() to keep the datastore in a consistent state
/// </summary>
/// <param name="questionId">
public void DeleteQuestion(long questionId)
{
    using (var redis = RedisManager.GetClient())
    {
        var redisQuestions = redis.As<question>();

        var question = redisQuestions.GetById(questionId);
        if (question == null) return;

        //decrement score in tags list
        question.Tags.ForEach(tag => redis.IncrementItemInSortedSet("urn:tags", tag, -1));

        //remove all related answers
        redisQuestions.DeleteRelatedEntities<answer>(questionId);

        //remove this question from user index
        redis.RemoveItemFromSet("urn:user>q:" + question.UserId, questionId.ToString());

        //remove tag => questions index for each tag
        question.Tags.ForEach("urn:tags>q:" + tag.ToLower(), questionId.ToString()));

        redisQuestions.DeleteById(questionId);
    }
}

public void StoreQuestion(Question question)
{
    using (var redis = RedisManager.GetClient())
    {
        var redisQuestions = redis.As<question>();

        if (question.Tags == null) question.Tags = new List<string>();
        if (question.Id == default(long))
        {
            question.Id = redisQuestions.GetNextSequence();
            question.CreatedDate = DateTime.UtcNow;

            //Increment the popularity for each new question tag
            question.Tags.ForEach(tag => redis.IncrementItemInSortedSet("urn:tags", tag, 1));
        }

        redisQuestions.Store(question);
        redisQuestions.AddToRecentsList(question);
        redis.AddItemToSet("urn:user>q:" + question.UserId, question.Id.ToString());

        //Usage of tags - Populate tag => questions index for each tag
        question.Tags.ForEach(tag => redis.AddItemToSet
        ("urn:tags>q:" + tag.ToLower(), question.Id.ToString()));
    }
}

/// <summary>
/// Delete Answer by performing compensating actions to 
/// StoreAnswer() to keep the datastore in a consistent state
/// </summary>
/// <param name="questionId">
/// <param name="answerId">
public void DeleteAnswer(long questionId, long answerId)
{
    using (var redis = RedisManager.GetClient())
    {
        var answer = redis.As<question>().GetRelatedEntities<answer>
        (questionId).FirstOrDefault(x => x.Id == answerId);
        if (answer == null) return;

        redis.As<question>().DeleteRelatedEntity<answer>(questionId, answerId);

        //remove user => answer index
        redis.RemoveItemFromSet("urn:user>a:" + answer.UserId, answerId.ToString());
    }
}

public void StoreAnswer(Answer answer)
{
    using (var redis = RedisManager.GetClient())
    {
        if (answer.Id == default(long))
        {
            answer.Id = redis.As<answer>().GetNextSequence();
            answer.CreatedDate = DateTime.UtcNow;
        }

        //Store as a 'Related Answer' to the parent Question
        redis.As<question>().StoreRelatedEntities(answer.QuestionId, answer);
        //Populate user => answer index
        redis.AddItemToSet("urn:user>a:" + answer.UserId, answer.Id.ToString());
    }
}

public List<answer> GetAnswersForQuestion(long questionId)
{
    using (var redis = RedisManager.GetClient())
    {
        return redis.As<question>().GetRelatedEntities<answer>(questionId);
    }
}

public void VoteQuestionUp(long userId, long questionId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register upvote against question and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:q>user+:" + questionId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:q>user-:" + questionId, userId.ToString()));

        //Register upvote against user and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:user>q+:" + userId, questionId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>q-:" + userId, questionId.ToString()));
    });
}

public void VoteQuestionDown(long userId, long questionId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register downvote against question and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:q>user-:" + questionId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:q>user+:" + questionId, userId.ToString()));

        //Register downvote against user and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet"urn:user>q-:" + userId, questionId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>q+:" + userId, questionId.ToString()));
    });
}

public void VoteAnswerUp(long userId, long answerId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register upvote against answer and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:a>user+:" + answerId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:a>user-:" + answerId, userId.ToString()));

        //Register upvote against user and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:user>a+:" + userId, answerId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>a-:" + userId, answerId.ToString()));
    });
}

public void VoteAnswerDown(long userId, long answerId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register downvote against answer and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:a>user-:" + answerId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:a>user+:" + answerId, userId.ToString()));

        //Register downvote against user and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:user>a-:" + userId, answerId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>a+:" + userId, answerId.ToString()));
    });
}

public QuestionResult GetQuestion(long questionId)
{
    var question = RedisManager.ExecAs<question>
    (redisQuestions => redisQuestions.GetById(questionId));
    if (question == null) return null;

    var result = ToQuestionResults(new[] { question })[0];
    var answers = GetAnswersForQuestion(questionId);
    var uniqueUserIds = answers.ConvertAll(x => x.UserId).ToHashSet();
    var usersMap = GetUsersByIds(uniqueUserIds).ToDictionary(x => x.Id);

    result.Answers = answers.ConvertAll(answer =>
        new AnswerResult { Answer = answer, User = usersMap[answer.UserId] });

    return result;
}

public List<user> GetUsersByIds(IEnumerable<long> userIds)
{
    return RedisManager.ExecAs<user>(redisUsers => redisUsers.GetByIds(userIds)).ToList();
}

public QuestionStat GetQuestionStats(long questionId)
{
    using (var redis = RedisManager.GetReadOnlyClient())
    {
        var result = new QuestionStat
        {
            VotesUpCount = redis.GetSetCount("urn:q>user+:" +questionId),
            VotesDownCount = redis.GetSetCount("urn:q>user-:" + questionId)
        };
        result.VotesTotal = result.VotesUpCount - result.VotesDownCount;
        return result;
    }
}

public List<tag> GetTagsByPopularity(int skip, int take)
{
    using (var redis = RedisManager.GetReadOnlyClient())
    {
        var tagEntries = redis.GetRangeWithScoresFromSortedSetDesc("urn:tags", skip, take);
        var tags = tagEntries.ConvertAll(kvp => new Tag { Name = kvp.Key, Score = (int)kvp.Value });
        return tags;
    }
}

public SiteStats GetSiteStats()
{
    using (var redis = RedisManager.GetClient())
    {
        return new SiteStats
        {
            QuestionsCount = redis.As<question>().TypeIdsSet.Count,
            AnswersCount = redis.As<answer>().TypeIdsSet.Count,
            TopTags = GetTagsByPopularity(0, 10)
        };
    }
}

附加資源說明

專案中引用的一些包在packages.config檔案中配置。

Funq IoC的相關配置,以及註冊型別和當前控制器目錄,在Global.asax檔案中配置。

基於IoC的快取使用以及Global.asax可以開啟以下URL:http://localhost:37447/Question/GetQuestions?tag=test 檢視。<wbr><wbr><wbr><wbr>

你可以將tag欄位設定成test3,test1,test2等。

Redis快取配置——在web config檔案(<system.web><sessionState>節點)以及RedisSessionStateProvider.cs檔案中。

在MVC專案中有很多待辦事項,因此,如果你想改進/繼續,請更新,並上傳。

如果有人能提供使用Redis(以及Funq IOC)快取的MVC應用程式示例,本人將不勝感激。Funq IOC已經配置,使用示例已經在Question controller中。

注:部分取樣於“ServiceStack.Examples-master”解決方案。

結論。優化應用程式快取以及快速本地快取

由於Redis並不在本地儲存(也不在本地複製)資料,那麼通過在本地快取區儲存一些輕量級或使用者依賴的物件(跳過序列化字串和客戶端—服務端資料轉換)來優化效能是有意義的。例如,在Web應用中,對於輕量級的物件使用’System.Runtime.Caching.ObjectCache‘ 會更好——使用者依賴,並且應用程式時常要用。否則,當經常性地需要使用該物件時,就必須在分散式Redis快取中儲存大量容積的內容。使用者依賴的物件舉例——個人資料資訊,個性化資訊 。常用物件——本地化資料,不同使用者之間的共享資訊,等等。

下載原始碼(Redis Funq LoC MVC 4版本)

連結

如何執行Redis服務:

https://github.com/kcherenkov/redis-windows-service<wbr><wbr><wbr><wbr>

文件:

http://redis.io/documentation<wbr><wbr>

.NET / C#示例:

https://github.com/ServiceStack/ServiceStack.Examples<wbr><wbr><wbr><wbr>

關於如何用C#在Windows上使用Redis的好建議:

http://maxivak.com/getting-started-with-redis-and-asp-net-mvc-under-windows/:<wbr><wbr><wbr><wbr><wbr><wbr>

http://www.piotrwalat.net/using-redis-with-asp-net-web-api/<wbr><wbr><wbr><wbr>

關於Redis:

https://github.com/ServiceStack/ServiceStack.Redis<wbr><wbr><wbr><wbr>

Azure快取

http://kotugoroshko.blogspot.ae/2013/07/windows-azure-caching-integration.html<wbr><wbr><wbr><wbr><wbr><wbr>

許可證

這篇文章,以及任何相關的原始碼和檔案,依據The Code Project Open License (CPOL)。

譯文連結:http://www.codeceo.com/article/distributed-caching-redis-server.html
英文原文:Distributed Caching using Redis Server with .NET/C# Client
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章