使用 .NET/C# 客户端通过 Redis 服务器实现分布式缓存






4.83/5 (69投票s)
在 Windows 机器上设置 Redis 服务器,并使用 C# 客户端访问它
引言
在本文中,我想描述我以最紧凑的方式安装和配置 Redis 服务器的经验。此外,我还想简要概述 .NET/C# 客户端中使用 Redis 哈希和列表。
本文内容
- 安装 Redis 服务器 (完整的 Redis Windows 服务应用程序文件集已附带)
- 保护服务器 (配置认证)
- 配置服务器复制
- 从 C# 应用程序访问缓存
- 将 ASP.NET 会话状态与 Redis 一起使用
- Redis 集合、列表和事务的示例用法
- 附加源代码说明 (MVC 项目中的 Redis with Funq IoC : 示例)
- 缓存优化思路
背景
Redis 是最快、功能最丰富的内存键值数据存储之一。
缺点
- 无本地数据缓冲 (例如 Azure 缓存上的同步本地数据缓冲)
- 尚未完全支持集群 (预计今年年底)
优点
- 易于配置
- 使用简单
- 高性能
- 支持不同数据类型 (例如哈希、列表、集合、有序集合)
- ASP.NET 会话集成
- 查看缓存内容的 Web UI
在这个简单的演示中,我将演示如何在服务器上安装和配置 Redis,并在 C# 代码中使用它。
Redis 安装
从 https://github.com/dmajkic/redis/downloads 下载二进制文件 (win32 win64 直接链接),将存档解压到应用程序目录 (例如 C:\Program Files\Redis)
从 https://github.com/kcherenkov/redis-windows-service/downloads 下载编译好的 Redis 服务,然后复制到程序文件夹 (例如 C:\Program Files\Redis。如果缺少配置文件,请下载并将其复制到应用程序目录。有效的 Redis 配置文件示例如 https://raw.github.com/antirez/redis/2.6/redis.conf。
Redis 应用程序的完整文件集也可在 zip 文件 (x64) 中找到.
当您拥有完整的应用程序文件集时 (如下图所示),
导航到应用程序目录并运行以下命令
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。
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 服务器实例成为另一个服务器的从属服务器,请按以下方式更改配置文件
找到下面的行
# slaveof <masterip> <masterport>
替换为
slaveof 192.168.1.1 6379
(指定主服务器的实际 IP,以及端口,如果您自定义了的话)。如果主服务器配置为需要密码 (认证),请按如下方式修改 redis.conf,找到一行
# masterauth <master-password>
删除开头的 #
符号并将 <master-password> 替换为主密码,使其变为
masterauth mastpassword
现在此 Redis 实例可用作主服务器的只读同步副本。
从 C# 代码使用 Redis 缓存
要从 C# 使用 Redis,请运行 "管理 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
- 手机的所有者。每个手机实例都有一个指向所有者的引用。此代码演示了我们如何按条件添加、删除或查找缓存中的项目
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
类型。
ASP.NET 会话状态与 Redis
要配置 ASP.NET 会话状态 与 Redis 提供程序,请向您的 Web 项目添加一个新文件,名为 RedisSessionStateProvider.cs,从 https://github.com/chadman/redis-service-provider/raw/master/RedisProvider/SessionProvider/RedisSessionProvider.cs 复制代码,然后添加或更改配置文件中的以下部分 (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 控制器中
public class HomeController : Controller
{
public ActionResult Index()
{
//...
ViewBag.Message = Session["testRedisSession"];
return View();
}
//...
}
结果
ASP.NET 输出缓存提供程序与 Redis 的配置方式类似。
Redis 集合和列表
主要注意的是,Redis 列表 实现 IList<T>
,而 Redis 集合实现 ICollection<T>
。让我们看看如何使用它们。
列表主要用于需要区分同一类型对象的不同类别时。例如,我们有 "最畅销手机" 和 "旧收藏" 两个手机列表
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");
}
Redis 集合在需要存储相关数据集合并收集统计信息时很有用,例如答案 -> 问题,对答案或问题的投票。假设我们有问题和答案,需要将它们存储在缓存中以提高性能。使用 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 中 (属性依赖注入)
简单客户端的使用 - 在 home 控制器中
IoC 基于缓存的使用 在 Question 和 Answer 控制器,以及 Global.asax 应用程序文件中。要查看其工作原理,您可以运行项目,并在浏览器中打开以下 URL: https://:37447/Question/GetQuestions?tag=test 。
您可以尝试使用标签,例如 test3, test11, test2, 等等。
Redis 缓存配置 - 在 Web.config (<system.web><sessionState>
部分) 和 RedisSessionStateProvider.cs 文件中。
MVC 项目中有很多 TODO,所以如果您想改进/继续,请更新并上传。
我非常希望有人能帮助构建一个使用 Redis (带 Funq IoC) 缓存的简单 UI 的 MVC 应用程序。Funq IoC 已经配置好,Question 控制器中有一个使用示例。
注意: 示例部分内容取自 "ServiceStack.Examples-master" 解决方案
结论。使用快速本地缓存优化应用程序中的缓存
由于 Redis 不本地存储数据 (无本地复制),因此通过将某些轻量级或用户相关的对象存储在本地缓存中 (以跳过序列化为字符串和客户端-服务器数据传输) 来优化性能可能是有意义的。例如,在 Web 应用程序中,最好使用 'System.Runtime.Caching.ObjectCache
' 来处理轻量级对象,这些对象与用户相关且应用程序频繁使用。否则,当对象被通用使用且体积较大时,它必须保存在分布式 Redis 缓存中。用户相关对象的示例 - 配置文件信息、个性化信息。通用对象 - 本地化数据、不同用户之间共享的信息等。
链接
- 如何运行 Redis 服务说明
https://github.com/kcherenkov/redis-windows-service - 文档
https://redis.ac.cn/documentation - .NET/C# 示例
https://github.com/ServiceStack/ServiceStack.Examples - 建议阅读关于在 Windows 上使用 C# 入门 Redis 的优秀文章
http://maxivak.com/getting-started-with-redis-and-asp-net-mvc-under-windows/
http://www.piotrwalat.net/using-redis-with-asp-net-web-api/ - Redis 介绍
https://github.com/ServiceStack/ServiceStack.Redis - Azure 缓存
http://kotugoroshko.blogspot.ae/2013/07/windows-azure-caching-integration.html