之前一个项目涉及到针对海量(千万级)实时变化数据的计算,由于对性能要求非常高,我们不得不将参与计算的数据存放到内存中,并通过检测数据存储的变化实时更新内存的数据。存量的数据几乎耗用了上百G的内存,再加上它们在每个时刻都在不断地变化,所以每时每刻都无数的对象被创建出来(添加+修改),同时无数现有的对象被“废弃”(删除+修改)。这种情况针对GC的压力可想而知,所以每当进行一次2代GC的时候,计算的耗时总会出现“抖动”。为了解决这类问题,几天前尝试着创建了一个名为NativeBuffering的框架。目前这个框架远未成熟,而且是一种“时间换空间”的解决方案,虽然彻底解决了内存分配的问题,但是以牺牲数据读取性能为代价的。这篇文章只是简单介绍一下NativeBuffering的设计原理和用法,并顺便收集一下大家的建议。[本文演示源代码从这里下载]

一、让对象映射一段连续的内存

二、Unmanaged类型

三、BufferedBinary类型

四、BufferedString类型

一、让对象映射一段连续的内存

针对需要高性能的互联网应用来说,GC针对性能的影响是不得不考虑的,减少GC影响最根本的解决方案就是“不需要GC”。如果一个对象占据的内存是“连续的”,并且承载该对象的字节数是可知的,那么我们就可以使用一个预先创建的字节数组来存储数据对象。我们进一步采用“对象池”的方式来管理这些字节数组,那么就能实现真正意义上的“零分配”,自然也就不会带来任何的GC压力。不仅如此,连续的内存布局还能充分地利用各级缓存,对提高性能来说是一个加分项。如果从序列化/发序列话角度来说,这样的实现直接省去了反序列化的过程。

但是我们知道在托管环境这一前提是不成立的,只有值类型的对象映射一片连续的内存。对于引用类型的对象来说,只有值类型的字段将自身的值存储在该对象所在的内存区域,对于引用类型的字段来说,存储的仅仅目标对象的地址而已,所以“让对象映射一段连续内存”是没法做到的。但是基元类型和结构体默认采用这样的内存布局,所以我们可以采用“非托管或者Unsafe”的方式将它们映射到我们构建的一段字节序列。对于一个只包含基元类型和结构体成员的“复合”类型来说,对应实例的所有数据成员可以存储到一段连续的字节序列中。

既然如此,我们就可以设计这样一种数据类型:它不在使用“字段”来定义其数据成员,而将所有的数据成员转换成一段字节序列。我们为每个成员定义一个属性将数据读出来,这相当于实现了“将对象映射为一段连续内存”的目标。以此类推,任何一个数据类型其实都可以通过这样的策略实现”连续内存布局“。

正如上面提到过的,这是一种典型的”时间换空间“的解决方案,所以NativeBuffering的一个目标就是尽可能地提高读取数据成员的性能,其中一个主要的途径就是Buffer存储的字节就是数据类型原生(Native)的表现形式。也就是说原生的数据类型采用怎样的内存布局,NativeBuffering就采用怎样的布局,这也是NativeBuffering名称的由来。在这一根本前提下,NativeBuffering针对单一数据的读取并没有性能损失,因为中间不存在任何Marshal的过程,针对影响读取性能的因素是需要额外计算待读取数据在Buffer中的偏移量。

也正是为了保证“与数据类型的Native形式保持一直”,NativeBuffering对于数据类型做了限制。总地来说,NativeBuffering只支持Unmanaged、BufferedBinary和BufferedString三种基本类型。NativeBuffering将定义的数据类型称为BufferedMessage,除了上述三种基本的数据类型,BufferedMessage的数据类型还可以是另一个BufferedMessage类型,以及基于这四种类型的集合和字典。下面的内容主要从“内存布局”的角度介绍上述三种基本的数据类型,同时通过实例演示其基本用法。

二、Unmanaged类型

顾名思义,Unmanaged类型可以理解为不涉及托管对象引用的值类型(可以参与我们的文章《.NET的基元类型包括哪些?Unmanaged和Blittable类型又是什么?》),如下的类型属于Unmanaged 类型的范畴。由于这样的类型在托管和非托管环境的内存布局是完全一致的,所以可以使用静态类型Unsafe从指定的地址指针将值直接读取出来。

  • 14种基元类型+Decimal(decimal)

  • 枚举类型

  • 指针类型(比如int*, long*)

  • 只包含Unmanaged类型字段的结构体

我们创建一个简单的控制台程序演示NativeBuffering的基本用法。NativeBuffering除了提供同名的NuGet包外,还提供了一个名为NativeBuffering.Generator的NuGet包,后者以Source Generator的形式根据“原类型”生成对应的BufferedMessage类型,并生成用来计算字节数量和输出字节内容的代码。我们定义了如下这个Entity类作为“源类型”(上面标注了BufferedMessageSourceAttribute特性),由于我们还需要为该类型生成一些额外成员,所以必须将其定义成partial类。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
} public readonly record struct UnmanagedStruct(int X, double Y);

如上面的代码片段所示,Entity具有Foo和Bar两个数据成员,类型分别为long(Int64)和UnmanagedStruct ,它们都是Unmanaged类型。如果将这个Entity转换成对应的BufferedMessage,承载字节将具有如下的结构。任何一个BufferedMessage对象承载的字节都存储在一个预先创建的字节数组中。如果它具有N个成员(被称为字段),前N * 4个字节用来存储一个整数指向对应成员的起始位置(在字节数组中的索引),后续的字节依次存储每个数据成员。在读取某个成员的时候,先根据字段索引读取目标内容在缓冲区中的位置,然后根据类型读取对应的值。

有人可能说,既然值类型的长度都是固定的,完全可以按照下图(上)所示的方式直接以“平铺”的方式存储每个字段的值,然后根据数据类型确定具体字段的初始位置。实际上最初我也是这么设计的,但是如果考虑内存地址对齐下图(下),针对字段初始位置的计算就比较麻烦。内存对齐目前尚未实现,实现了之后相信对性能有较大的提升。

具有上述结构的字节不可能手工生成,所以我们采用了Source Generator的方式。安装的Source Generator(NativeBuffering.Generator)将会帮助我们生成如下图所示的两个.cs文件。

Entity.g.cs补上上了Entity这个partial类余下的部分。如下面的代码片段所示,自动生成的代码让这个类实现了IBufferedObjectSource接口,实现的CalculateSize用于计算生成的字节数,而具体的字节输出则实现在Writer方法中。

public partial class Entity : IBufferedObjectSource
{
public int CalculateSize()
{
var size = 0;
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Foo);
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Bar);
return size;
}
public void Write(BufferedObjectWriteContext context)
{
using var scope = new BufferedObjectWriteContextScope(context);
scope.WriteUnmanagedField(Foo);
scope.WriteUnmanagedField(Bar);
}
}

NativeBuffering.Generator还帮助我们自动生成对应的EntityBufferedMessage 类型。如下面的代码片段所示,为了尽可能节省内存,我们将其定义为只读的结构体,并实现了IReadOnlyBufferedObject<EntityBufferedMessage>
接口。EntityBufferedMessage是对一个NativeBuffer对象的封装,NativeBuffer是一个核心类型,用来表示从指定位置开始的一段缓冲区。它Bytes属性表示作为缓存区的字节数组,Start属性表示起始地址的指针。至于两个属性Foo和Bar返回的值,分别调用相应的方法从这个NativeBuffer对象中读取出来。

public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public System.Int64 Foo => Buffer.ReadUnmanagedField<System.Int64>(0);
public ref readonly UnmanagedStruct Bar => ref Buffer.ReadUnmanagedFieldAsRef<UnmanagedStruct>(1);
} public interface IReadOnlyBufferedObject<T> where T: IReadOnlyBufferedObject<T>
{
static abstract T Parse(NativeBuffer buffer);
} public unsafe readonly struct NativeBuffer
{
public byte[] Bytes { get; }
public void* Start { get; }
...
}

由于UnmanagedStruct 是一个自定义的结构体,我们知道值类型赋值采用“拷贝”的方式。如果这个结构体包含过多的成员,可能会因为拷贝的字节过多而带来性能问题,为此我直接返回这个结构体的引用。由于整个BufferedMessage 是只读的,所以返回的引用也是只读的。为了方便BufferedMessage对象的创建,我们为实现的IReadOnlyBufferedObject<EntityBufferedMessage>接口定义了一个静态方法Parse。如下的程序验证了EntityBufferedMessage 与原始Entity类的“等效性”。

using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Foo = 123,
Bar = new UnmanangedStruct(789, 3.14)
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
File.WriteAllBytes(".data", bytes); EntityBufferedMessage bufferedMessage;
BufferOwner? bufferOwner = null; try
{
using (var fs = new FileStream(".data", FileMode.Open))
{
var byteCount = (int)fs.Length;
bufferOwner = BufferPool.Rent(byteCount);
fs.Read(bufferOwner.Bytes, 0, byteCount);
} bufferedMessage = BufferedMessage.Create<EntityBufferedMessage>(ref bufferOwner);
Debug.Assert(bufferedMessage.Foo == 123);
Debug.Assert(bufferedMessage.Bar.X == 789);
Debug.Assert(bufferedMessage.Bar.Y == 3.14);
}
finally
{
bufferOwner?.Dispose();
}

整个演示程序分两个部分,第一个部分演示了如何将一个Entity对象转换成我们需要的字节,并持久化到一个文件中。第二部分演示如何读取字节并生成对应的EntityBufferedMessage,这里我们使用了“缓冲池”,所以针对EntityBufferedMessage的创建不会涉及内存分配。我们没有直接使用ArrayPool<byte>,因为数据成员根据指针读取,我们需要保证整个缓冲区不会因GC的“压缩”而移动位置,通过BufferPool实现的内存池将字节数组存储在POH中,位置永远不会改变。

三、BufferedBinary类型

BufferedBinary 是NativeBuffering支持的第二种基本类型,它表示一个长度确定的字节序列。和Unmanaged类型不同,这是一种长度可变的类型,所以我们使用前置的4字节以整数的形式表示字节长度。BufferedBinary 被定义成如下这样一个结构体,它同样实现了IReadOnlyBufferedObject<BufferedBinary>接口。我们可以调用AsSpan方法以ReadOnlySpan<byte>的形式字节序列。

public unsafe readonly struct BufferedBinary : IReadOnlyBufferedObject<BufferedBinary>
{
public BufferedBinary(NativeBuffer buffer) => Buffer = buffer;
public NativeBuffer Buffer { get; }
public int Length => Unsafe.Read<int>(Buffer.Start);
public ReadOnlySpan<byte> AsSpan() => new(Buffer.GetPointerByOffset(sizeof(int)), Length);
public static BufferedBinary Parse(NativeBuffer buffer) => new(buffer);
}

为了演示字节序列在NativeBuffering中的应用,我们为Entity类添加了如下这个字节数组类型的属性Baz。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
public byte[] Baz { get; set; }
}

新的Entity对应的BufferedMessage将具有如下的内存布局。

Entity类的定义一旦放生改变,NativeBuffering.Generator将自动修正生成的两个.cs文件的内容。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
public byte[] Baz { get; set; }
} public partial class Entity : IBufferedObjectSource
{
public int CalculateSize()
{
var size = 0;
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Foo);
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Bar);
size += NativeBuffering.Utilities.CalculateBinaryFieldSize(Baz);
return size;
}
public void Write(BufferedObjectWriteContext context)
{
using var scope = new BufferedObjectWriteContextScope(context);
scope.WriteUnmanagedField(Foo);
scope.WriteUnmanagedField(Bar);
scope.WriteBinaryField(Baz);
}
} public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public System.Int64 Foo => Buffer.ReadUnmanagedField<System.Int64>(0);
public ref UnmanagedStruct Bar => ref Buffer.ReadUnmanagedFieldAsRef<UnmanagedStruct>(1);
public BufferedBinary Baz => Buffer.ReadBufferedObjectField<BufferedBinary>(2);
}

在如下所示的演示程序中,通过Entity的Baz属性设置的字节数组,在生成的EntityBufferedMessage对象中,同样可以利用同名的属性读取出来。

using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Foo = 123,
Bar = new UnmanangedStruct(789, 3.14),
Baz = new byte[] { 1, 2, 3 }
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
File.WriteAllBytes(".data", bytes); EntityBufferedMessage bufferedMessage;
BufferOwner? bufferOwner = null; try
{
using (var fs = new FileStream(".data", FileMode.Open))
{
var byteCount = (int)fs.Length;
bufferOwner = BufferPool.Rent(byteCount);
fs.Read(bufferOwner.Bytes, 0, byteCount);
} bufferedMessage = BufferedMessage.Create<EntityBufferedMessage>(ref bufferOwner);
Debug.Assert(bufferedMessage.Foo == 123);
Debug.Assert(bufferedMessage.Bar.X == 789);
Debug.Assert(bufferedMessage.Bar.Y == 3.14); Debug.Assert(bufferedMessage.Baz.Length == 3);
var byteSpan = bufferedMessage.Baz.AsSpan();
Debug.Assert(byteSpan[0] == 1);
Debug.Assert(byteSpan[1] == 2);
Debug.Assert(byteSpan[2] == 3);
}
finally
{
bufferOwner?.Dispose();
}

四、BufferedString类型

字符串同样是一个“长度可变”数据类型。如果将一个字符串转换成一个一段连续的字节呢?可能很多人会说,那还不容易,将其编码不久可以了吗?确实没错,但是如何将编码转换成字符串呢?解码吗?不要忘了我们的目标是“创建一个完全无内存分配”的数据类型。当我们解码字节将其“还原”一个字符串时,实际上CLR会创建一个String类型(引用类型)的实例,并将指定的字节转换成标准的字符字节(采用UTF-16编码)并将其拷贝到实例所在的内存区域。

要达到我们“无分配”的目标,字符串转换的字节序列必须与这个String实例在内存中的内容完全一致。此时你不了解字符串对象在.NET中的内存布局,可以参阅我的另一篇文章《你知道.NET的字符串在内存中是如何存储的吗?》。总的来说,一个字符串实例由ObjHeader+TypeHandle+Length+Encoded Characters4部分组成。我们还需要知道整个字节序列的长度,所以我们还需要前置的4个字节。

字符串在NativeBuffering通过如下这个名为BufferedString的结构体表示,它同样实现了IReadOnlyBufferedObject<BufferedString>接口。BufferedString可以通过AsString方法转换成String类型,该方法不会带来任何的内存分配。AsString方法用在针对String的隐式类型转换操作符上,所以在任何使用到String类型的地方都可以直接使用BufferedString类型。

public unsafe readonly struct BufferedString : IReadOnlyBufferedObject<BufferedString>
{
private readonly void* _start;
public BufferedString(NativeBuffer buffer) => _start = buffer.Start;
public BufferedString(void* start)=> _start = start;
public static BufferedString Parse(NativeBuffer buffer) => new(buffer);
public static BufferedString Parse(void* start) => new(start);
public static int CalculateSize(void* start) => Unsafe.Read<int>(start);
public string AsString()
{
string v = default!;
Unsafe.Write(Unsafe.AsPointer(ref v), new IntPtr(Unsafe.Add<byte>(_start, sizeof(int) + IntPtr.Size)));
return v;
}
public static implicit operator string(BufferedString value) => value.AsString();
public override string ToString() => AsString();
}

为了演示字符串在NativeBuffering中的应用,我们为Entity添加了字符串类型的Qux属性。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
public byte[] Baz { get; set; }
public string Qux { get; set; }
}

对于新的Entity类型,它对应的BufferedMessage封装的字节序列将变成如下的结构。

在Entity添加的Qux属性,也将同步体现在生成的两个.cs文件中。

public partial class Entity : IBufferedObjectSource
{
public int CalculateSize()
{
var size = 0;
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Foo);
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Bar);
size += NativeBuffering.Utilities.CalculateBinaryFieldSize(Baz);
size += NativeBuffering.Utilities.CalculateStringFieldSize(Qux);
return size;
}
public void Write(BufferedObjectWriteContext context)
{
using var scope = new BufferedObjectWriteContextScope(context);
scope.WriteUnmanagedField(Foo);
scope.WriteUnmanagedField(Bar);
scope.WriteBinaryField(Baz);
scope.WriteStringField(Qux);
}
} public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public System.Int64 Foo => Buffer.ReadUnmanagedField<System.Int64>(0);
public ref UnmanagedStruct Bar => ref Buffer.ReadUnmanagedFieldAsRef<UnmanagedStruct>(1);
public BufferedBinary Baz => Buffer.ReadBufferedObjectField<BufferedBinary>(2);
public BufferedString Qux => Buffer.ReadBufferedObjectField<BufferedString>(3);
}

我们同样在演示程序中添加了针对字符串数据成员的验证。

using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Foo = 123,
Bar = new UnmanangedStruct(789, 3.14),
Baz = new byte[] { 1, 2, 3 },
Qux = "Hello, World!"
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
File.WriteAllBytes(".data", bytes); EntityBufferedMessage bufferedMessage;
BufferOwner? bufferOwner = null; try
{
using (var fs = new FileStream(".data", FileMode.Open))
{
var byteCount = (int)fs.Length;
bufferOwner = BufferPool.Rent(byteCount);
fs.Read(bufferOwner.Bytes, 0, byteCount);
} bufferedMessage = BufferedMessage.Create<EntityBufferedMessage>(ref bufferOwner);
Debug.Assert(bufferedMessage.Foo == 123);
Debug.Assert(bufferedMessage.Bar.X == 789);
Debug.Assert(bufferedMessage.Bar.Y == 3.14);
Debug.Assert(bufferedMessage.Baz.Length == 3); var byteSpan = bufferedMessage.Baz.AsSpan();
Debug.Assert(byteSpan[0] == 1);
Debug.Assert(byteSpan[1] == 2);
Debug.Assert(byteSpan[2] == 3); Debug.Assert(bufferedMessage.Qux == "Hello, World!");
}
finally
{
bufferOwner?.Dispose();
}

NativeBuferring&mdash;&mdash;一种零分配的数据类型[上篇]的更多相关文章

  1. 深入剖析Linux IO原理和几种零拷贝机制的实现

    深入剖析Linux IO原理和几种零拷贝机制的实现 来源 https://zhuanlan.zhihu.com/p/83398714 零壹技术栈      公众号[零壹技术栈] 前言 零拷贝(Zero ...

  2. [转]详述DHCP服务器的三种IP分配方式

    DHCP就是动态主机配置协议(Dynamic Host Configuration Protocol),它的目的就是为了减轻TCP/IP网络的规划.管理和维护的负担,解决IP地址空间缺乏问题.这种网络 ...

  3. Nginx upstream的5种权重分配方式【转】

    原文地址:Nginx upstream的5种权重分配方式 1.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除. 2.weight指定轮询几率,weig ...

  4. C++三种内存分配方式

    从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量,static变量.静态分配的区域的生命期是整个软件运行期,就是说从软件运行开始到软件终止退出.只 ...

  5. Nginx upstream的5种权重分配方式

    .轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器down掉,能自动剔除 .weight 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况. upstre ...

  6. Nginx upstream的5种权重分配方式分享

    Nginx负载均衡的分发方式有4种: 1.轮询,默认采取此方式,Nginx会按照请求时间的先后顺序进行轮询分发,若某台Web Server宕机,Nginx自动将其摘掉. 2.weight,权重,即轮询 ...

  7. Nginx upstream的5种权重分配方式(转)

    出处:http://www.cnblogs.com/funsion/p/4003499.html?utm_source=tuicool 1.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器, ...

  8. 算发帖&mdash;&mdash;俄罗斯方块覆盖问题一共有多少个解

    问题的提出:如下图,用13块俄罗斯方块覆盖8*8的正方形.   那么一共可以有多少个解呢?(若通过旋转.翻转一个解而得到的新解,则两个解视为同一个解)   首先,求解的问题,已经在上一篇帖子里完成 算 ...

  9. Maven&mdash;&mdash;软件开发中一个神奇的项目管理工具

    由于本人是从c++转入从事JAVA工作的 所以很多东西要从头学起,相信有很多跟我一样的人吧,那么我们一起来学习. 今天我们一起来认识下Maven这个工具,很多人可能会问题了,为什么说是工具呢?不是写代 ...

  10. 渗透测试流程&mdash;&mdash;渗透测试的9个步骤(转)

    目录 明确目标 分析风险,获得授权 信息收集 漏洞探测(手动&自动) 漏洞验证 信息分析 利用漏洞,获取数据 信息整理 形成报告 1.明确目标 1)确定范围:测试的范围,如:IP.域名.内外网 ...

随机推荐

  1. 【有问必答】搭建uniapp项目流程手把手教学

    前言 缘由 博友有问,狗哥必答 前段时间,博友加本狗微信,询问uniapp的学习方法.本狗资历浅薄,没有专门学过uniapp,只能将自己日常开发uniapp的基本流程和步骤进行分享,希望可以略尽绵薄之 ...

  2. 深度相机(TOF)的工作原理

    文章目录 深度相机(TOF)的工作原理 TOF由什么组成? 一.TOF相机采用主动光探测,通常包括以下几个部分: 二.TOF是如何测距的呢? 三.TOF会受什么影响? 四.那TOF相机最后输出的是什么 ...

  3. OceanBase的学习与使用

    OceanBase的学习与使用 简介 1. OceanBase数据库 注意这一块下载的其实是rpm包. 一般是通过下面的OAT或者是OCP工具进行安装. 有x86还有ARM两种架构. 虽然是el7结尾 ...

  4. 在Winform分页控件中集成保存用户列表显示字段及宽度调整设置

    在Winform的分页控件里面,我们提供了很多丰富的功能,如常规分页,中文转义.导出Excel.导出PDF等,基于DevExpress的样式的分页控件,我们在其上面做了不少封装,以便更好的使用,其中就 ...

  5. php获取未解码之前的原始接口请求参数

    前言 目前的几个项目,业务方基本都使用POST方式请求接口,我们本机磁盘会保留一份请求的原始参数用于请求分析和问题排查使用,一般有问题,也会基于seqid(请求唯一id)捞到日志,copy参数模拟请求 ...

  6. 2020-11-21:java中,什么是跨代引用?

    福哥答案2020-11-21: [答案来自此链接:](http://bbs.xiangxueketang.cn/question/404) 跨代引用常出现与 CMS 分代收集时需要使用可达性分析判断对 ...

  7. OData WebAPI实践-与ABP vNext集成

    本文属于 OData 系列文章 ABP 是一个流行的 ASP. NET 开发框架,旧版的的 ABP 已经能够非常好的支持了 OData ,并提供了对应的 OData 包. ABP vNext 是一个重 ...

  8. for循环原理补充、生成器对象、yield冷门用法、生成器表达式的面试题、常见内置函数

    目录 一.for循环原理补充 二.生成器对象 (1).自定义生成器对标range功能(一个参数 两个参数 三个参数 迭代器对象) 三.yield冷门用法 (1).yield与return的对比 四.生 ...

  9. [ABC268C] Chinese Restaurant

    [ABC268C] Chinese Restaurant 声明:以上的所有操作都会再做一次\(%n+n)%n\),比如\(i - 1\)会变成\(((i-1)%n+n)%n\) 题意 有 \(n\) ...

  10. pbootcms对接微信扫码登录代码核心片段和步骤(前后端)

    首先需要在微信公众平台或开放平台中创建应用,并获取到AppID和AppSecret. 在pbootcms中创建一个自定义模板页面(例如:wechat_login.html),并在该页面中添加以下代码, ...