.NET Core中使用Redis與Memcached的序列化問(wèn)題詳析
前言
在使用分布式緩存的時(shí)候,都不可避免的要做這樣一步操作,將數(shù)據(jù)序列化后再存儲(chǔ)到緩存中去。
序列化這一操作,或許是顯式的,或許是隱式的,這個(gè)取決于使用的package是否有幫我們做這樣一件事。
本文會(huì)拿在.NET Core環(huán)境下使用Redis和Memcached來(lái)當(dāng)例子說(shuō)明,其中,Redis主要是用StackExchange.Redis,Memcached主要是用EnyimMemcachedCore。
先來(lái)看看一些我們常用的序列化方法。
常見(jiàn)的序列化方法
或許,比較常見(jiàn)的做法就是將一個(gè)對(duì)象序列化成byte數(shù)組,然后用這個(gè)數(shù)組和緩存服務(wù)器進(jìn)行交互。
關(guān)于序列化,業(yè)界有不少算法,這些算法在某種意義上表現(xiàn)的結(jié)果就是速度和體積這兩個(gè)問(wèn)題。
其實(shí)當(dāng)操作分布式緩存的時(shí)候,我們對(duì)這兩個(gè)問(wèn)題其實(shí)也是比較看重的!
在同等條件下,序列化和反序列化的速度,可以決定執(zhí)行的速度是否能快一點(diǎn)。
序列化的結(jié)果,也就是我們要往內(nèi)存里面塞的東西,如果能讓其小一點(diǎn),也是能節(jié)省不少寶貴的內(nèi)存空間。
當(dāng)然,本文的重點(diǎn)不是去比較那種序列化方法比較牛逼,而是介紹怎么結(jié)合緩存去使用,也順帶提一下在使用緩存時(shí),序列化可以考慮的一些點(diǎn)。
下面來(lái)看看一些常用的序列化的庫(kù):
在這些庫(kù)中
System.Runtime.Serialization.Formatters.Binary是.NET類(lèi)庫(kù)中本身就有的,所以想在不依賴(lài)第三方的packages時(shí),這是個(gè)不錯(cuò)的選擇。
Newtonsoft.Json應(yīng)該不用多說(shuō)了。
protobuf-net是.NET實(shí)現(xiàn)的Protocol Buffers。
MessagePack-CSharp是極快的MessagePack序列化工具。
這幾種序列化的庫(kù)也是筆者平時(shí)有所涉及的,還有一些不熟悉的就沒(méi)列出來(lái)了!
在開(kāi)始之前,我們先定義一個(gè)產(chǎn)品類(lèi),后面相關(guān)的操作都是基于這個(gè)類(lèi)來(lái)說(shuō)明。
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
下面先來(lái)看看Redis的使用。
Redis
在介紹序列化之前,我們需要知道在StackExchange.Redis中,我們要存儲(chǔ)的數(shù)據(jù)都是以RedisValue的形式存在的。并且RedisValue是支持string,byte[]等多種數(shù)據(jù)類(lèi)型的。
換句話(huà)說(shuō)就是,在我們使用StackExchange.Redis時(shí),存進(jìn)Redis的數(shù)據(jù)需要序列化成RedisValue所支持的類(lèi)型。
這就是前面說(shuō)的需要顯式的進(jìn)行序列化的操作。
先來(lái)看看.NET類(lèi)庫(kù)提供的BinaryFormatter。
序列化的操作
using (var ms = new MemoryStream())
{
formatter.Serialize(ms, product);
db.StringSet("binaryformatter", ms.ToArray(), TimeSpan.FromMinutes(1));
}
反序列化的操作
var value = db.StringGet("binaryformatter");
using (var ms = new MemoryStream(value))
{
var desValue = (Product)(new BinaryFormatter().Deserialize(ms));
Console.WriteLine($"{desValue.Id}-{desValue.Name}");
}
寫(xiě)起來(lái)還是挺簡(jiǎn)單的,但是這個(gè)時(shí)候運(yùn)行代碼會(huì)提示下面的錯(cuò)誤!

說(shuō)是我們的Product類(lèi)沒(méi)有標(biāo)記Serializable。下面就是在Product類(lèi)加上[Serializable]。

再次運(yùn)行,已經(jīng)能成功了。

再來(lái)看看Newtonsoft.Json
序列化的操作
using (var ms = new MemoryStream())
{
using (var sr = new StreamWriter(ms, Encoding.UTF8))
using (var jtr = new JsonTextWriter(sr))
{
jsonSerializer.Serialize(jtr, product);
}
db.StringSet("json", ms.ToArray(), TimeSpan.FromMinutes(1));
}
反序列化的操作
var bytes = db.StringGet("json");
using (var ms = new MemoryStream(bytes))
using (var sr = new StreamReader(ms, Encoding.UTF8))
using (var jtr = new JsonTextReader(sr))
{
var desValue = jsonSerializer.Deserialize<Product>(jtr);
Console.WriteLine($"{desValue.Id}-{desValue.Name}");
}
由于Newtonsoft.Json對(duì)我們要進(jìn)行序列化的類(lèi)有沒(méi)有加上Serializable并沒(méi)有什么強(qiáng)制性的要求,所以去掉或保留都可以。
運(yùn)行起來(lái)是比較順利的。

當(dāng)然,也可以用下面的方式來(lái)處理的:
var objStr = JsonConvert.SerializeObject(product);
db.StringSet("json", Encoding.UTF8.GetBytes(objStr), TimeSpan.FromMinutes(1));
var resStr = Encoding.UTF8.GetString(db.StringGet("json"));
var res = JsonConvert.DeserializeObject<Product>(resStr);
再來(lái)看看ProtoBuf
序列化的操作
using (var ms = new MemoryStream())
{
Serializer.Serialize(ms, product);
db.StringSet("protobuf", ms.ToArray(), TimeSpan.FromMinutes(1));
}
反序列化的操作
var value = db.StringGet("protobuf");
using (var ms = new MemoryStream(value))
{
var desValue = Serializer.Deserialize<Product>(ms);
Console.WriteLine($"{desValue.Id}-{desValue.Name}");
}
用法看起來(lái)也是中規(guī)中矩。
但是想這樣就跑起來(lái)是沒(méi)那么順利的。錯(cuò)誤提示如下:

處理方法有兩個(gè),一個(gè)是在Product類(lèi)和屬性上面加上對(duì)應(yīng)的Attribute,另一個(gè)是用ProtoBuf.Meta在運(yùn)行時(shí)來(lái)處理這個(gè)問(wèn)題??梢詤⒖?a rel="external nofollow" target="_blank" >AutoProtobuf的實(shí)現(xiàn)。
下面用第一種方式來(lái)處理,直接加上[ProtoContract]和[ProtoMember]這兩個(gè)Attribute。

再次運(yùn)行就是我們所期望的結(jié)果了。

最后來(lái)看看MessagePack,據(jù)其在Github上的說(shuō)明和對(duì)比,似乎比其他序列化的庫(kù)都強(qiáng)悍不少。
它默認(rèn)也是要像Protobuf那樣加上MessagePackObject和Key這兩個(gè)Attribute的。
不過(guò)它也提供了一個(gè)IFormatterResolver參數(shù),可以讓我們有所選擇。
下面用的是不需要加Attribute的方法來(lái)演示。
序列化的操作
var serValue = MessagePackSerializer.Serialize(product, ContractlessStandardResolver.Instance);
db.StringSet("messagepack", serValue, TimeSpan.FromMinutes(1));
反序列化的操作
var value = db.StringGet("messagepack");
var desValue = MessagePackSerializer.Deserialize<Product>(value, ContractlessStandardResolver.Instance);
此時(shí)運(yùn)行起來(lái)也是正常的。

其實(shí)序列化這一步,對(duì)Redis來(lái)說(shuō)是十分簡(jiǎn)單的,因?yàn)樗@式的讓我們?nèi)ヌ幚恚缓蟀呀Y(jié)果進(jìn)行存儲(chǔ)。
上面演示的4種方法,從使用上看,似乎都差不多,沒(méi)有太大的區(qū)別。
如果拿Redis和Memcached對(duì)比,會(huì)發(fā)現(xiàn)Memcached的操作可能比Redis的略微復(fù)雜了一點(diǎn)。
下面來(lái)看看Memcached的使用。
Memcached
EnyimMemcachedCore默認(rèn)有一個(gè) DefaultTranscoder
,對(duì)于常規(guī)的數(shù)據(jù)類(lèi)型(int,string等)本文不細(xì)說(shuō),只是特別說(shuō)明object類(lèi)型。
在DefaultTranscoder中,對(duì)Object類(lèi)型的數(shù)據(jù)進(jìn)行序列化是基于Bson的。
還有一個(gè)BinaryFormatterTranscoder是屬于默認(rèn)的另一個(gè)實(shí)現(xiàn),這個(gè)就是基于我們前面的說(shuō).NET類(lèi)庫(kù)自帶的System.Runtime.Serialization.Formatters.Binary。
先來(lái)看看這兩種自帶的Transcoder要怎么用。
先定義好初始化Memcached相關(guān)的方法,以及讀寫(xiě)緩存的方法。
初始化Memcached如下:
private static void InitMemcached(string transcoder = "")
{
IServiceCollection services = new ServiceCollection();
services.AddEnyimMemcached(options =>
{
options.AddServer("127.0.0.1", 11211);
options.Transcoder = transcoder;
});
services.AddLogging();
IServiceProvider serviceProvider = services.BuildServiceProvider();
_client = serviceProvider.GetService<IMemcachedClient>() as MemcachedClient;
}
這里的transcoder就是我們要選擇那種序列化方法(針對(duì)object類(lèi)型),如果是空就用Bson,如果是BinaryFormatterTranscoder用的就是BinaryFormatter。
需要注意下面兩個(gè)說(shuō)明
- 2.1.0版本之后,Transcoder由ITranscoder類(lèi)型變更為string類(lèi)型。
- 2.1.0.5版本之后,可以通過(guò)依賴(lài)注入的形式來(lái)完成,而不用指定string類(lèi)型的Transcoder。
讀寫(xiě)緩存的操作如下:
private static void MemcachedTrancode(Product product)
{
_client.Store(Enyim.Caching.Memcached.StoreMode.Set, "defalut", product, DateTime.Now.AddMinutes(1));
Console.WriteLine("serialize succeed!");
var desValue = _client.ExecuteGet<Product>("defalut").Value;
Console.WriteLine($"{desValue.Id}-{desValue.Name}");
Console.WriteLine("deserialize succeed!");
}
我們?cè)贛ain方法中的代碼如下 :
static void Main(string[] args)
{
Product product = new Product
{
Id = 999,
Name = "Product999"
};
//Bson
string transcoder = "";
//BinaryFormatter
//string transcoder = "BinaryFormatterTranscoder";
InitMemcached(transcoder);
MemcachedTrancode(product);
Console.ReadKey();
}
對(duì)于自帶的兩種Transcoder,跑起來(lái)還是比較順利的,在用BinaryFormatterTranscoder時(shí)記得給Product類(lèi)加上[Serializable]就好!
下面來(lái)看看如何借助MessagePack來(lái)實(shí)現(xiàn)Memcached的Transcoder。
這里繼承DefaultTranscoder就可以了,然后重寫(xiě)SerializeObject,DeserializeObject和Deserialize
public class MessagePackTranscoder : DefaultTranscoder
{
protected override ArraySegment<byte> SerializeObject(object value)
{
return MessagePackSerializer.SerializeUnsafe(value, TypelessContractlessStandardResolver.Instance);
}
public override T Deserialize<T>(CacheItem item)
{
return (T)base.Deserialize(item);
}
protected override object DeserializeObject(ArraySegment<byte> value)
{
return MessagePackSerializer.Deserialize<object>(value, TypelessContractlessStandardResolver.Instance);
}
}
慶幸的是,MessagePack有方法可以讓我們直接把一個(gè)object序列化成ArraySegment
相比Json和Protobuf,省去了不少操作!!
這個(gè)時(shí)候,我們有兩種方式來(lái)使用這個(gè)新定義的MessagePackTranscoder。
方式一 :在使用的時(shí)候,我們只需要替換前面定義的transcoder變量即可(適用>=2.1.0版本)。
string transcoder = "CachingSerializer.MessagePackTranscoder,CachingSerializer";
注:如果使用方式一來(lái)處理,記得將transcoder的拼寫(xiě)不要錯(cuò),并且要帶上命名空間,不然創(chuàng)建的Transcoder會(huì)一直是null,從而走的就是Bson了! 本質(zhì)是 Activator.CreateInstance,應(yīng)該不用多解釋。
方式二:通過(guò)依賴(lài)注入的方式來(lái)處理(適用>=2.1.0.5版本)
private static void InitMemcached(string transcoder = "")
{
IServiceCollection services = new ServiceCollection();
services.AddEnyimMemcached(options =>
{
options.AddServer("127.0.0.1", 11211);
//這里保持空字符串或不賦值,就會(huì)走下面的AddSingleton
//如果這里賦了正確的值,后面的AddSingleton就不會(huì)起作用了
options.Transcoder = transcoder;
});
//使用新定義的MessagePackTranscoder
services.AddSingleton<ITranscoder, MessagePackTranscoder>();
//others...
}
運(yùn)行之前加個(gè)斷點(diǎn),確保真的進(jìn)了我們重寫(xiě)的方法中。

最后的結(jié)果:

Protobuf和Json的,在這里就不一一介紹了,這兩個(gè)處理起來(lái)比MessagePack復(fù)雜了不少??梢詤⒖?a rel="external nofollow" target="_blank" >MemcachedTranscoder這個(gè)開(kāi)源項(xiàng)目,也是MessagePack作者寫(xiě)的,雖然是5年前的,但是一樣的好用。
對(duì)于Redis來(lái)說(shuō),在調(diào)用Set方法時(shí)要顯式的將我們的值先進(jìn)行序列化,不那么簡(jiǎn)潔,所以都會(huì)進(jìn)行一次封裝在使用。
對(duì)于Memcached來(lái)說(shuō),在調(diào)用Set方法的時(shí)候雖然不需要顯式的進(jìn)行序列化,但是有可能要我們自己去實(shí)現(xiàn)一個(gè)Transcoder,這也是有點(diǎn)麻煩的。
下面給大家推薦一個(gè)簡(jiǎn)單的緩存庫(kù)來(lái)處理這些問(wèn)題。
使用EasyCaching來(lái)簡(jiǎn)化操作
EasyCaching是筆者在業(yè)余時(shí)間寫(xiě)的一個(gè)簡(jiǎn)單的開(kāi)源項(xiàng)目,主要目的是想簡(jiǎn)化緩存的操作,目前也在不斷的完善中。
EasyCaching提供了前面所說(shuō)的4種序列化方法可供選擇:
- BinaryFormatter
- MessagePack
- Json
- ProtoBuf
如果這4種都不滿(mǎn)足需求,也可以自己寫(xiě)一個(gè),只要實(shí)現(xiàn)IEasyCachingSerializer這個(gè)接口相應(yīng)的方法即可。
Redis
在介紹怎么用序列化之前,先來(lái)簡(jiǎn)單看看是怎么用的(用ASP.NET Core Web API做演示)。
添加Redis相關(guān)的nuget包
Install-Package EasyCaching.Redis
修改Startup
public class Startup
{
//...
public void ConfigureServices(IServiceCollection services)
{
//other services.
//Important step for Redis Caching
services.AddDefaultRedisCache(option=>
{
option.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
option.Password = "";
});
}
}
然后在控制器中使用:
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly IEasyCachingProvider _provider;
public ValuesController(IEasyCachingProvider provider)
{
this._provider = provider;
}
[HttpGet]
public string Get()
{
//Set
_provider.Set("demo", "123", TimeSpan.FromMinutes(1));
//Get without data retriever
var res = _provider.Get<string>("demo");
_provider.Set("product:1", new Product { Id = 1, Name = "name"}, TimeSpan.FromMinutes(1))
var product = _provider.Get<Product>("product:1");
return $"{res.Value}-{product.Value.Id}-{product.Value.Name}";
}
}
- 使用的時(shí)候,在構(gòu)造函數(shù)對(duì)IEasyCachingProvider進(jìn)行依賴(lài)注入即可。
- Redis默認(rèn)用了BinaryFormatter來(lái)進(jìn)行序列化。
下面我們要如何去替換我們想要的新的序列化方法呢?
以MessagePack為例,先通過(guò)nuget安裝package
Install-Package EasyCaching.Serialization.MessagePack
然后只需要在ConfigureServices方法中加上下面這句就可以了。
public void ConfigureServices(IServiceCollection services)
{
//others..
services.AddDefaultMessagePackSerializer();
}
Memcached
同樣先來(lái)簡(jiǎn)單看看是怎么用的(用ASP.NET Core Web API做演示)。
添加Memcached的nuget包
Install-Package EasyCaching.Memcached
修改Startup
public class Startup
{
//...
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//Important step for Memcached Cache
services.AddDefaultMemcached(option=>
{
option.AddServer("127.0.0.1",11211);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//Important step for Memcache Cache
app.UseDefaultMemcached();
}
}
在控制器中使用時(shí)和Redis是一模一樣的。
這里需要注意的是,在EasyCaching中,默認(rèn)使用的序列化方法并不是DefaultTranscoder中的Bson,而是BinaryFormatter
如何去替換默認(rèn)的序列化操作呢?
同樣以MessagePack為例,先通過(guò)nuget安裝package
Install-Package EasyCaching.Serialization.MessagePack
剩下的操作和Redis是一樣的!
public void ConfigureServices(IServiceCollection services)
{
//others..
services.AddDefaultMemcached(op=>
{
op.AddServer("127.0.0.1",11211);
});
//specify the Transcoder use messagepack serializer.
services.AddDefaultMessagePackSerializer();
}
因?yàn)樵贓asyCaching中,有一個(gè)自己的Transcoder,這個(gè)Transcoder對(duì)IEasyCachingSerializer進(jìn)行注入,所以只需要指定對(duì)應(yīng)的Serializer即可。
總結(jié)
一、 先來(lái)看看文中提到的4種序列化的庫(kù)
System.Runtime.Serialization.Formatters.Binary在使用上需要加上[Serializable],效率是最慢的,優(yōu)勢(shì)就是類(lèi)庫(kù)里面就有,不需要額外引用其他package。
Newtonsoft.Json使用起來(lái)比較友善,可能是用的多的緣故,也不需要我們對(duì)已經(jīng)定義好的類(lèi)加一些Attribute上去。
protobuf-net使用起來(lái)可能就略微麻煩一點(diǎn),可以在定義類(lèi)的時(shí)候加上相應(yīng)的Attribute,也可以在運(yùn)行時(shí)去處理(要注意處理子類(lèi)),不過(guò)它的口碑還是不錯(cuò)的。
MessagePack-CSharp雖然可以不添加Attribute,但是不加比加的時(shí)候也會(huì)有所損耗。
至于如何選擇,可能就要視情況而定了!
有興趣的可以用BenchmarkDotNet跑跑分,我也簡(jiǎn)單寫(xiě)了一個(gè)可供參考:SerializerBenchmark
二、在對(duì)緩存操作的時(shí)候,可能會(huì)更傾向于“隱式”操作,能直接將一個(gè)object扔進(jìn)去,也可以直接將一個(gè)object拿出來(lái),至少能方便使用方。
三、序列化操作時(shí),Redis要比Memcached簡(jiǎn)單一些。
最后,如果您在使用EasyCaching,有問(wèn)題或建議可以聯(lián)系我!
前半部分的示例代碼:CachingSerializer
后半部分的示例代碼:sample
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- .NET?Core使用Redis實(shí)現(xiàn)創(chuàng)建分布式鎖
- .net core 3.1 Redis安裝和簡(jiǎn)單使用
- 在.NET?Core中使用CSRedis的詳細(xì)過(guò)程
- ASP.NET?Core中使用Redis實(shí)現(xiàn)緩存
- .net core如何使用Redis發(fā)布訂閱
- .net core使用redis基于StackExchange.Redis
- asp.net性能優(yōu)化之使用Redis緩存(入門(mén))
- 詳解.NET中使用Redis數(shù)據(jù)庫(kù)
- 詳解Asp.net Core 使用Redis存儲(chǔ)Session
- 詳解如何在ASP.NET Core中使用Redis
- .net web優(yōu)雅地使用 redis的方法步驟
相關(guān)文章
asp.net下實(shí)現(xiàn)URL重寫(xiě)技術(shù)的代碼
asp.net下實(shí)現(xiàn)URL重寫(xiě)技術(shù)的代碼...2007-10-10
asp.net 擴(kuò)展GridView 增加單選按鈕列的代碼
asp.net 擴(kuò)展GridView 增加單選按鈕列的代碼2010-02-02
Asp.net配合easyui實(shí)現(xiàn)返回json數(shù)據(jù)實(shí)例
這篇文章主要介紹了Asp.net配合easyui實(shí)現(xiàn)返回json數(shù)據(jù)的方法,實(shí)例分析了Asp.net配合easyui返回json數(shù)據(jù)時(shí)出現(xiàn)的問(wèn)題及解決方法,非常具有實(shí)用價(jià)值的技巧,需要的朋友可以參考下2014-12-12
.NET Core利用動(dòng)態(tài)代理實(shí)現(xiàn)AOP(面向切面編程)
用動(dòng)態(tài)代理可以做AOP(面向切面編程),進(jìn)行無(wú)入侵式實(shí)現(xiàn)自己的擴(kuò)展業(yè)務(wù),調(diào)用者和被調(diào)用者之間的解耦,提高代碼的靈活性和可擴(kuò)展性。本文將為大家詳細(xì)介紹實(shí)現(xiàn)的方法,感興趣的可以學(xué)習(xí)一下2022-01-01
.NET中求復(fù)雜類(lèi)型集合的差集、交集、并集
這篇文章主要介紹了.NET的求復(fù)雜類(lèi)型集合的差集、交集、并集,下面我將帶著大家去了解如何通過(guò)微軟自帶方法方式去取**復(fù)雜類(lèi)型集合**的差集、交集、并集,需要的朋友可以參考下2022-07-07
.Net Core構(gòu)建Webservice服務(wù)的實(shí)現(xiàn)步驟
本文主要介紹了.NET Core項(xiàng)目中創(chuàng)建和配置一個(gè)簡(jiǎn)單的WebService(ASMX),包括添加必要的NuGet包、定義服務(wù)接口和實(shí)現(xiàn)類(lèi)、配置終結(jié)點(diǎn)以及處理不同版本的SOAP協(xié)議,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2026-01-01
vs2012創(chuàng)建的ado.net模型無(wú)法實(shí)例化的解決方案
本文給大家分享的是升級(jí)vs2012后,發(fā)現(xiàn)創(chuàng)建數(shù)據(jù)模型無(wú)法實(shí)例化使用,嘗試了很多種方法,最后在度娘的幫助下,才解決了這個(gè)問(wèn)題,這里記錄下來(lái),分享給大家。2015-03-03
ASP.NET小結(jié)之MVC, MVP, MVVM比較以及區(qū)別(一)
MVC, MVP和MVVM都是用來(lái)解決界面呈現(xiàn)和邏輯代碼分離而出現(xiàn)的模式。以前只是對(duì)它們有部分的了解,沒(méi)有深入的研究過(guò),對(duì)于一些里面的概念和區(qū)別也是一知半解。現(xiàn)在一邊查資料,并結(jié)合自己的理解,來(lái)談一下對(duì)于這三種模式思想的理解,以及它們的區(qū)別。歡迎各位高手拍磚。2014-05-05

