前言
在使用分布式緩存的時候,都不可避免的要做這樣一步操作,将資料序列化後再存儲到緩存中去。
序列化這一操作,或許是顯式的,或許是隐式的,這個取決于使用的package是否有幫我們做這樣一件事。
本文會拿在.NET Core環境下使用Redis和Memcached來當例子說明,其中,Redis主要是用StackExchange.Redis,Memcached主要是用EnyimMemcachedCore。
先來看看一些我們常用的序列化方法。
常見的序列化方法
或許,比較常見的做法就是将一個對象序列化成byte數組,然後用這個數組和緩存伺服器進行互動。
關于序列化,業界有不少算法,這些算法在某種意義上表現的結果就是速度和體積這兩個問題。
其實當操作分布式緩存的時候,我們對這兩個問題其實也是比較看重的!
在同等條件下,序列化和反序列化的速度,可以決定執行的速度是否能快一點。
序列化的結果,也就是我們要往記憶體裡面塞的東西,如果能讓其小一點,也是能節省不少寶貴的記憶體空間。
當然,本文的重點不是去比較那種序列化方法比較牛逼,而是介紹怎麼結合緩存去使用,也順帶提一下在使用緩存時,序列化可以考慮的一些點。
下面來看看一些常用的序列化的庫:
- System.Runtime.Serialization.Formatters.Binary
- Newtonsoft.Json
- protobuf-net
- MessagePack-CSharp
- ....
在這些庫中
System.Runtime.Serialization.Formatters.Binary是.NET類庫中本身就有的,是以想在不依賴第三方的packages時,這是個不錯的選擇。
Newtonsoft.Json應該不用多說了。
protobuf-net是.NET實作的Protocol Buffers。
MessagePack-CSharp是極快的MessagePack序列化工具。
這幾種序列化的庫也是筆者平時有所涉及的,還有一些不熟悉的就沒列出來了!
在開始之前,我們先定義一個産品類,後面相關的操作都是基于這個類來說明。
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
下面先來看看Redis的使用。
Redis
在介紹序列化之前,我們需要知道在StackExchange.Redis中,我們要存儲的資料都是以RedisValue的形式存在的。并且RedisValue是支援string,byte[]等多種資料類型的。
換句話說就是,在我們使用StackExchange.Redis時,存進Redis的資料需要序列化成RedisValue所支援的類型。
這就是前面說的需要顯式的進行序列化的操作。
先來看看.NET類庫提供的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}");
}
寫起來還是挺簡單的,但是這個時候運作代碼會提示下面的錯誤!

說是我們的Product類沒有标記Serializable。下面就是在Product類加上
[Serializable]
。
再次運作,已經能成功了。
再來看看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對我們要進行序列化的類有沒有加上Serializable并沒有什麼強制性的要求,是以去掉或保留都可以。
運作起來是比較順利的。
當然,也可以用下面的方式來處理的:
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);
再來看看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}");
}
用法看起來也是中規中矩。
但是想這樣就跑起來是沒那麼順利的。錯誤提示如下:
處理方法有兩個,一個是在Product類和屬性上面加上對應的Attribute,另一個是用ProtoBuf.Meta在運作時來處理這個問題。可以參考AutoProtobuf的實作。
下面用第一種方式來處理,直接加上
[ProtoContract]
和
[ProtoMember]
這兩個Attribute。
再次運作就是我們所期望的結果了。
最後來看看MessagePack,據其在Github上的說明和對比,似乎比其他序列化的庫都強悍不少。
它預設也是要像Protobuf那樣加上
MessagePackObject
Key
這兩個Attribute的。
不過它也提供了一個IFormatterResolver參數,可以讓我們有所選擇。
下面用的是不需要加Attribute的方法來示範。
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);
此時運作起來也是正常的。
其實序列化這一步,對Redis來說是十分簡單的,因為它顯式的讓我們去處理,然後把結果進行存儲。
上面示範的4種方法,從使用上看,似乎都差不多,沒有太大的差別。
如果拿Redis和Memcached對比,會發現Memcached的操作可能比Redis的略微複雜了一點。
下面來看看Memcached的使用。
Memcached
EnyimMemcachedCore預設有一個 DefaultTranscoder
,對于正常的資料類型(int,string等)本文不細說,隻是特别說明object類型。
在DefaultTranscoder中,對Object類型的資料進行序列化是基于Bson的。
還有一個BinaryFormatterTranscoder是屬于預設的另一個實作,這個就是基于我們前面的說.NET類庫自帶的System.Runtime.Serialization.Formatters.Binary。
先來看看這兩種自帶的Transcoder要怎麼用。
先定義好初始化Memcached相關的方法,以及讀寫緩存的方法。
初始化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就是我們要選擇那種序列化方法(針對object類型),如果是空就用Bson,如果是BinaryFormatterTranscoder用的就是BinaryFormatter。
需要注意下面兩個說明
- 2.1.0版本之後,Transcoder由ITranscoder類型變更為string類型。
- 2.1.0.5版本之後,可以通過依賴注入的形式來完成,而不用指定string類型的Transcoder。
讀寫緩存的操作如下:
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!");
}
我們在Main方法中的代碼如下 :
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();
}
對于自帶的兩種Transcoder,跑起來還是比較順利的,在用BinaryFormatterTranscoder時記得給Product類加上
[Serializable]
就好!
下面來看看如何借助MessagePack來實作Memcached的Transcoder。
這裡繼承DefaultTranscoder就可以了,然後重寫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有方法可以讓我們直接把一個object序列化成ArraySegment,也可以把ArraySegment 反序列化成一個object!!
相比Json和Protobuf,省去了不少操作!!
這個時候,我們有兩種方式來使用這個新定義的MessagePackTranscoder。
方式一 :在使用的時候,我們隻需要替換前面定義的transcoder變量即可(适用>=2.1.0版本)。
string transcoder = "CachingSerializer.MessagePackTranscoder,CachingSerializer";
注:如果使用方式一來處理,記得将transcoder的拼寫不要錯,并且要帶上命名空間,不然建立的Transcoder會一直是null,進而走的就是Bson了! 本質是 Activator.CreateInstance,應該不用多解釋。
方式二:通過依賴注入的方式來處理(适用>=2.1.0.5版本)
private static void InitMemcached(string transcoder = "")
{
IServiceCollection services = new ServiceCollection();
services.AddEnyimMemcached(options =>
{
options.AddServer("127.0.0.1", 11211);
//這裡保持空字元串或不指派,就會走下面的AddSingleton
//如果這裡賦了正确的值,後面的AddSingleton就不會起作用了
options.Transcoder = transcoder;
});
//使用新定義的MessagePackTranscoder
services.AddSingleton<ITranscoder, MessagePackTranscoder>();
//others...
}
運作之前加個斷點,確定真的進了我們重寫的方法中。
最後的結果:
Protobuf和Json的,在這裡就不一一介紹了,這兩個處理起來比MessagePack複雜了不少。可以參考MemcachedTranscoder這個開源項目,也是MessagePack作者寫的,雖然是5年前的,但是一樣的好用。
對于Redis來說,在調用Set方法時要顯式的将我們的值先進行序列化,不那麼簡潔,是以都會進行一次封裝在使用。
對于Memcached來說,在調用Set方法的時候雖然不需要顯式的進行序列化,但是有可能要我們自己去實作一個Transcoder,這也是有點麻煩的。
下面給大家推薦一個簡單的緩存庫來處理這些問題。
使用EasyCaching來簡化操作
EasyCaching是筆者在業餘時間寫的一個簡單的開源項目,主要目的是想簡化緩存的操作,目前也在不斷的完善中。
EasyCaching提供了前面所說的4種序列化方法可供選擇:
- BinaryFormatter
- MessagePack
- Json
- ProtoBuf
如果這4種都不滿足需求,也可以自己寫一個,隻要實作IEasyCachingSerializer這個接口相應的方法即可。
在介紹怎麼用序列化之前,先來簡單看看是怎麼用的(用ASP.NET Core Web API做示範)。
添加Redis相關的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}";
}
}
- 使用的時候,在構造函數對IEasyCachingProvider進行依賴注入即可。
- Redis預設用了BinaryFormatter來進行序列化。
下面我們要如何去替換我們想要的新的序列化方法呢?
以MessagePack為例,先通過nuget安裝package
Install-Package EasyCaching.Serialization.MessagePack
然後隻需要在ConfigureServices方法中加上下面這句就可以了。
public void ConfigureServices(IServiceCollection services)
{
//others..
services.AddDefaultMessagePackSerializer();
}
同樣先來簡單看看是怎麼用的(用ASP.NET Core Web API做示範)。
添加Memcached的nuget包
Install-Package EasyCaching.Memcached
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();
}
}
在控制器中使用時和Redis是一模一樣的。
這裡需要注意的是,在EasyCaching中,預設使用的序列化方法并不是DefaultTranscoder中的Bson,而是BinaryFormatter
如何去替換預設的序列化操作呢?
同樣以MessagePack為例,先通過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();
}
因為在EasyCaching中,有一個自己的Transcoder,這個Transcoder對IEasyCachingSerializer進行注入,是以隻需要指定對應的Serializer即可。
總結
一、 先來看看文中提到的4種序列化的庫
System.Runtime.Serialization.Formatters.Binary在使用上需要加上
[Serializable]
,效率是最慢的,優勢就是類庫裡面就有,不需要額外引用其他package。
Newtonsoft.Json使用起來比較友善,可能是用的多的緣故,也不需要我們對已經定義好的類加一些Attribute上去。
protobuf-net使用起來可能就略微麻煩一點,可以在定義類的時候加上相應的Attribute,也可以在運作時去處理(要注意處理子類),不過它的口碑還是不錯的。
MessagePack-CSharp雖然可以不添加Attribute,但是不加比加的時候也會有所損耗。
至于如何選擇,可能就要視情況而定了!
有興趣的可以用BenchmarkDotNet跑跑分,我也簡單寫了一個可供參考:SerializerBenchmark
二、在對緩存操作的時候,可能會更傾向于“隐式”操作,能直接将一個object扔進去,也可以直接将一個object拿出來,至少能友善使用方。
三、序列化操作時,Redis要比Memcached簡單一些。
最後,如果您在使用EasyCaching,有問題或建議可以聯系我!
前半部分的示例代碼:CachingSerializer
後半部分的示例代碼:sample