谈谈在.NET Core中使用Redis和Memcached的序列化问题

前言

在使用分布式缓存的时候,都不可避免的要做这样一步操作,将数据序列化后再存储到缓存中去。

序列化这一操作,或许是显式的,或许是隐式的,这个取决于使用的package是否有帮我们做这样一件事。

本文会拿在.NET Core环境下使用Redis和Memcached来当例子说明,其中,Redis主要是用StackExchange.Redis,Memcached主要是用EnyimMemcachedCore

先来看看一些我们常用的序列化方法。

常见的序列化方法

或许,比较常见的做法就是将一个对象序列化成byte数组,然后用这个数组和缓存服务器进行交互。

关于序列化,业界有不少算法,这些算法在某种意义上表现的结果就是速度体积这两个问题。

其实当操作分布式缓存的时候,我们对这两个问题其实也是比较看重的!

在同等条件下,序列化和反序列化的速度,可以决定执行的速度是否能快一点。

序列化的结果,也就是我们要往内存里面塞的东西,如果能让其小一点,也是能节省不少宝贵的内存空间。

当然,本文的重点不是去比较那种序列化方法比较牛逼,而是介绍怎么结合缓存去使用,也顺带提一下在使用缓存时,序列化可以考虑的一些点。

下面来看看一些常用的序列化的库:

  1. System.Runtime.Serialization.Formatters.Binary

  2. Newtonsoft.Json

  3. protobuf-net

  4. MessagePack-CSharp

  5. ....

在这些库中

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那样加上MessagePackObjectKey这两个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。

需要注意下面两个说明

  1. 2.1.0版本之后,Transcoder由ITranscoder类型变更为string类型。

  2. 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"};    //Bsonstring transcoder = "";    //BinaryFormatter//string transcoder = "BinaryFormatterTranscoder";            InitMemcached(transcoder);MemcachedTrancode(product);Console.ReadKey();
}

对于自带的两种Transcoder,跑起来还是比较顺利的,在用BinaryFormatterTranscoder时记得给Product类加上[Serializable]就好!

下面来看看如何借助MessagePack来实现Memcached的Transcoder。

这里继承DefaultTranscoder就可以了,然后重写SerializeObjectDeserializeObjectDeserialize这三个方法。

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;});    //使用新定义的MessagePackTranscoderservices.AddSingleton<ITranscoder, MessagePackTranscoder>();    //others...}

运行之前加个断点,确保真的进了我们重写的方法中。

最后的结果:

Protobuf和Json的,在这里就不一一介绍了,这两个处理起来比MessagePack复杂了不少。可以参考MemcachedTranscoder这个开源项目,也是MessagePack作者写的,虽然是5年前的,但是一样的好用。

对于Redis来说,在调用Set方法时要显式的将我们的值先进行序列化,不那么简洁,所以都会进行一次封装在使用。

对于Memcached来说,在调用Set方法的时候虽然不需要显式的进行序列化,但是有可能要我们自己去实现一个Transcoder,这也是有点麻烦的。

下面给大家推荐一个简单的缓存库来处理这些问题。

使用EasyCaching来简化操作

EasyCaching是笔者在业余时间写的一个简单的开源项目,主要目的是想简化缓存的操作,目前也在不断的完善中。

EasyCaching提供了前面所说的4种序列化方法可供选择:

  1. BinaryFormatter

  2. MessagePack

  3. Json

  4. 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 retrievervar 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}";  } }
  1. 使用的时候,在构造函数对IEasyCachingProvider进行依赖注入即可。

  2. 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 Cacheservices.AddDefaultMemcached(option=>{                option.AddServer("127.0.0.1",11211);            });        }    

   public void Configure(IApplicationBuilder app, IHostingEnvironment env)    {        //Important step for Memcache Cacheapp.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

原文:https://www.cnblogs.com/catcher1994/p/8543711.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321955.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

牛客练习赛52-记录

正题 比赛链接:https://ac.nowcoder.com/acm/contest/1084#question 成绩 T1:T1:T1:数数 题目大意 给出nnn&#xff0c;求∑i1n∑j1n(i∗j)\sum_{i1}^n \sum_{j1}^n (i*j)i1∑n​j1∑n​(i∗j) 和 ∏i1n∏j1n(i∗j)\prod_{i1}^n\prod_{j1}^n(i*j)i1∏n​j1∏n​(i∗j) 解题…

Java 并发总结——高并发与同步锁

一、高并发同步锁 &#xff08;1&#xff09;ThreadLocal threadLocal为每个线程维护一个本地变量。 采用空间换时间&#xff0c;它用于线程间的数据隔离&#xff0c;为每一个使用该变量的线程提供一个副本&#xff0c;每个线程都可以独立地改变自己的副本&#xff0c;而不会…

Windows Developer Day - Windows AI Platform

本次 Windows Developer Day&#xff0c;最值得期待的莫过于 Windows AI Platform 了&#xff0c;可以说是千呼万唤始出来。观看直播的开发者们&#xff0c;留言最多的也是 Windows AI Platform。下面结合微软提供的展示过程&#xff0c;文档和 Git Sample 来详细分析一下。基础…

NOI.AC-random【期望概率,统计】

正题 题目链接:http://noi.ac/contest/235/problem/227 题目大意 两个nnn长度为AAA和BBB的序列&#xff0c;从两个序列中各随机取一个数出来&#xff0c;求期望哪个序列的数大。 解题思路 总共有n∗nn*nn∗n种情况&#xff0c;每种情况等概率&#xff0c;排序用指针统计一下…

Java 并发总结——线程池

一、线程池 在程序启动的时候就创建若干线程来响应处理&#xff0c;它们被称为线程池&#xff0c;里面的线程叫工作线程 &#xff08;1&#xff09;线程池的作用 1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 2、提高响应速度。当任务到达时&am…

EF Core:一统SQL和NoSQL数据库

推出EF Core的初衷之一&#xff0c;就是开发出一种可在很少甚至不更改代码的情况下使用SQL和NoSQL数据库的模型。Microsoft正向此目标迈出第一步&#xff0c;发布了用于Azure Cosmos DB的实验性EF提供程序&#xff08;provider&#xff09;。据EF 2.1路线图介绍&#xff1a;Cos…

P3470 [POI2008]BBB-BBB【线段树,贪心】

正题 题目链接:https://www.luogu.org/problem/P3470 题目大意 一个−-−序列&#xff0c;表示111&#xff0c;−-−表示−1-1−1。sis_isi​表示到第iii个的前缀和&#xff0c;要求 qsnp且qsi≥0(i∈[1..n])qs_np且qs_i\geq 0(i\in[1..n])qsn​p且qsi​≥0(i∈[1..n]) 然后…

Java 并发总结——进程与线程

一、进程与线程 &#xff08;1&#xff09;线程与进程 进程是程序在一个数据集合上运行的过程&#xff0c;它是系统进行资源分配和调度的一个独立单位。进程实体由程序段&#xff0c; 数据段 PCB&#xff08;进程控制块&#xff09;组成。 线程可以看做轻量级进程&#xff0c;…

.NET Core使用skiasharp文字头像生成方案(基于docker发布)

一、问题背景目前.NET Core下面针对于图像处理的库微软并没有集成&#xff0c;在.NET FrameWork下我们已经习惯使用System.Drawing类库做简单的图像处理&#xff0c;到了.NET Core下一脸懵逼的我&#xff0c;只能百度谷歌看看有没啥解决方案&#xff0c;好在网上资料也多&#…

jzoj6375-华灵「蝶妄想」【结论题】

正题 题目大意 n∗mn*mn∗m填(((或者)))。求一个方案使得最多的行和列匹配。 解题思路 我们先考虑nnn或mmm为奇数&#xff0c;那么显然奇数的肯定不必配&#xff0c;那么就只需要考虑行或列即可。 若nnn和mmm都为偶数时 我们发现在边边的行列不可能都匹配上&#xff0c;那就…

C# - Span 全面介绍:探索 .NET 新增的重要组成部分

假设要公开特殊化排序例程&#xff0c;以就地对内存数据执行操作。可能要公开需要使用数组的方法&#xff0c;并提供对相应 T[] 执行操作的实现。如果方法的调用方有数组&#xff0c;且希望对整个数组进行排序&#xff0c;这样做就非常合适。但如果调用方只想对部分数组进行排序…

Java 并发总结——AQS

一、AQS Java并发包&#xff08;JUC&#xff09;中提供了很多并发工具&#xff0c;ReentrangLock、Semaphore、CountDownLatch&#xff0c;它们的实现都用到了一个共同的基类——AbstractQueuedSynchronizer&#xff0c;简称AQS。 AQS是一个用来构建锁和同步器的框架&#xf…

P4427-[BJOI2018]求和【LCA】

正题 题目大意:https://www.luogu.org/problem/P4427 题目大意 一棵树&#xff0c;每次给一条路径&#xff0c;求路径上每个点的深度的kkk次方的和。 解题思路 路径上分成两条深度连续的链&#xff0c;所以我们可以先预处理出kkk次方的前缀和。 然后LCALCALCA就好了。 code…

C#中DateTime的缺陷与代替品DateTimeOffset

C#中的DateTime在逻辑上有个非常严重的缺陷&#xff1a;> var d DateTime.Now;> var d2 d.ToUniversalTime();> d d2false> d.Equals(d2);false在C#交互模式中输入以上代码&#xff0c;可以发现尽管一个是本地时间&#xff08;d&#xff09;&#xff0c;一个是U…

Java SSL与TLS客户端证书配置

一、TLS安全概念 &#xff08;1&#xff09;PKI PKI是 Public Key Infrastructure的简称&#xff0c;意思是公钥基础设施。 公钥基础设施是提供公钥加密和数字签名服务的系统或平台&#xff0c;目的是为了管理密钥和证书。通过证书和秘钥来确认通讯双方是否可信任。 &#x…

P3901-数列找不同【模拟】

正题 题目链接:https://www.luogu.org/problem/P3901 题目大意 一个序列AAA&#xff0c;每次询问一个区间求这个区间的数是否各不相同。 解题思路 用lastilast_ilasti​表示前一个与iii相同数的位置。 然后Lastimax{lastj}(j<i)Last_imax\{last_j\}(j<i)Lasti​max{la…

Java RSA私钥的格式pkcs1和pkcs8、PrivateKey转换

&#xff08;&#xff09;一、私钥格式 &#xff08;1&#xff09;pkcs1 格式&#xff1a; -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY----- &#xff08;2&#xff09;pkcs8 格式&#xff1a; -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KE…

重磅消息-Service Fabric 正式开源

微软的Azure Service Fabric的官方博客在2017.3.24日发布了一篇博客 Service Fabric .NET SDK goes open source &#xff0c;介绍了社区呼声最高的Service Fabric开源的情况以及当前的情况&#xff0c;当时开源了Service Fabric的.NET SDK部分&#xff0c;社区一直在期盼着Ser…

Java 证书pem转KeyStore、jks文件

一、服务端pem转KeyStore CertificateFactory certificateFactory CertificateFactory.getInstance("X.509");FileInputStream caInputStream new FileInputStream(caPath);List<X509Certificate> caList certificateFactory.generateCertificates(caInput…

牛客国庆集训派对day6T A- 2016【矩阵乘法】

正题 题目链接:https://ac.nowcoder.com/acm/contest/1111/A 题目大意 2∗22*22∗2的矩阵AAA&#xff0c;求AnA^nAn 解题思路 我们将其十进制的转化过来&#xff0c;如 A2016(A103)2∗(A10)∗(A)6A^{2016}(A^{10^3})^2*(A^{10})*(A)^6A2016(A103)2∗(A10)∗(A)6 这样时间复杂…