.NET Core中使用Redis与Memcached的序列化问题详析
前言
在使用分布式缓存的时候,都不可避免的要做这样一步操作,将数据序列化后再存储到缓存中去。
序列化这一操作,或许是显式的,或许是隐式的,这个取决于使用的package是否有帮我们做这样一件事。
本文会拿在.net core环境下使用redis和memcached来当例子说明,其中,redis主要是用stackexchange.redis
,memcached主要是用enyimmemcachedcore。
先来看看一些我们常用的序列化方法。
常见的序列化方法
或许,比较常见的做法就是将一个对象序列化成byte数组,然后用这个数组和缓存服务器进行交互。
关于序列化,业界有不少算法,这些算法在某种意义上表现的结果就是速度和体积这两个问题。
其实当操作分布式缓存的时候,我们对这两个问题其实也是比较看重的!
在同等条件下,序列化和反序列化的速度,可以决定执行的速度是否能快一点。
序列化的结果,也就是我们要往内存里面塞的东西,如果能让其小一点,也是能节省不少宝贵的内存空间。
当然,本文的重点不是去比较那种序列化方法比较牛逼,而是介绍怎么结合缓存去使用,也顺带提一下在使用缓存时,序列化可以考虑的一些点。
下面来看看一些常用的序列化的库:
在这些库中
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
相比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这个接口相应的方法即可。
redis
在介绍怎么用序列化之前,先来简单看看是怎么用的(用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(); }
memcached
同样先来简单看看是怎么用的(用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(); } }
在控制器中使用时和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
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: Java中的RASP机制实现详解
下一篇: vue-router实现嵌套路由的讲解