视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
.NET Core中使用Redis与Memcached的序列化问题详析
2020-11-27 22:35:02 责编:小采
文档


前言

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

序列化这一操作,或许是显式的,或许是隐式的,这个取决于使用的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那样加上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。

    需要注意下面两个说明

  • 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这个接口相应的方法即可。

    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

    好了,

    下载本文
    显示全文
    专题