线程池

“线程池”就是用来存放“线程”的对象池。

作用:因为创建一个线程的代价较高,因此我们使用线程池设法复用线程。

CLR线程池

在.NET中,CLR线程和操作系统线程对应,您可以简单地认为.NET中的Thread对象便封装了一个操作系统线程,并附带一些托管环境下所需要的数据(如GC Handle)。而CLR线程池便是存放这些CLR线程的对象池。Thread对象只有当真正Start了之后,CLR才会创建一个操作系统线程与它绑定。

ThreadPool类的两个静态方法:QueueUserWorkItem和UnsafeUserQueueWorkItem向CLR线程池中添加任务(一个WorkCallback委托对象),这两个方法的区别,在于前者会收集调用方的ExecutionContext,也就是保留了的当前线程的执行信息(如认证或语言文化等),使任务最终会在“创建”时刻的环境中执行——后者就不会。因此,如果比较两个方法的绝对性能,Unsafe方法会略胜一筹。但是平时还是建议使用QueueUserWorkItem方法,因为保留执行上下文会避免很多麻烦事情,且这点性能损耗其实算不上什么。

注:

ASP.NET在得到一个请求后,也会将这个请求处理的任务交由CLR线程池去执行——请注意,它们最多只是添加任务而已,并不表示任务会立即执行。所有添加到CLR线程池的任务都会在合适的时候得以执行——可能马上,也可能要稍等片刻,甚至更久。简单的概括说来,便是线程池内有空闲的线程,或线程池所管理的线程数量还没有达到上限的时候。如果有空闲的线程,线程池就会立即让它领取一个任务执行。如果是第二种情况,线程池便会创建新的Thread对象。由于让操作系统管理太多线程反而会造成性能下降,因此CLR线程池会有一个上限。

对于ASP.NET应用程序来说,CLR线程池容量代表了应用程序最多可以同时执行的请求数量。对于托管在IIS上的ASP.NET执行环境来说,这个值由全局配置决定。这个配置在machine.config文件中system.web/processModel节点中,为maxWorkerThreads属性,它决定了为单个处理器分配的线程数。如果这个值为40,且机器上拥有4个处理器(2 * 2CPU),那么这台机器目前的配置表示在同一时刻,ASP.NET可以同时处理160个请求。

既然有最大值,也就相应有了最小值,它代表了CLR线程池“总是会保留”的最少线程数量。由于线程会占用资源,如在默认情况下,每个线程将获得1MB大小的栈空间3。所以如果在系统中保留太多空闲线程对资源也是一种浪费。因此,CLR线程池在使用大量线程处理完大量任务之后,也会逐步地释放线程,直至到达最小值。CLR线程池的最小线程数量确保了在任务数量较少的情况下,新来的任务可以立即执行,从而省去了创建新线程的时间。在普通应用程序中这个值为“处理器数 * 1”,而在ASP.NET应用程序中这个值配置在machine.config文件中system.web/processModel节点的minWorkerThreads属性中4。

对于processModel节点的数据,ASP.NET只会读取machine.config中的全局配置信息,这意味着我们不能使用web.config为不同应用程序配置不同的参数。如果我们要实现应用程序级别的配置,那么必须使用ThreadPool类中提供的API进行设置。

CLR线程池限制了线程的创建速度不超过每秒2个。这样,即使在某个瞬时获得了大量的任务,CLR线程池也可以使用相对较少的线程来完成所有工作。

但是,还有一种情况也值得考虑。例如,对于一个比较繁忙的Web应用程序来说,一打开便会涌入大量的连接。由于线程的创建速度有限,因此可以执行的请求数量也只能慢慢增加。对于这种您预料到会产生大量线程,而且忙碌状况会持续一段时间的情况,限制线程的创建速度反而会带来损伤效率。这时,您就可以手动设置CLR线程池的最小线程数量。如果此时CLR线程池中拥有的线程数量较少,那么系统就会立即创建一定数量的线程来达到这个最小值。设置和获取CLR线程池最小线程数量的接口为:

1
2
3
4
5
6
public static class ThreadPool
{
public static void GetMinThreads(out int workerThreads, out int completionPortThreads);
public static bool SetMinThreads(int workerThreads, int completionPortThreads);
public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads);
}

参考:ThreadPool 类

注意:

无论是设置还是获取到的这些数值,都与处理器数量没有任何关系了。也就是说,在一台2 * 2CPU的机器上运行一个普通的.NET应用程序时:

  • 调用GetMaxThreads方法将获得1000,表示CLR线程池最大容量为1000(250 * 4),而不是250。
  • 调用SetMinThreads并传入100,表示CLR线程池所拥有的最小线程数量为100,而不是400(100 * 4)。

独立线程池

在一个.NET应用程序中会有一个CLR线程池,可以使用ThreadPool类中的静态方法来使用这个线程池。整个进程内部几乎所有的任务都会依赖这个线程池。由于开发人员对于统一的线程池无法做到精确控制,因此在一些特别的需要就无法满足了。举个最常见例子:控制运算能力。

我们可以简单的认为,在同样的环境下,一个任务使用的线程数量越多,它所获得的运算能力就比另一个线程数量较少的任务要来得多。运算能力自然就涉及到任务执行的快慢。

可以设想一下,有一个生产任务,和一个消费任务,它们使用一个队列做临时存储。在理想情况下,生产和消费的速度应该保持相同,这样可以带来最好的吞吐量。如果生产任务执行较快,则队列中便会产生堆积,反之消费任务就会不断等待,吞吐量也会下降。因此,在实现的时候,我们往往会为生产任务和消费任务分别指派独立的线程池,并且通过增加或减少线程池内线程数量来条件运算能力,使生产和消费的步调达到平衡。

如果需要在同一进程内创建多个线程池,可以借助 SmartThreadPool

IO线程池

IO线程池便是为异步IO服务的线程池。

BeginGetResponse将发起一个利用IOCP的异步IO操作,并在结束时调用HandleAsyncCallback回调函数。那么,这个回调函数是由哪里的线程执行的呢?没错,就是传说中“IO线程池”的线程。.NET在一个进程中准备了两个线程池,除了上篇文章中所提到的CLR线程池之外,它还为异步IO操作的回调准备了一个IO线程池。IO线程池的特性与CLR线程池类似,也会动态地创建和销毁线程,并且也拥有最大值和最小值。

只可惜,IO线程池也仅仅是那“一整个”线程池,CLR线程池的缺点IO线程池也一应俱全。例如,在使用异步IO方式读取了一段文本之后,下一步操作往往是对其进行分析,这就进入了计算密集型操作了。但对于计算密集型操作来说,如果使用整个IO线程池来执行,我们无法有效的控制某项任务的运算能力。

参考:

浅谈线程池(上):线程池的作用及CLR线程池

浅谈线程池(中):独立线程池的作用及IO线程池

浅谈线程池(下):相关试验及注意事项

.NET Standard 是一项 API 规范,每一个特定的版本,都定义了必须实现的基类库(NETStandard.Library)。

如图:

dotnet-tomorrow.png

托管框架的每一种实现都有一套自己的基类库。基类库(BCL)包含诸如异常处理、字符串、XML、I/O、网络和集合这样的类。

.NET Standard 是一项实现BCL 的规范。由于.NET 实现需要遵循这项规范,所以应用程序开发人员就不用担心每一种托管框架实现的BCL 不同。

框架类库(FCL),如 WPF、WCF、ASP.NET,不包含在 BCL 中,因此,也就不包含在.NET Standard 中。

.NET Standard 与.NET 实现之间的关系就和 HTML 规范与浏览器之间的关系一样。后者是前者的实现。

因此,.NET Framework、Xamarin 和.NET Core,每一种托管框架都实现了.NET Standard 中的 BCL。随着计算机工业不断推出新的硬件和操作系统,将来还会出现新的.NET 托管框架。该标准让应用程序开发人员知道,他们可以依赖于一套始终如一的 API。

每个.NET 版本都对应一个.NET Standard 版本。

版本对应图:

QQ截图20200110132446.png

API 一致,将应用程序移植到不同的托管实现以及提供工具都会更简单。

.NET Standard 被定义为一个单独的 NuGet 包,因为所有的.NET 实现都必须支持它。工具变得简单了,因为对于特定的版本,它们有一套相同的 API。你还可以针对多个.NET 实现构建一个库项目。

参考:

Introducing .NET Standard

.NET Core 和.NET Standard 有什么不同

.NET Standard Versions

转载:TPL DataFlow初探(二)

TPL DataFlow初探(二)简单的介绍了TDF提供的一些Block,通过对这些Block配置和组合,可以满足很多的数据处理的场景。这一篇将继续介绍与这些Block配置的相关类,和挖掘一些高级功能。

在一些Block的构造函数中,我们常常可以看见需要你输入 DataflowBlockOptions 类型或者它的两个派生类型ExecutionDataflowBlockOptionsGroupingDataflowBlockOptions

DataflowBlockOptions

DataflowBlockOptions 有五个属性:BoundedCapacityCancellationTokenMaxMessagesPerTaskNameFormatTaskScheduler

用BoundedCapacity来限定容量

这个属性用来限制一个Block中最多可以缓存数据项的数量,大多数Block都支持这个属性,这个值默认是DataflowBlockOptions.Unbounded = -1,也就是说没有限制。开发人员可以制定这个属性设置数量的上限。那后面的新数据将会延迟。比如说用一个 BufferBlock 连接一个 ActionBlock,如果在 ActionBlock 上面设置了上限,ActionBlock 处理的操作速度比较慢,留在 ActionBlock 中的数据到达了上限,那么余下的数据将留在BufferBlock中,直到 ActionBlock 中的数据量低于上限。这种情况常常会发生在生产者生产的速度大于消费者速度的时候,导致的问题是内存越来越大,数据操作越来越延迟。我们可以通过一个 BufferBlock 连接多个ActionBlock 来解决这样的问题,也就是负载均衡。一个 ActionBlock 满了,就会放到另外一个 ActionBlock 中去了。

用CancellationToken来取消操作

TPL中常用的类型。在Block的构造函数中放入 CancellationToken,Block将在它的整个生命周期中全程监控这个对象,只要在这个Block结束运行(调用Complete方法)前,用 CancellationToken 发送取消请求,该Block将会停止运行,如果Block中还有没有处理的数据,那么将不会再被处理。

用MaxMessagesPerTask控制公平性

每一个Block内部都是异步处理,都是使用TPL的 Task。TDF的设计是在保证性能的情况下,尽量使用最少的任务对象来完成数据的操作,这样效率会高一些,一个任务执行完成一个数据以后,任务对象并不会销毁,而是会保留着去处理下一个数据,直到没有数据处理的时候,Block才会回收掉这个任务对象。但是如果数据来自于多个Source,公平性就很难保证。从其他Source来的数据必须要等到早前的那些Source的数据都处理完了才能被处理。这时我们就可以通过MaxMessagesPerTask 来控制。这个属性的默认值还是 DataflowBlockOptions.Unbounded=-1,表示没有上限。假如这个数值被设置为1的话,那么单个任务只会处理一个数据。这样就会带来极致的公平性,但是将带来更多的任务对象消耗。

用NameFormat来定义Block名称

MSDN上说属性 NameFormat 用来获取或设置查询块的名称时要使用的格式字符串。

Block的名字 Name=string.format(NameFormat, block.GetType ().Name, block.Completion.Id)。所以当我们输入”{0}”的时候,名字就是 block.GetType ().Name,如果我们数据的是”{1}”,那么名字就是block.Completion.Id。如果是“{2}”,那么就会抛出异常。

用TaskScheduler来调度Block行为

TaskScheduler 是非常重要的属性。同样这个类型来源于TPL。每个Block里面都使用 TaskScheduler 来调度行为,无论是源Block和目标Block之间的数据传递,还是用户自定义的执行数据方法委托,都是使用的 TaskScheduler。如果没有特别设置的话,将使用 TaskScheduler.Default(System.Threading.Tasks.ThreadPoolTaskScheduler)来调度。我们可以使用其他的一些继承于 TaskScheduler 的类型来设置这个调度器,一旦设置了以后,Block中的所有行为都会使用这个调度器来执行。.Net Framework 4 中内建了两个 Scheduler ,一个是默认的 ThreadPoolTaskScheduler ,另一个是用于UI线程切换的 SynchronizationContextTaskScheduler 。如果你使用的Block设计到UI的话,那可以使用后者,这样在UI线程切换上面将更加方便。

.Net Framework 4.5 中,还有一个类型被加入到 System.Threading.Tasks 名称空间下:ConcurrentExclusiveSchedulerPair 。这个类是两个 TaskScheduler 的组合。它提供两个 TaskSchedulerConcurrentSchedulerExclusiveScheduler;我们可以把这两个 TaskScheduler 构造进要使用的Block中。他们保证了在没有排他任务的时候(使用 ExclusiveScheduler 的任务),其他任务(使用ConcurrentScheduler)可以同步进行,当有排他任务在运行的时候,其他任务都不能运行。其实它里面就是一个读写锁。这在多个Block操作共享资源的问题上是一个很方便的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public ActionBlock<int> readerAB1;

public ActionBlock<int> readerAB2;

public ActionBlock<int> readerAB3;

public ActionBlock<int> writerAB1;

public BroadcastBlock<int> bb = new BroadcastBlock<int>((i) => { return i; });

public void Test()
{
ConcurrentExclusiveSchedulerPair pair = new ConcurrentExclusiveSchedulerPair();

readerAB1 = new ActionBlock<int>((i) =>
{
Console.WriteLine("ReaderAB1 begin handling." + " Execute Time:" + DateTime.Now);
Thread.Sleep(500);
}
, new ExecutionDataflowBlockOptions() { TaskScheduler = pair.ConcurrentScheduler });

readerAB2 = new ActionBlock<int>((i) =>
{
Console.WriteLine("ReaderAB2 begin handling." + " Execute Time:" + DateTime.Now);
Thread.Sleep(500);
}
, new ExecutionDataflowBlockOptions() { TaskScheduler = pair.ConcurrentScheduler });

readerAB3 = new ActionBlock<int>((i) =>
{
Console.WriteLine("ReaderAB3 begin handling." + " Execute Time:" + DateTime.Now);
Thread.Sleep(500);
}
, new ExecutionDataflowBlockOptions() { TaskScheduler = pair.ConcurrentScheduler });

writerAB1 = new ActionBlock<int>((i) =>
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("WriterAB1 begin handling." + " Execute Time:" + DateTime.Now);
Console.ResetColor();
Thread.Sleep(3000);
}
, new ExecutionDataflowBlockOptions() { TaskScheduler = pair.ExclusiveScheduler });

bb.LinkTo(readerAB1);
bb.LinkTo(readerAB2);
bb.LinkTo(readerAB3);


Task.Run(() =>
{
while (true)
{
bb.Post(1);
Thread.Sleep(1000);
}
});

Task.Run(() =>
{
while (true)
{
Thread.Sleep(6000);
writerAB1.Post(1);
}
});

}

01161456-7b8227892cb14d20b5347fa310224619.jpg

用MaxDegreeOfParallelism来并行处理

通常,Block中处理数据都是单线程的,一次只能处理一个数据,比如说 ActionBlock 中自定义的代理。使用 MaxDegreeOfParallelism 可以让你并行处理这些数据。属性的定义是最大的并行处理个数。如果定义成-1的话,那就是没有限制。用户需要在实际情况中选择这个值的大小,并不是越大越好。如果是平行处理的话,还应该考虑是否有共享资源。

TDF中的负载均衡

我们可以使用Block很方便的构成一个生产者消费者的模式来处理数据。当生产者产生数据的速度快于消费者的时候,消费者Block的Buffer中的数据会越来越多,消耗大量的内存,数据处理也会延时。这时,我们可以用一个生产者Block连接多个消费者Block来解决这个问题。由于多个消费者Block一定是并行处理,所以对共享资源的处理一定要做同步处理。

使用BoundedCapacity属性来实现

当连接多个 ActionBlock 的时候,可以通过设置 ActionBlockBoundedCapacity 属性。当第一个满了,就会放到第二个,第二个满了就会放到第三个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public BufferBlock<int> bb = new BufferBlock<int>();

public ActionBlock<int> ab1 = new ActionBlock<int>((i) =>
{
Thread.Sleep(1000);
Console.WriteLine("ab1 handle data" + i + " Execute Time:" + DateTime.Now);
}
, new ExecutionDataflowBlockOptions() { BoundedCapacity = 2 });

public ActionBlock<int> ab2 = new ActionBlock<int>((i) =>
{
Thread.Sleep(1000);
Console.WriteLine("ab2 handle data" + i + " Execute Time:" + DateTime.Now);
}
, new ExecutionDataflowBlockOptions() { BoundedCapacity = 2 });

public ActionBlock<int> ab3 = new ActionBlock<int>((i) =>
{
Thread.Sleep(1000);
Console.WriteLine("ab3 handle data:" + i + " Execute Time:" + DateTime.Now);
}
, new ExecutionDataflowBlockOptions() { BoundedCapacity = 2 });

public void Test()
{
bb.LinkTo(ab1);
bb.LinkTo(ab2);
bb.LinkTo(ab3);

for (int i = 0; i < 9; i++)
{
bb.Post(i);
}
}

01161504-e9c265982cb54d0eb844ee1a1be1e92d.jpg

转载:TPL DataFlow初探(一)

属性 TPL Dataflow 是微软面向高并发应用而推出的一个类库。借助于异步消息传递与管道,它可以提供比线程池更好的控制,也比手工线程方式具备更好的性能。我们常常可以消息传递,生产-消费模式或Actor-Agent模式中使用。在TDF是构建于 Task Parallel Library (TPL)之上的,它是我们开发高性能,高并发的应用程序的又一利器。

TDF的主要作用就是 Buffering DataProcessing Data ,在TDF中,有两个非常重要的接口,ISourceBlock<T>ITargetBlock<T> 接口。继承于 ISourceBlock<T> 的对象时作为提供数据的数据源对象-生产者,而继承于 ITargetBlock<T> 接口类主要是扮演目标对象-消费者。在这个类库中,System.Threading.Tasks.Dataflow 名称空间下,提供了很多以Block名字结尾的类,ActionBlockBufferBlockTransformBlockBroadcastBlock 等9个Block,我们在开发中通常使用单个或多个Block组合的方式来实现一些功能。

支持的版本:

QQ截图20200110135719.png

备注:
TPL 数据流库(System.Threading.Tasks.Dataflow 命名空间)不随 .NET 一起分发。 若要在 Visual Studio 中安装 System.Threading.Tasks.Dataflow 命名空间,请打开项目,选择“项目” 菜单中的“管理 NuGet 包” ,再在线搜索 System.Threading.Tasks.Dataflow 包。 或者,若要使用 .NET Core CLI 进行安装,请运行 dotnet add package System.Threading.Tasks.Dataflow

BufferBlock

BufferBlock 是TDF中最基础的 BlockBufferBlock 提供了一个有界限或没有界限的 Buffer,该 Buffer 中存储T。该 Block 很像 BlockingCollection<T> 。可以用过 Post 往里面添加数据,也可以通过Receive 方法阻塞或异步的的获取数据,数据处理的顺序是 FIFO 的。它也可以通过Link向其他 Block 输出数据。

01161517-0f5c7949243a4f9b995672da83950fb9.png

简单的同步的生产者消费者代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static BufferBlock<int> m_buffer = new BufferBlock<int>();

// Producer
private static void Producer()
{
while(true)
{
int item = Produce();
m_buffer.Post(item);
}
}

// Consumer
private static void Consumer()
{
while(true)
{
int item = m_buffer.Receive();
Process(item);
}
}

// Main
public static void Main()
{
var p = Task.Factory.StartNew(Producer);
var c = Task.Factory.StartNew(Consumer);
Task.WaitAll(p,c);
}

BufferBlock

ActionBlock

ActionBlock 实现 ITargetBlock,说明它是消费数据的,也就是对输入的一些数据进行处理。它在构造函数中,允许输入一个委托,来对每一个进来的数据进行一些操作。如果使用Action(T) 委托,那说明每一个数据的处理完成需要等待这个委托方法结束,如果使用了 Func<TInput, Task>) 来构造的话,那么数据的结束将不是委托的返回,而是Task的结束。默认情况下,ActionBlockFIFO 的处理每一个数据,而且一次只能处理一个数据,一个处理完了再处理第二个,但也可以通过配置来并行的执行多个数据。

01161519-5f62f15310e548b9a06f3fa9b603a149.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ActionBlock<int> abSync = new ActionBlock<int>((i) =>
{
Thread.Sleep(1000);
Console.WriteLine(i + " ThreadId:" + Thread.CurrentThread.ManagedThreadId + " Execute Time:" + DateTime.Now);
}
);

public void TestSync()
{
for (int i = 0; i < 10; i++)
{
abSync.Post(i);
}

Console.WriteLine("Post finished");
}

01161524-b5276dfab96c4200aa72b906a3b4009b.jpg

可见,ActionBlock 是顺序处理数据的,这也是 ActionBlock 一大特性之一。主线程在往 ActionBlockPost 数据以后马上返回,具体数据的处理是另外一个线程来做的。数据是异步处理的,但处理本身是同步的,这样在一定程度上保证数据处理的准确性。下面的例子是使用async和await。

1
2
3
4
5
public ActionBlock<int> abSync2 = new ActionBlock<int>(async (i) =>
{
await Task.Delay(1000);
Console.WriteLine(i + " ThreadId:" + Thread.CurrentThread.ManagedThreadId + " Execute Time:" + DateTime.Now);
}

01161525-ab3d15e3e91641be9a15988a88effddf.jpg

虽然还是1秒钟处理一个数据,但是处理数据的线程会有不同。

如果你想异步处理多个消息的话,ActionBlock 也提供了一些接口,让你轻松实现。在 ActionBlock 的构造函数中,可以提供一个 ExecutionDataflowBlockOptions 的类型,让你定义 ActionBlock 的执行选项,在下面了例子中,我们定义了 MaxDegreeOfParallelism 选项,设置为3。目的的让 ActionBlock 中的Item最多可以3个并行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ActionBlock<int> abAsync = new ActionBlock<int>((i) =>
{
Thread.Sleep(1000);
Console.WriteLine(i + " ThreadId:" + Thread.CurrentThread.ManagedThreadId + " Execute Time:" + DateTime.Now);
}, new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 3 });

public void TestAsync()
{
for (int i = 0; i < 10; i++)
{
abAsync.Post(i);
}
Console.WriteLine("Post finished");
}

01161530-c51b7a2e84254edebe2d85a63e3bf576.jpg

运行程序,我们看见,每3个数据几乎同时处理,并且他们的线程ID也是不一样的。

ActionBlock 也有自己的生命周期,所有继承 IDataflowBlock 的类型都有 Completion 属性和 Complete 方法。调用 Complete 方法是让 ActionBlock 停止接收数据,而 Completion 属性则是一个Task,是在 ActionBlock 处理完所有数据时候会执行的任务,我们可以使用 Completion.Wait() 方法来等待 ActionBlock 完成所有的任务,Completion 属性只有在设置了 Complete 方法后才会有效。

1
2
3
4
5
6
7
8
9
10
11
public void TestAsync()
{
for (int i = 0; i < 10; i++)
{
abAsync.Post(i);
}
abAsync.Complete();
Console.WriteLine("Post finished");
abAsync.Completion.Wait();
Console.WriteLine("Process finished");
}

01161531-151aebc85b31469894f4cae064eac264.jpg

TransformBlock

TransformBlock 是TDF提供的另一种 Block ,顾名思义它常常在数据流中充当数据转换处理的功能。在TransformBlock 内部维护了2个 Queue,一个 InputQueue,一个 OutputQueueInputQueue 存储输入的数据,而通过 Transform 处理以后的数据则放在 OutputQueueOutputQueue 就好像是一个 BufferBlock。最终我们可以通过 Receive 方法来阻塞的一个一个获取 OutputQueue 中的数据。 TransformBlockCompletion.Wait() 方法只有在 OutputQueue 中的数据为0的时候才会返回。

01161534-bdb94184e8814888b2fcc44c75c89a76.png

举个例子,我们有一组网址的URL,我们需要对每个URL下载它的HTML数据并存储。那我们通过如下的代码来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public TransformBlock<string, string> tbUrl = new TransformBlock<string, string>((url) =>
{
WebClient webClient = new WebClient();
return webClient.DownloadString(new Uri(url));
}

public void TestDownloadHTML()
{
tbUrl.Post("www.baidu.com");
tbUrl.Post("www.sina.com.cn");

string baiduHTML = tbUrl.Receive();
string sinaHTML = tbUrl.Receive();
}

当然,Post 操作和 Receive 操作可以在不同的线程中进行,Receive 操作同样也是阻塞操作,在 OutputQueue 中有可用的数据时,才会返回。

TransformManyBlock

TransformManyBlockTransformBlock 非常类似,关键的不同点是,TransformBlock 对应于一个输入数据只有一个输出数据,而 TransformManyBlock 可以有多个,及可以从 InputQueue 中取一个数据出来,然后放多个数据放入到 OutputQueue 中。

01161536-f582de1c67994e409b4f7835251b3c53.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TransformManyBlock<int, int> tmb = new TransformManyBlock<int, int>((i) => { return new int[] { i, i + 1 }; });

ActionBlock<int> ab = new ActionBlock<int>((i) => Console.WriteLine(i));

public void TestSync()
{
tmb.LinkTo(ab);

for (int i = 0; i < 4; i++)
{
tmb.Post(i);
}

Console.WriteLine("Finished post");
}

01161542-6d6c0dd2327a43f5982ad5405487b717.jpg

BroadcastBlock

BroadcastBlock 的作用不像 BufferBlock ,它是使命是让所有和它相联的目标 Block 都收到数据的副本,这点从它的命名上面就可以看出来了。还有一点不同的是,BroadcastBlock 并不保存数据,在每一个数据被发送到所有接收者以后,这条数据就会被后面最新的一条数据所覆盖。如没有目标 BlockBroadcastBlock 相连的话,数据将被丢弃。但 BroadcastBlock 总会保存最后一个数据,不管这个数据是不是被发出去过,如果有一个新的目标 Block 连上来,那么这个 Block 将收到这个最后一个数据。

01161543-bbfd0d43c8ad434f9af8dab2827c148c.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BroadcastBlock<int> bb = new BroadcastBlock<int>((i) => { return i; });

ActionBlock<int> displayBlock = new ActionBlock<int>((i) => Console.WriteLine("Displayed " + i));

ActionBlock<int> saveBlock = new ActionBlock<int>((i) => Console.WriteLine("Saved " + i));

ActionBlock<int> sendBlock = new ActionBlock<int>((i) => Console.WriteLine("Sent " + i));

public void TestSync()
{
bb.LinkTo(displayBlock);
bb.LinkTo(saveBlock);
bb.LinkTo(sendBlock);

for (int i = 0; i < 4; i++)
{
bb.Post(i);
}

Console.WriteLine("Post finished");
}

01161545-e9724f3e074644f5b5d756efe3ea4c02.jpg

如果我们在Post以后再添加连接Block的话,那些Block就只会收到最后一个数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void TestSync()
{
for (int i = 0; i < 4; i++)
{
bb.Post(i);
}

Thread.Sleep(5000);

bb.LinkTo(displayBlock);
bb.LinkTo(saveBlock);
bb.LinkTo(sendBlock);
Console.WriteLine("Post finished");
}

WriteOnceBlock

如果说 BufferBlock 是最基本的 Block ,那么 WriteOnceBock 则是最最简单的 Block 。它最多只能存储一个数据,一旦这个数据被发送出去以后,这个数据还是会留在Block中,但不会被删除或被新来的数据替换,同样所有的接收者都会收到这个数据的备份。

01161551-f163f3bb35014568ac90dd5d3175d7ee.png

和BroadcastBlock同样的代码,但是结果不一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WriteOnceBlock<int> bb = new WriteOnceBlock<int>((i) => { return i; });

ActionBlock<int> displayBlock = new ActionBlock<int>((i) => Console.WriteLine("Displayed " + i));

ActionBlock<int> saveBlock = new ActionBlock<int>((i) => Console.WriteLine("Saved " + i));

ActionBlock<int> sendBlock = new ActionBlock<int>((i) => Console.WriteLine("Sent " + i));

public void TestSync()
{
bb.LinkTo(displayBlock);
bb.LinkTo(saveBlock);
bb.LinkTo(sendBlock);
for (int i = 0; i < 4; i++)
{
bb.Post(i);
}

Console.WriteLine("Post finished");
}

01161558-66b3a94704ba426385f65e0f0d78433e.jpg

WriteOnceBock只会接收一次数据。而且始终保留那个数据。

同样使用Receive方法来获取数据也是一样的结果,获取到的都是第一个数据:

1
2
3
4
5
6
7
8
9
10
11
12
public void TestReceive()
{
for (int i = 0; i < 4; i++)
{
bb.Post(i);
}
Console.WriteLine("Post finished");

Console.WriteLine("1st Receive:" + bb.Receive());
Console.WriteLine("2nd Receive:" + bb.Receive());
Console.WriteLine("3rd Receive:" + bb.Receive());
}

01161605-e4ff0fa705c941059b3d0d6d0087eb25.jpg

BatchBlock

01161612-424f44c875a64999aa48d0a15a6d2f8a.png

BatchBlock 提供了能够把多个单个的数据组合起来处理的功能,如上图。应对有些需求需要固定多个数据才能处理的问题。在构造函数中需要制定多少个为一个 Batch,一旦它收到了那个数量的数据后,会打包放在它的 OutputQueue 中。当 BatchBlock 被调用 Complete 告知 Post 数据结束的时候,会把 InputQueue 中余下的数据打包放入 OutputQueue 中等待处理,而不管 InputQueue 中的数据量是不是满足构造函数的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
BatchBlock<int> bb = new BatchBlock<int>(3);

ActionBlock<int[]> ab = new ActionBlock<int[]>((i) =>
{
string s = string.Empty;

foreach (int m in i)
{
s += m + " ";
}
Console.WriteLine(s);
});

public void TestSync()
{
bb.LinkTo(ab);

for (int i = 0; i < 10; i++)
{
bb.Post(i);
}
bb.Complete();

Console.WriteLine("Finished post");
}

01161613-22d4c06138354a2db448bc5bfa3c9d6b.jpg

BatchBlock 执行数据有两种模式:贪婪模式和非贪婪模式。贪婪模式是默认的。贪婪模式是指任何Post到BatchBlockBatchBlock 都接收,并等待个数满了以后处理。非贪婪模式是指 BatchBlock 需要等到构造函数中设置的 BatchSize 个数的 Source 都向 BatchBlock 发数据,Post 数据的时候才会处理。不然都会留在Source的Queue中。也就是说 BatchBlock 可以使用在每次从N个 Source 那个收一个数据打包处理或从1个 Source 那里收N个数据打包处理。这里的Source是指其他的继承 ISourceBlock 的,用 LinkTo 连接到这个BatchBlockBlock

在另一个构造参数中 GroupingDataflowBlockOptions,可以通过设置 Greedy 属性来选择是否贪婪模式和 MaxNumberOfGroups 来设置最大产生Batch的数量,如果到达了这个数量,BatchBlock 将不会再接收数据。

JoinBlock

01161619-5805d12c170c4dd291897abe0fff07f6.png

JoinBlock 一看名字就知道是需要和两个或两个以上的 Source Block 相连接的。它的作用就是等待一个数据组合,这个组合需要的数据都到达了,它才会处理数据,并把这个组合作为一个 Tuple 传递给目标 Block。举个例子,如果定义了 JoinBlock<int, string> 类型,那么 JoinBlock 内部会有两个 ITargetBlock,一个接收int类型的数据,一个接收string类型的数据。那只有当两个 ITargetBlock 都收到各自的数据后,才会放到JoinBlock的OutputQueue 中,输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JoinBlock<int, string> jb = new JoinBlock<int, string>();
ActionBlock<Tuple<int, string>> ab = new ActionBlock<Tuple<int, string>>((i) =>
{
Console.WriteLine(i.Item1 + " " + i.Item2);
});

public void TestSync()
{
jb.LinkTo(ab);

for (int i = 0; i < 5; i++)
{
jb.Target1.Post(i);
}

for (int i = 5; i > 0; i--)
{
Thread.Sleep(1000);
jb.Target2.Post(i.ToString());
}

Console.WriteLine("Finished post");
}

01161623-de3f5cbecec145d88c3bc4ac70276026.jpg

BatchedJoinBlock

01161627-35dfb0f1f22448f6a68d78ea78e6012a.png

BatchedJoinBlock 一看就是 BacthBlockJoinBlick 的组合。JoinBlick 是组合目标队列的一个数据,而 BatchedJoinBlock 是组合目标队列的N个数据,当然这个N可以在构造函数中配置。如果我们定义的是BatchedJoinBlock<int, string>, 那么在最后的 OutputQueue 中存储的是 Tuple<IList<int>, IList<string>>,也就是说最后得到的数据是 Tuple<IList<int>, IList<string>>。它的行为是这样的,还是假设上文的定义,BatchedJoinBlock<int, string>, 构造 BatchSize 输入为3。那么在这个BatchedJoinBlock 种会有两个 ITargetBlock,会接收Post的数据。那什么时候会生成一个 Tuple<IList<int>,IList<string>>OutputQueue 中呢,测试下来并不是我们想的需要有3个int数据和3个string数据,而是只要2个 ITargetBlock 中的数据个数加起来等于3就可以了。3和0,2和1,1和2或0和3的组合都会生成 Tuple<IList<int>,IList<string>> OutputQueue 中。可以参看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
BatchedJoinBlock<int, string> bjb = new BatchedJoinBlock<int, string>(3);

ActionBlock<Tuple<IList<int>, IList<string>>> ab = new ActionBlock<Tuple<IList<int>, IList<string>>>((i) =>
{
Console.WriteLine("-----------------------------");

foreach (int m in i.Item1)
{
Console.WriteLine(m);
};

foreach (string s in i.Item2)
{
Console.WriteLine(s);
};
});

public void TestSync()
{
bjb.LinkTo(ab);

for (int i = 0; i < 5; i++)
{
bjb.Target1.Post(i);
}

for (int i = 5; i > 0; i--)
{
bjb.Target2.Post(i.ToString());
}

Console.WriteLine("Finished post");
}

01161631-152815cac25746c9a5a04d09bc7cf172.jpg

最后剩下的一个数据1,由于没有满3个,所以一直被保留在Target2中。

TDF中最有用的功能之一就是多个 Block 之间可以组合应用。ISourceBlock 可以连接 ITargetBlock ,一对一,一对多,或多对多。下面的例子就是一个 TransformBlock 和一个 ActionBlock 的组合。TransformBlock 用来把数据*2,并转换成字符串,然后把数据扔到 ActionBlock 中,而 ActionBlock 则用来最后的处理数据打印结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public ActionBlock<string> abSync = new ActionBlock<string>((i) =>
{
Thread.Sleep(1000);
Console.WriteLine(i + " ThreadId:" + Thread.CurrentThread.ManagedThreadId + " Execute Time:" + DateTime.Now);
}
);

public TransformBlock<int, string> tbSync = new TransformBlock<int, string>((i) =>
{
i = i * 2;
return i.ToString();
}
);

public void TestSync()
{
tbSync.LinkTo(abSync);

for (int i = 0; i < 10; i++)
{
tbSync.Post(i);
}
tbSync.Complete();
Console.WriteLine("Post finished");

tbSync.Completion.Wait();
Console.WriteLine("TransformBlock process finished");
}

01161632-f4dc0afbc9fa49b3a36336842ac52009.jpg

参考:

System.Threading.Tasks.Dataflow 命名空间

数据流(任务并行库)

什么是Hash

Hash也称散列、哈希,对应的英文都是Hash。Hash算法,也叫散列算法(Hash Algorithm),又称哈希算法,杂凑算法,是一种从任意文件中创造小的数字「指纹」的方法。

基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。活动开发中经常使用的MD5和SHA都是历史悠久的Hash算法。

echo md5(“这是一个测试文案”);

1
// 输出结果:2124968af757ed51e71e6abeac04f98d

在这个例子里,这是一个测试文案是原始值,2124968af757ed51e71e6abeac04f98d 就是经过hash算法得到的Hash值。整个Hash算法的过程就是把原始任意长度的值空间,映射成固定长度的值空间的过程。

Hash的特点

一个优秀的 hash 算法,将能实现:

  • 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
  • 逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
  • 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
  • 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

但在不同的使用场景中,如数据结构和安全领域里,其中对某一些特点会有所侧重。

Hash流行的算法

目前流行的 Hash 算法包括 MD5、SHA-1 和 SHA-2。

  • MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest 的缩写。其输出为 128 位。MD4 已证明不够安全。

  • MD5(RFC 1321)是 Rivest 于1991年对 MD4 的改进版本。它对输入仍以 512 位分组,其输出是 128 位。MD5 比 MD4 复杂,并且计算速度要慢一点,更安全一些。MD5 已被证明不具备”强抗碰撞性”。

  • SHA (Secure Hash Algorithm)是一个 Hash 函数族,由 NIST(National Institute of Standards and Technology)于 1993 年发布第一个算法。目前知名的 SHA-1 在 1995 年面世,它的输出为长度 160 位的 hash 值,因此抗穷举性更好。SHA-1 设计时基于和 MD4 相同原理,并且模仿了该算法。SHA-1 已被证明不具”强抗碰撞性”。

  • 为了提高安全性,NIST 还设计出了 SHA-224、SHA-256、SHA-384,和 SHA-512 算法(统称为 SHA-2),跟 SHA-1 算法原理类似。SHA-3 相关算法也已被提出。

注意:MD5在数年前就已经不被推荐作为应用中的散列算法方案,取代它的是SHA家族算法,也就是安全散列算法(Secure Hash Algorithm,缩写为SHA)。

git等版本控制工具使用SHA1等散列函数检查文件。

SHA家族算法以及SHA1碰撞

安全散列算法与MD5算法本质上的算法是类似的,但安全性要领先很多——这种领先型更多的表现在碰撞攻击的时间开销更大,当然相对应的计算时间也会慢一点。

SHA家族算法的种类很多,有SHA0、SHA1、SHA256、SHA384等等,它们的计算方式和计算速度都有差别。其中SHA1是现在用途最广泛的一种算法。包括GitHub在内的众多版本控制工具以及各种云同步服务都是用SHA1来区别文件,很多安全证书或是签名也使用SHA1来保证唯一性。长期以来,人们都认为SHA1是十分安全的,至少大家还没有找到一次碰撞案例。

但这一事实在2017年2月破灭了。CWI和Google的研究人员们成功找到了一例SHA1碰撞,而且很厉害的是,发生碰撞的是两个真实的、可阅读的PDF文件。这两个PDF文件内容不相同,但SHA1值完全一样。(对于这件事的影响范围及讨论,可参考知乎上的讨论:如何评价 2 月 23 日谷歌宣布实现了 SHA-1 碰撞?)

所以,对于一些大的商业机构来说, MD5 和 SHA1 已经不够安全,推荐至少使用 SHA2-256 算法。

murmurhash算法

murmur:低语,嘟囔

MurmurHash是一种非加密型哈希函数,适用于一般的哈希检索操作。由Austin Appleby在2008年发明,并且有多个变种,都已经发布到了公有领域(public domain)。与其它流行的哈希函数相比,对于规律性较强的key,MurmurHash的随机分布特征表现更良好。

特点:对于规律性较强的key,MurmurHash的随机分布特性表现更良好。与加密散列函数不同,它不是专门设计为难以被对手逆转,因此不适用于加密目的。

    Redis在实现字典时用到了两种不同的哈希算法,MurmurHash便是其中一种(另一种是djb),在Redis中应用十分广泛,包括数据库、集群、哈希键、阻塞操作等功能都用到了这个算法。发明算法的作者被邀到google工作,该算法最新版本是MurmurHash3,基于MurmurHash2改进了一些小瑕疵,使得速度更快,实现了32位(低延时)、128位HashKey,尤其对大块的数据,具有较高的平衡性与低碰撞率。

版本:

Murmurhash3
2018年的版本是Murmurhash3,它产生一个32位或128位散列值。 使用128位时,x86和x64版本不会生成相同的值,因为算法针对各自的平台进行了优化。

Murmurhash2
旧的Murmurhash2 产生一个32位或64位的值。 较慢版本的Murmurhash2可用于大端和对齐的机器。 Murmurhash2A变体添加了Merkle-Damgård构造,因此可以逐渐调用它。 有两种变体生成64位值; 针对64位处理器的Murmurhash64A和针对32位处理器的Murmurhash64B。 Murmurhash2-160生成160位散列,而Murmurhash1已过时。

一致性hash算法

一致性hash算法

参考:

什么是 hash?

Hash算法总结

一致性 hash 算法 (consistent hashing)

转载自:

一致性hash算法 - consistent hashing

consistent hashing 算法早在 1997 年就在论文 Consistent hashing and random trees 中被提出,目前在 cache 系统中应用越来越广泛;

基本场景

比如你有 N 个 cache 服务器(后面简称 cache ),那么如何将一个对象 object 映射到 N 个 cache 上呢,你很可能会采用类似下面的通用方法计算 object 的 hash 值,然后均匀的映射到到 N 个 cache ;

hash(object)%N

一切都运行正常,再考虑如下的两种情况;

1 一个 cache 服务器 m down 掉了(在实际应用中必须要考虑这种情况),这样所有映射到 cache m 的对象都会失效,怎么办,需要把 cache m 从 cache 中移除,这时候 cache 是 N-1 台,映射公式变成了 hash(object)%(N-1) ;

2 由于访问加重,需要添加 cache ,这时候 cache 是 N+1 台,映射公式变成了 hash(object)%(N+1) ;

1 和 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器;

再来考虑第三个问题,由于硬件能力越来越强,你可能想让后面添加的节点多做点活,显然上面的 hash 算法也做不到。

  有什么方法可以改变这个状况呢,这就是 consistent hashing…

hash 算法和单调性

   Hash 算法的一个衡量指标是单调性( Monotonicity ),定义如下:

  单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

容易看到,上面的简单 hash 算法 hash(object)%N 难以满足单调性要求。

consistent hashing 算法的原理

consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。

下面就来按照 5 个步骤简单讲讲 consistent hashing 算法的基本原理。

环形hash 空间

考虑通常的 hash 算法都是将 value 映射到一个 32 为的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环,如下面图 1 所示的那样。

circle.jfif
图1 环形 hash 空间

把对象映射到hash 空间

接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash 值 key 在环上的分布如图 2 所示。

hash(object1) = key1;

… …

hash(object4) = key4;

object.jfif
图2 4个对象的 key 值分布

把cache 映射到hash 空间

Consistent hashing 的基本思想就是将对象和 cache 都映射到同一个 hash 数值空间中,并且使用相同的 hash 算法。

假设当前有 A,B 和 C 共 3 台 cache ,那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash 值排列。

hash(cache A) = key A;

… …

hash(cache C) = key C;

cache.jfif
图3 cache 和对象的 key 值分布

说到这里,顺便提一下 cache 的 hash 计算,一般的方法可以使用 cache 机器的 IP 地址或者机器名作为 hash 输入。

把对象映射到cache

现在 cache 和对象都已经通过同一个 hash 算法映射到 hash 数值空间中了,接下来要考虑的就是如何将对象映射到 cache 上面了。

在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 cache ,那么就将该对象存储在这个 cache 上,因为对象和 cache 的 hash 值是固定的,因此这个 cache 必然是唯一和确定的。这样不就找到了对象和 cache 的映射方法了吗?!

依然继续上面的例子(参见图 3 ),那么根据上面的方法,对象 object1 将被存储到 cache A 上; object2 和 object3 对应到 cache C ; object4 对应到 cache B ;

考察cache 的变动

前面讲过,通过 hash 然后求余的方法带来的最大问题就在于不能满足单调性,当 cache 有所变动时, cache 会失效,进而对后台服务器造成巨大的冲击,现在就来分析分析 consistent hashing 算法。

移除 cache

考虑假设 cache B 挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿 cache B 逆时针遍历直到下一个 cache ( cache C )之间的对象,也即是本来映射到 cache B 上的那些对象。

因此这里仅需要变动对象 object4 ,将其重新映射到 cache C 上即可;参见图 4 。

remove.jfif
图4 Cache B 被移除后的 cache 映射

添加 cache

再考虑添加一台新的 cache D 的情况,假设在这个环形 hash 空间中, cache D 被映射在对象 object2 和 object3 之间。这时受影响的将仅是那些沿 cache D 逆时针遍历直到下一个 cache ( cache B )之间的对象(它们是也本来映射到 cache C 上对象的一部分),将这些对象重新映射到 cache D 上即可。

因此这里仅需要变动对象 object2 ,将其重新映射到 cache D 上;参见图 5 。

add.jfif
图5 添加 cache D 后的映射关系

虚拟节点

考量 Hash 算法的另一个指标是平衡性 (Balance) ,定义如下:

平衡性

平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

hash 算法并不是保证绝对的平衡,如果 cache 较少的话,对象并不能被均匀的映射到 cache 上,比如在上面的例子中,仅部署 cache A 和 cache C 的情况下,在 4 个对象中, cache A 仅存储了 object1 ,而 cache C 则存储了 object2 、 object3 和 object4 ;分布是很不均衡的。

为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:

“虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。

仍以仅部署 cache A 和 cache C 的情况为例,在图 4 中我们已经看到, cache 分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了 cache A ; cache C1, cache C2 代表了 cache C ;假设一种比较理想的情况,参见图 6 。

virtual.jfif
图 6 引入“虚拟节点”后的映射关系

此时,对象到“虚拟节点”的映射关系为:

objec1->cache A2 ; objec2->cache A1 ; objec3->cache C1 ; objec4->cache C2 ;

因此对象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了 cache C 上;平衡性有了很大提高。

引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如图 7 所示。

map.jfif
图7 查询对象所在 cache

“虚拟节点”的 hash 计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 cache A 的 IP 地址为 202.168.14.241 。

引入“虚拟节点”前,计算 cache A 的 hash 值:

Hash(“202.168.14.241”);

引入“虚拟节点”后,计算“虚拟节”点 cache A1 和 cache A2 的 hash 值:

Hash(“202.168.14.241#1”);  // cache A1

Hash(“202.168.14.241#2”);  // cache A2

C#代码实现

https://github.com/wsq003/consistent-hash

转载自:

.NET中间语言(IL)1

.NET中间语言(IL)2

.NET CLR 和 Java VM 都是堆栈式虚拟机(Stack-Based VM),也就是说,它们的指令集(Instruction Set)都是采用堆栈运算的方式:执行时的数据都是先放在堆栈中,再进行运算。 Java VM 有约 200 个指令(Instruction),每个指令都是 1 byte 的 opcode(操作码),后面接不等数目的参数;.NET CLR 有超过 220 个指令,但是有些指令使用相同的 opcode,所以 opcode 的 数目比指令数略少。 特别注意,.NET 的 opcode 长度并不固定,大部分的 opcode 长度是 1 byte,少部分是 2 byte。

本文章以一个实际的例子,让你了解堆栈式 VM 的运作原理,并对 .NET IL(Intermediate Language)有最基本的领略。

下面是一个简单的 C# 原始码:

1
2
3
4
5
6
7
8
9
10
11
using System;

public class Test {
public static void Main(String[] args) {
int i=1;
int j=2;
int k=3;
int answer = i+j+k;
Console.WriteLine("i+j+k="+answer);
}
}

将此原始码编译之后,可以得到一个 EXE 档案。 我们可以透过 ILDASM. EXE 来反组译 EXE 以观察 IL。 我将 Main() 的 IL 反组译条列如下,这里共有十八道 IL 指令,有的指令(例如 ldstr 与 box)后面需要接参数,有的指令(例如 ldc.i4.1 与 add)后面不需要接参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ldc.i4.1
stloc.0
ldc.i4.2
stloc.1
ldc.i4.3
stloc.2
ldloc.0
ldloc.1
add
ldloc.2
add
stloc.3
ldstr "i+j+k="
ldloc.3
box [mscorlib]System.Int32
call string [mscorlib]System.String::Concat(object, object)
call void [mscorlib]System.Console::WriteLine(string)
ret

此程序执行时,关键的内存有三种,分别是:

  • Managed Heap:这是动态配置(Dynamic Allocation)的内存,由 Garbage Collector(GC)在执行时自动管理,整个 Process 共享一个 Managed Heap。

  • Call Stack:这是由 .NET CLR 在执行时自动管理的内存,每个 Thread 都有自己专属的 Call Stack。 每呼叫一次 method,就会使得 Call Stack 上多了一个 Record Frame;呼叫完毕之后,此 Record Frame 会被丢弃。 一般来说,Record Frame 内纪录着 method 参数(Parameter)、返回地址(Return Address)、以及局部变量(Local Variable)。 Java VM 和 .NET CLR 都是使用 0, 1, 2… 编号的方式来识别局部变量。

  • Evaluation Stack:这是由 .NET CLR 在执行时自动管理的内存,每个 Thread 都有自己专属的 Evaluation Stack。 前面所谓的堆栈式虚拟机,指的就是这个堆栈。

后面有一连串的示意图,用来解说在执行时此三种内存的变化。 首先,在进入 Main() 之后,尚未执行任何指令之前,内存的状况如图 1 所示:

dd229210.il_f1(zh-tw,msdn.10).jpg
图 1

接着要执行第一道指令 ldc.i4.1。 此指令的意思是:在 Evaluation Stack 置入一个 4 byte 的常数,其值为 1。 执行完此道指令之后,内存的变化如图 2 所示:

dd229210.il_f2(zh-tw,msdn.10).jpg
图 2

接着要执行第二道指令 stloc.0。 此指令的意思是:从 Evaluation Stack 取出一个值,放到第 0 号变量(V0)中。 这里的第 0 号变量其实就是原始码中的 i。 执行完此道指令之后,内存的变化如图 3 所示:

dd229210.il_f3(zh-tw,msdn.10).jpg
图 3

后面的第三道指令和第五道指令雷同于第一道指令,且第四道指令和第六道指令雷同于第二道指令。 为了节省篇幅,我不在此一一赘述。 提醒大家第 1 号变量(V1)其实就是原始码中的 j,且第 2 号变量(V2)其实就是源码中的 k。 图 47 分别是执行完第三六道指令之后,内存的变化图:

dd229210.il_f4(zh-tw,msdn.10).jpg
图 4

dd229210.il_f5(zh-tw,msdn.10).jpg
图 5

dd229210.il_f6(zh-tw,msdn.10).jpg
图 6

dd229210.il_f7(zh-tw,msdn.10).jpg
图 7

接着要执行第七道指令 ldloc.0 以及第八道指令 ldloc.1:分别将 V0(也就是 i)和 V1(也就是 j)的值放到 Evaluation Stack,这是相加前的准备动作。 图 8 与图 9 分别是执行完第七、第八道指令之后,内存的变化图:

dd229210.il_f8(zh-tw,msdn.10).jpg
图 8

dd229210.il_f9(zh-tw,msdn.10).jpg
图 9

接着要执行第九道指令 add。 此指令的意思是:从 Evaluation Stack 取出两个值(也就是 i 和 j),相加之后将结果放回 Evaluation Stack 中。 执行完此道指令之后,内存的变化如图 10 所示:

dd229210.il_f10(zh-tw,msdn.10).jpg
图 10

接着要执行第十道指令 ldloc.2。 此指令的意思是:分别将 V2(也就是 k)的值放到 Evaluation Stack,这是相加前的准备动作。 执行完此道指令之后,内存的变化如图 11 所示:

dd229211.il_f11(zh-tw,msdn.10).jpg
图 11

接着要执行第十一道指令 add。 从 Evaluation Stack 取出两个值,相加之后将结果放回 Evaluation Stack 中,此为 i+j+k 的值。 执行完此道指令之后,内存的变化如图 12 所示:

dd229211.il_f12(zh-tw,msdn.10).jpg
图 12

接着要执行第十二道指令 stloc.3。 从 Evaluation Stack 取出一个值,放到第 3 号变量(V3)中。 这里的第3号变量其实就是原始码中的 answer。 执行完此道指令之后,内存的变化如图 13 所示:

dd229211.il_f13(zh-tw,msdn.10).jpg
图 13

接着要执行第十三道指令 ldstr “i+j+k=”。 此指令的意思是:将 “i+j+k=” 的 Reference 放进 Evaluation Stack。 执行完此道指令之后,内存的变化如图 14 所示:

dd229211.il_f14(zh-tw,msdn.10).jpg
图 14

接着要执行第十四道指令 ldloc.3。 将 V3 的值放进 Evaluation Stack。 执行完此道指令之后,内存的变化如图 15 所示:

dd229211.il_f15(zh-tw,msdn.10).jpg
图 15

接着要执行第十五道指令 box [mscorlib]System.Int32。 此指令的意思是:从 Evaluation Stack 中取出一个值,将此 Value Type 包装(box)成为 Reference Type。 执行完此道指令之后,内存的变化如图 16 所示:

dd229211.il_f16(zh-tw,msdn.10).jpg
图 16

接着要执行第十六道指令 call string [mscorlib] System.String::Concat(object, object)。 此指令的意思是:从 Evaluation Stack 中取出两个值,此二值皆为 Reference Type,下面的值当作第一个参数,上面的值当作第二个参数,呼叫 mscorlib.dll 所提供的 System.String.Concat() method 来将此二参数进行字符串接合(String Concatenation),将接合出来的新字符串放在 Managed Heap,将其 Reference 放进 Evaluation Stack。 值得注意的是:由于 System.String.Concat() 是 static method,所以此处使用的指令是 call,而非 callvirt(呼叫虚拟)。 执行完此道指令之后,内存的变化如图 17 所示:

dd229211.il_f17(zh-tw,msdn.10).jpg
图 17

请注意:此时 Managed Heap 中的 Int32(6) 以及 String(“i+j+k=”) 已经不再被参考到,所以变成垃圾,等待 GC 的回收。

接着要执行第十七道指令 call void [mscorlib] System.Console::WriteLine(string)。 此指令的意思是:从 Evaluation Stack 中取出一个值,此值为 Reference Type,将此值当作参数,呼叫 mscorlib.dll 所提供的 System.Console.WriteLine() method 来将此字符串显示在 Console 窗口上。 System.Console.WriteLine() 也是 static method。 执行完此道指令之后,内存的变化如图 18 所示:

dd229211.il_f18(zh-tw,msdn.10).jpg
图 18

接着要执行第十八道指令 ret。 此指令的意思是:结束此次呼叫(也就是 Main 的呼叫)。 此时会检查 Evaluation Stack 内剩下的数据,由于 Main() 宣告不需要传出值(void),所以 Evaluation Stack 内必须是空的,本范例符合这样的情况,所以此时可以顺利结束此次呼叫。 而 Main 的呼叫一结束,程序也随之结束。 执行完此道指令之后(且在程序结束前),内存的变化如图 19 所示:

dd229211.il_f19(zh-tw,msdn.10).jpg
图 19

透过此范例,读者应该可以对于 IL 有最基本的认识。 对 IL 感兴趣的读者应该自行阅读 Serge Lidin 所著的《Inside Microsoft .NET IL Assembler》(Microsoft Press 出版)。 我认为:熟知 IL 每道指令的作用,是 .NET 程序员必备的知识。.NET 程序员可以不会用 IL Assembly 写程序,但是至少要看得懂 ILDASM 反组译出来的 IL 组合码。

codeMaid

CodeMaid快速整理代码文件,规范你的代码,提高代码阅读体验。

Markdown Editor

Markdown Editor一个在visual studio 中的markdown工具
功能齐全的Markdown编辑器,具有实时预览和语法突出显示功能。支持GitHub风格的Markdown。

JSON Viewer

JSON Viewer用于显示和处理JSON数据。

Output enhancer

Output enhancer将output窗口的输出的文字添加样式值得赶紧试试的插件,给输出内容着色。

参考:

收藏!推荐12个超实用的Visual Studio插件

Wow64

Wow64,全称是32bit Windows On 64bit Windows(64位Windows上的32位Windows)。

Windows系统的主要系统文件都是放在一个叫做System32的文件夹中的。为了能同时放下两套系统文件,Windows会在64位的系统上,增加了一个文件夹,叫SysWow64。

总结:

1
2
3
SysWow64文件夹,是64位Windows,用来存放32位Windows系统文件的地方,而System32文件夹,是用来存放64位程序文件的地方。
32位程序加载System32文件夹中的dll时,操作系统会自动映射到SysWow64文件夹中的对应的文件。
只要32位程序访问System32文件夹,无论是加载dll,还是读取文本信息,都会被映射到SysWow64文件夹

参考:

什么是SysWow64

dll文件32位64位检测工具以及Windows文件夹SysWow64的坑

【结果很简单,过程很艰辛】记阿里云Ons消息队列服务.NET接口填坑过程

玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理

Windows Sysinternals

live.sysinternals.com

事务定义

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种 “要么什么都不做,要么做全套(All or Nothing)” 机制。

数据库事务

一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:

1,为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
2,当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

数据库事务拥有以下四个特性,ACID 特性。

  • A 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • C 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • I 隔离性(Isolation)或独立性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

  • D 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的 分布式系统 的不同节点之上。

在应用程序只部署在一台计算机,数据库只部署在一台计算机的情况下,事务的ACID四个特性很容易全部满足。

但是单机的处理能力很容易达到上限,此时必须使用分布式系统。在分布式环境下,应用程序可能部署在多台计算机,并且可能有多个不同的应用程序参与到同一个事务中;数据库也可能部署在多台计算机,并且多个不同的数据库可能会参与到同一个事务中。一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务的基础

CAP

CAP定理,又被叫作布鲁尔定理。

  • C (一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。

  • A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。

  • P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。

对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。

CAP理论中是忽略网络延迟,也就是当事务提交时,从节点A复制到节点B,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。同时CAP中选择两个,比如你选择了CP,并不是叫你放弃A。因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA。就算分区出现了你也要为后来的A做准备,比如通过一些日志的手段,是其他机器回复至可用。

BASE

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写,是对CAP中AP的一个扩展。

  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  • 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
  • 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务方案

基于XA协议的2PC(2阶段提交)

XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:

QQ截图20191221173353.png

两阶段提交,是实现分布式事务的成熟方案。

  • 第一阶段是表决阶段,事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
  • 第二阶段是执行阶段,事务协调器要求每个数据库提交数据,或者回滚数据。

优点: 尽量保证了数据的强一致,实现成本较低,XA目前在商业数据库支持的比较理想,在MySQL(MySQL5.5开始支持)数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。

缺点:

  • 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
  • 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,XA协议比较简单,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。XA协议已在业界成熟运行数十年,但目前它在互联网海量流量的应用场景中,吞吐量这个瓶颈变得十分致命,因此很少被用到。

两阶段提交可以满足ACID,但代价是吞吐量。例如,数据库需要频繁地对资源上锁等等。而且更致命的是,资源被锁住的时间相对较长—-在第一阶段即需要上锁,第二阶段才能解锁,依赖于所有分支的最慢者—-这期间没有任何人可以对该资源进行修改。

TCC

TCC(Try、Confirm、Cancel)是两阶段提交的一个变种。TCC提供了一个框架,需要应用程序按照该框架编程,将业务逻辑的每个分支都分为Try、Confirm、Cancel三个操作集。TCC让应用程序自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。

TCC事务机制相比于上面介绍的XA,解决了其几个缺点:

  • 1.解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 2.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 3.数据一致性,有了补偿机制之后,由业务活动管理器控制一致性

流程图:

1010726-20191031060901955-1099206419.png

  • Try阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性)

  • Confirm阶段:确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具备幂等设计,Confirm失败后需要进行重试。

  • Cancel阶段:取消执行,释放Try阶段预留的业务资源

Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。

以一个典型的淘宝订单为例,按照TCC框架,应用需要在Try阶段将商品的库存减去,将买家支付宝账户中的相应金额扣掉,在临时表中记录下商品的数量,订单的金额等信息;另外再编写Confirm的逻辑,即在临时表中删除相关记录,生成订单,告知CRM、物流等系统,等等;以及Cancel逻辑,即恢复库存和买家账户金额,删除临时表相关记录。

很明显,最终一致性部分牺牲了ACID中的C和I,但它带来了可观的收益:资源不再需要长时间上锁,极大地提高了吞吐量。

最终一致性在互联网应用场景中被广泛用做吞吐量和ACID的妥协点。

在前面TCC的例子。在这个流程中,商品库存和买家余额都没有被锁住,因此可以得到很高的吞吐量。但在交易进行中,商品库存和买家余额的变化就已经被外界感知到,而物流系统却可能还没有相应的记录,此时数据是不一致的,但最终(无论是Confirm阶段结束后,还是Cancel阶段结束后)它们会一致。

TXC

TXC(Taobao Transaction Constructor)是阿里巴巴的一个分布式事务中间件,它可以通过极少的代码侵入,实现分布式事务。

在大部分情况下,应用只需要引入TXC Client的jar包,进行几项简单配置,以及以行计的代码改造,即可轻松保证分布式数据一致性。

TXC同时提供了丰富的编程和配置策略,以适应各种长尾的应用需求。

实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。

TXC的目标应用场景是:解决在分布式应用中,多条数据库记录被修改而可能带来的一致性问题;该分布式应用可以接受最终一致性;该应用的事务改造对工作量有较严格的限制。

LCN

原理:LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。

参考:TX-LCN

本地消息表 (异步确保)

本地消息表是国外的 ebay 搞出来的一套方案,如图所示:

1010726-20191031061916148-594757786.png

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

我们首先需要在本地数据新建一张本地消息表,然后我们必须还要一个MQ(不一定是mq,但必须是类似的中间件)。

这个表应该包括这些字段: id, biz_id, biz_type, msg, msg_result, msg_desc,atime,try_count。分别表示uuid,业务id,业务类型,消息内容,消息结果(成功或失败),消息描述,创建时间,重试次数, 其中biz_id,msg_desc字段是可选的。

实现思路为:

  • A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
  • 接着 A 系统将这个消息发送到 MQ 中去;
  • B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息;
  • B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
  • 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
  • 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。

  
这个方案严重依赖于数据库的消息表来管理事务,这样在高并发的情况下难以扩展,同时要在数据库中额外添加一个与实际业务无关的消息表来实现分布式事务,繁琐。

本地消息表核心是把大事务转变为小事务。

例如,用100元去买一瓶水的例子。

  • 1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。
  • 2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。
  • 3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。
  • 4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。

MQ事务

直接基于 MQ 来实现事务,不再用本地的消息表。所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。

在RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部。

20170320083222287.png

基本流程如下:

  • 1、A系统向消息中间件发送一条预备消息
  • 2、消息中间件保存预备消息并返回成功
  • 3、A执行本地事务
  • 4、A发送提交消息给消息中间件

通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:

  • 步骤一出错,则整个事务失败,不会执行A的本地操作
  • 步骤二出错,则整个事务失败,不会执行A的本地操作
  • 步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息
  • 步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务

基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。

20170320083228100.png

上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这个就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失。

164d77389afdfd6b.png

Saga事务

SAGA可以看做一个异步的、利用队列实现的补偿事务。

其适用于无需马上返回业务发起方最终状态的场景,例如:你的请求已提交,请稍后查询或留意通知 之类。

Saga是30年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

saga的提出,最早是为了解决可能会长时间运行的分布式事务(long-running process)的问题。所谓long-running的分布式事务,是指那些企业业务流程,需要跨应用、跨企业来完成某个事务,甚至在事务流程中还需要有手工操作的参与,这类事务的完成时间可能以分计,以小时计,甚至可能以天计。这类事务如果按照事务的ACID的要求去设计,势必造成系统的可用性大大的降低。试想一个由两台服务器一起参与的事务,服务器A发起事务,服务器B参与事务,B的事务需要人工参与,所以处理时间可能很长。如果按照ACID的原则,要保持事务的隔离性、一致性,服务器A中发起的事务中使用到的事务资源将会被锁定,不允许其他应用访问到事务过程中的中间结果,直到整个事务被提交或者回滚。这就造成事务A中的资源被长时间锁定,系统的可用性将不可接受。

而saga,则是一种基于补偿的消息驱动的用于解决long-running process的一种解决方案。目标是为了在确保系统高可用的前提下尽量确保数据的一致性。还是上面的例子,如果用saga来实现,那就是这样的流程:服务器A的事务先执行,如果执行顺利,那么事务A就先行提交;如果提交成功,那么就开始执行事务B,如果事务B也执行顺利,则事务B也提交,整个事务就算完成。但是如果事务B执行失败,那事务B本身需要回滚,这时因为事务A已经提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态。这样的基于消息驱动的实现思路,就是saga。我们可以看出,saga是牺牲了数据的强一致性,仅仅实现了最终一致性,但是提高了系统整体的可用性。

Saga的组成:

每个Saga由一系列sub-transaction Ti 组成
每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果,这里的每个T,都是一个本地事务。
可以看到,和TCC相比,Saga没有“预留 try”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

  • T1, T2, T3, …, Tn
  • T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n

Saga定义了两种恢复策略:

  • 向后恢复,即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。
  • 向前恢复,适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。

这里要注意的是,在saga模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。

拿100元买一瓶水的例子来说,这里定义:

T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水
C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水

我们一次进行T1,T2,T3如果发生问题,就执行发生问题的C操作的反向。

上面说到的隔离性的问题会出现在,如果执行到T3这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题

可以看见saga模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。也可以在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。

具体实例:可以参考华为的 servicecomb 。

参考:

saga中的saga(A Saga on Sagas)

如何选择分布式事务形态(TCC,SAGA,2PC,补偿,基于消息最终一致性等等)

再有人问你分布式事务,把这篇扔给他

TX-LCN分布式事务Demo实战

TXC分布式事务简介

分布式事务的解决方案

GTS让分布式事务简单高效

微服务架构下分布式事务解决方案——阿里GTS

ENode 1.0 - Saga的思想与实现

结构vs类,栈vs堆

  在托管.NET世界中,您应该关注两个内存区域:栈和堆。堆是在其中分配所有对象的托管内存(无论何时调用 new YourClass)。重要的方面是它与任何线程都不相关或没有连接。一个线程可以分配一个对象并将其传递给另一线程。只要垃圾收集器没有收集该对象,该对象就会在堆上。

  栈以不同的方式工作。它被分配给特定的线程(每个线程的堆栈大小有限)。栈具有框架,可用于分配一些内存,例如用于在调用过程中创建并分配给变量的结构值,例如,通过调用Guid.NewGuid()

  堆分配的内存可用于在线程之间传输某些数据。由于栈是分配给特定线程的,因此不能用于传输某些数据。另一方面,如果您要分配少量内存,则堆栈将更快一些,因为它是线程本地的,并且不会陷入启动垃圾收集器等缓慢的路径中。

Task

Task 在 .NET Framework 4 中 System.Threading.Tasks 命名空间被引入,在 C#5 以及 .NET Framwrok 4.5 中的异步编程模式引入了 async/await

Task 的核心就是 promise,它表示最终完成的一些操作。当初始化一个操作时,返回一个 Task,当操作完成时,Task 会完成。

特点:

Task 很灵活,并且有很多好处。例如你可以通过多个消费者并行等待多次。你可以存储一个到字典中,以便后面的任意数量的消费者等待,它允许为异步结果像缓存一样使用。如果场景需要,你可以阻塞一个等待完成。并且你可以在这些任务上编写和使用各种操作(就像组合器),例如 WhenAny 操作,它可以异步等待第一个操作完成。

然而,这种灵活性对于大多数情况下是不需要的:仅仅只是调用异步操作并且等待结果:

1
2
TResult result = await SomeOperationAsync();
UseResult(result);

副作用:

Task 是一个类。作为一个类,就是说任意操作创建一个 Task 都会分配一个对象,越来越多的对象都会被分配,所以 GC 操作也会越来越频繁,也就会消耗越来越多的资源,本来它应该是去做其他事的。

ValueTask

.NET CORE 2.0 引入了一个新类型 ValueTask<TResult>,在 System.Threading.Tasks.Extensions 命名空间中。ValueTask<TResult> 作为结构体引入的,它是 TResultTask<TResult> 包装器。

实例可以等待或 Task<TResult> 使用 AsTask 转换为。 ValueTask<TResult> 实例仅可等待一次, 并且在实例完成之前, 使用者可能不会调用 GetAwaiter()

永远不应对 ValueTask<TResult>实例执行以下操作:

1
2
3
4
等待实例多次。
多次AsTask调用。
使用/操作未完成/使用多次 .Result.GetAwaiter().GetResult()
使用多种方法来使用此实例。

如果执行上述任一操作, 则结果是不确定的。

同步完成 (synchronous completion)

它能从异步方法返回并且如果这个方法同步成功完成,不需要分配任何内存:只简单的初始化这个 ValueTask<TResult> 结构体,它返回 TResult

只有当该方法异步完成时,Task<TResult> 才需要进行分配, 创建包装该 ValueTask <TResult> 的实例(以最小化 ValueTask <TResult>的大小,并用于优化成功路径,异步方法未处理的异常故障也将分配一个 Task<TResult>,所以 ValueTask<TResult> 可以简单地包裹该 Task<TResult>, 而并不是 随身携带附加字段以存储异常)。

异步完成 (asynchronous completion)

在.NET Core 2.1,引入 IValueTaskSource<TResult> 支持池化和重用。

.NET Core 2.1为 ValueTask 添加了另一个构造函数:

1
2
3
4
public readonly struct ValueTask<TResult>
{
public ValueTask(IValueTaskSource<TResult> source, short token)
}

token:令牌参数,该值确保将 IValueTaskSource 返回到池后不会使用 ValueTask。有效地,此令牌值将传递到 IValueTaskSource 实现的每个方法,该方法能够检查调用者是否滥用 ValueTask

1
2
3
4
5
6
public interface IValueTaskSource<out TResult>
{
ValueTaskSourceStatus GetStatus(short token);
void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags);
TResult GetResult(short token);
}

参与:

ValueTask 结构

Understanding the Whys, Whats, and Whens of ValueTask

Task, Async Await, ValueTask, IValueTaskSource and how to keep your sanity in modern .NET world

传统三层无状态架构的缺陷

三层架构包括表示层、业务逻辑层(中间层)、数据访问层(存储层)

  传统的三层体系结构具有无状态前端、无状态的中间层和存储层,由于存储层的延迟和吞吐量限制,其可扩展性有限(存储层常常会成为系统的瓶颈),因此必须针对每个请求进行咨询。这就意味着整套系统也会因为存储层的限制而变得低效。通常的做法是在中间层与存储层中间加一层缓存逻辑出来,以提升系统性能,但是很快就会遇到存储层与缓存层的数据一致性问题,为了防止缓存项的并发更新导致不一致,应用程序或缓存管理器必须实现并发控制协议。这无疑为开发人员和运维人员增加了额外的工作量。

Actor模型和CSP模型的出现,解决了传统三层架构的缺陷。

Actor模型

Actor模型简介

  actor模型最早被1973年的“A Universal Modular ACTOR Formalism for Artificial Intelligence”引入,作为人工智能研究中的一种计算方法。它的最初目标是用一种能安全地跨工作站并发分布的通信方式来建模并行计算。这篇文章几乎没有假设实现细节,而是定义了一种高级消息传递通信模型。actor模型的四个主要变体:经典actor模型、基于进程的actor模型、通信事件循环模型、以及活动对象模型。

  有些健壮的工业级actor系统正被用于赋能大规模可伸缩分布式系统,主要案例:例如Akka被用于服务PayPal的十亿级的事务,Erlang被用于为WhatsApp的上亿用户发送消息,而Orleans被用于服务Halo4的数百万玩家。

  Actor模型作为一种用于处理并发计算的数学概念模型,它定义了系统组件该如何表现,以及如何与其他组件交互的一些通用规则。它将Actor对象用作并发计算的基本单元,它也是一种重要的软件设计思想,它在架构、设计、实现以及组件之间的消息传递方面有着非常好的应用,也更好的发挥了多核计算机的潜力。该思想与我们经常用到的面向对象语言的思想很类似:一个对象接受消息(方法调用)然后依据消息数据进行计算。主要的不同点在于,actor之间完全独立,没有共享内存。值得注意的是,一个actor维持的私有状态从来不会被其他actor改变,而只会通过接受消息,自己改变自己。通过创建新的Actor对象,可以在计算操作的生命周期中以抽象方式提高系统的分布性。

  Actor模型允许建立一个有状态的中间层,其内存级的读写性能和特定于相关领域的业务实体行为,确保了系统的高性能以及数据的一致性。Actor模型天然的拥有着面向对象的程序设计功能,在实践中我们应该把主要精力放到组件之间的消息传递,而不是对象的属性和内部行为。

一个actor并不能成为actor模型。actor是系统的组件,actor模型认为系统所有的组件都是actor,都有地址,actor通过这些地址进行异步通信。

260879-325a351b6a349d3c.png

经典actor模型

经典actor模型,保持了在隔离的计算单元和状态单元之间通过消息来做异步通信的思想,主要特点如下:

1
2
3
4
5
6
7
异步通信:使消息就像从一个Actor对象传输到了另一个Actor对象
状态机:Actor模型支持有限状态机
独立性:多个Actor对象之间不会共享状态
无锁的并发处理:由于Actor不会共享状态,且在同一时刻只处理一条消息,因而无需使用锁策略,这极大的提高了Actor系统的性能
并行性:当顶级Actor将任务分拆后发送给多个下级Actor后,可以使用Actor模型的并行处理方式
位置透明:可以使用抽象引用表示Actor对象的地址
Future/Promise对象:这是对异步操作的发送与接收方式,以表示异步操作的完成结果

当一个actor接收消息时,通常做以下三件事情:

1
2
3
create:用一种行为的描述和一组参数(包括其它actor)来创建一个actor。
send:向其它actor发送消息。
become:将一个actor的当前行为替换为另一种行为。(设置对下一条消息做出的回应方式)

  在经典actor模型中,状态变化均由become操作来聚合完成。每当actor处理一条消息时,它会计算出一个行为,来响应它期望处理的下一种消息类型。become操作的参数是一个有名字的continuation b,表示actor应该被更新为的行为,以及它应该传递给 b 的状态。(continuation:延续)

可以简单理解成,在一个actor维持私有状态之前,第三条基本上意味着定义了接收下条信息之前actor的状态。或者说,actor是如何改变自身状态的。

假设一个actor是一个计算器,初始状态为0。当这个actor接收到“+1”的消息后,它将指定接收下一个消息时,actor的状态为1,而不是改变原始的状态。

Orleans对Actor的应用

  Actor平台(例如Erlang和Akka)在简化分布式系统编程方面向前迈了一步。但是,由于提供的抽象和系统服务的水平相对较低,它们仍然使开发人员承担着许多分布式系统的复杂性。主要包括开发用于管理Actor的生命周期,处理分布式簇,处理Actor的失败和恢复,放置Actor以及由此产生的管理分布式资源的应用程序代码。要为应用程序中的这些问题构建正确的解决方案,这就开发人员的要求就非常高了,必须是分布式系统专家级别的。

  为了减少这些问题的发生,Orleans框架引入了虚拟Actor的新型抽象,它解决了许多复杂的分布式系统问题,例如可靠性和分布式资源管理,从而使开发人员摆脱了那些麻烦。同时,Orleans运行时使应用程序能够获得高性能,可靠性和可伸缩性。

Orleans对Actor的实现特点:

  • 1,Orleans Actor无处不在:无法明确创建或销毁它。它的生命周期超越了其任何内存对象的生命周期,因此也超越了任何特定服务器的生命周期。
  • 2,Orleans Actor会自动实例化:如果没有Actor的内存实例,则发送给Actor的消息会促使在可用服务器上创建一个新实例。作为运行时资源管理的一部分,将自动回收未使用的Actor实例。
  • 3,Actor永远不会失败:如果服务器崩溃了,下一条发送给运行在故障服务器上的Actor的消息将会促使Orleans自动在另一台服务器上重新实例化该Actor ,从而无需应用程序来监督和显式重新创建已经挂掉的Actor。
  • 4,Actor实例的位置对于应用程序代码是透明的,从而大大简化了编程。
  • 5,Orleans可以自动创建同一个无状态Actor的多个实例,从而无缝扩展热门Actor。

  虚拟Actor的引入,相当于为开发者提供了一个虚拟的内存空间,使开发人员可以调用系统中的任何角色,无论它是否存在于内存中。虚拟化依赖于从虚拟角色映射到当前运行的物理实例的间接寻址。运行时通过一个分布式目录支持间接寻址,该目录将Actor标识映射到其当前物理位置。Orleans通过使用该映射的本地缓存来最小化间接寻址的运行时开销。这个策略被证明是非常有效的。在微软的生产服务中,缓存命中率通常远远超过90%。

533598-20190922155518083-577463983.jpg

CSP模型

  CSP是 Communicating Sequential Processes(通信顺序进程) 的简称。在CSP中,多了一个角色Channel,过程(比如goroutine,Worker1)与过程(Worker2)之间不直接通信,而是通过Channle进行通信。

1
Worker1 --> Channel --> Worker2

CSP模型特点:

  • Channel是过程的中间媒介,Worker1想要跟Worker2发信息时,直接把信息放到Channel里(在程序中其实就是一块内存),然后Worker2在方便的时候到Channel里获取。
  • Worker1和Worker2之间可以存在很多个Channel;在Golang中每个Channel定义不同的数据类型,即发送不同类型的消息的时候会用到多个不同的Channel。

Go语言的CSP模型是由协程Goroutine与通道Channel实现:

  • Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。
  • 通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。

channel.png

Actor模型和CSP模型比较

Actor之间直接通讯,而CSP是通过Channel通讯,在耦合度上两者是有区别的,后者更加松耦合。

  同时,它们都是描述独立的流程通过消息传递进行通信。主要的区别在于:在CSP消息交换是同步的(即两个流程的执行”接触点”的,在此他们交换消息),而Actor模型是完全解耦的,可以在任意的时间将消息发送给任何未经证实的接受者。由于Actor享有更大的相互独立,因为他可以根据自己的状态选择处理哪个传入消息。自主性更大些。

  在Go语言中为了不堵塞流程,程序员必须检查不同的传入消息,以便预见确保正确的顺序。CSP好处是Channel不需要缓冲消息,而Actor理论上需要一个无限大小的邮箱作为消息缓冲。

参考:

[翻译]消息传递与actor模型

Orleans – Virtual Actors

并发模型:Actors与CSP

.NET分布式大规模计算利器-Orleans(一)

十分钟了解Actor模型

Actor模型和CSP模型的区别

十六进制

十六进制的表示:
C语言、Shell、Python语言及其他相近的语言使用字首“0x”,例如“0x5A3”。开头的“0”令解析器更易辨认数,而“x”则代表十六进制。在“0x”中的“x”可以大写或小写。

16进制就有16个数,015,用二进制表示15的方法就是1111,从而可以推断出,16进制用2进制可以表现成00001111,顾名思义,也就是每四个为一位。最高位不够可用零代替。

一个字节包含8个二进制位,一个十六进制可表示4个二进制位,所以,一个字节可以由2个十六进制表示。即,一个byte 对应两位十六进制位。

十六进制转换

字符串转为16进制字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static string StringToHexString(string s, Encoding encode)
{
byte[] b = encode.GetBytes(s);//按照指定编码将string编程字节数组
string result = string.Empty;
for (int i = 0; i < b.Length; i++)//逐字节变为16进制字符
{
result += Convert.ToString(b[i], 16);
}
return result;
}

// 使用
System.Console.WriteLine(StringToHexString("严", System.Text.Encoding.UTF8));
System.Console.WriteLine(BitConverter.ToString(Encoding.UTF8.GetBytes("严")));

QQ截图20191116174552.png

16进制字符串转为字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static string HexStringToString(string hs, Encoding encode)
{
string strTemp = "";
byte[] b = new byte[hs.Length / 2];
for (int i = 0; i < hs.Length / 2; i++)
{
strTemp = hs.Substring(i * 2, 2);
b[i] = Convert.ToByte(strTemp, 16);
}
//按照指定编码将字节数组变为字符串
return encode.GetString(b);
}

// 使用
string hexstring = StringToHexString("严", System.Text.Encoding.UTF8);
string content = HexStringToString(hexstring, System.Text.Encoding.UTF8);

byte[]转为16进制字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static string ByteToHexStr(byte[] bytes)
{
string returnStr = "";
if (bytes != null)
{
for (int i = 0; i < bytes.Length; i++)
{
returnStr += bytes[i].ToString("X2");
}
}
return returnStr;
}

// 使用

var byteStr = StringToHexString("严", Encoding.UTF8);
Console.WriteLine(byteStr);// 输出 e4b8a5

var bytes = Encoding.UTF8.GetBytes("严");
var str = Encoding.UTF8.GetString(bytes);// 输出 严

var str1 = ByteToHexStr(bytes); // 输出 E4B8A5

var bytes1 = StrToToHexByte(str1);
var bytes2 = StrToToHexByte(byteStr);

16进制的字符串转为byte[]

1
2
3
4
5
6
7
8
9
10
public static byte[] StrToToHexByte(string hexString)
{
hexString = hexString.Replace(" ", "");
if ((hexString.Length % 2) != 0)
hexString += " ";
byte[] returnBytes = new byte[hexString.Length / 2];
for (int i = 0; i < returnBytes.Length; i++)
returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
return returnBytes;
}

参考:

C#数字、16进制字符串和字节之间互转

UltraEdit

文本编辑器,可以查看16进制字节;

ef core power tool

生成DbContext;反向生成 POCO 类等;

emeditor

大文本文件编辑器,方便查找;

ListDLLs

检测当前运行的进程已经加载的dll文件

WinHex

是在Windows下执行的十六进制编辑软件,此软件功能很强大,有完好的分区管理功能和文件管理功能。能自己主动分析分区链和文件簇链。能对硬盘进行不同方式不同程度的备份。甚至克隆整个硬盘;它可以编辑不论什么一种文件类型的二进制内容(用十六进制显示)其磁盘编辑器可以编辑物理磁盘或逻辑磁盘的随意扇区。是手工恢复数据的首选工具软件。

PEid

PEiD(PE Identifier)是一款著名的查壳工具,其功能强大,几乎可以侦测出所有的壳,其数量已超过470 种PE 文档 的加壳类型和签名。

cmder

命令行增强工具

VLC mediaplayer

视频流播放软件

ASCII 码

每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。
ASCII,美国信息交换标准代码,是基于拉丁字母的一套电脑编码系统。

QQ截图20191116140828.png

参考:ASCII对照表

Unicode 与 UCS

Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是”Universal Multiple-Octet Coded Character Set”,简称为UCS。UCS可以看作是”Unicode Character Set”的缩写。

在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。

Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字严。

Unicode符号对应表

UTF编码

UTF-8就是以8位为单元对UCS进行编码。从UCS-2到UTF-8的编码方式如下:

UCS-2编码(16进制) UTF-8 字节流(二进制)
0000 - 007F 0xxxxxxx
0080 - 07FF 110xxxxx 10xxxxxx
0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx

BOM

BOM —— Byte Order Mark,中文名译作“字节顺序标记”。在这里找到一段关于 BOM 的说明:

在UCS 编码中有一个叫做 “Zero Width No-Break Space” ,中文译名作“零宽无间断间隔”的字符,它的编码是 FEFF。而 FFFE 在 UCS 中是不存在的字符,所以不应该出现在实际传输中。

UCS 规范建议我们在传输字节流前,先传输字符 “Zero Width No-Break Space”。这样如果接收者收到 FEFF,就表明这个字节流是 Big-Endian 的;如果收到FFFE,就表明这个字节流是 Little- Endian 的。因此字符 “Zero Width No-Break Space” (“零宽无间断间隔”)又被称作 BOM。

UTF-8 不需要 BOM 来表明字节顺序,但可以用 BOM 来表明编码方式。字符 “Zero Width No-Break Space” 的 UTF-8 编码是 EF BB BF。所以如果接收者收到以 EF BB BF 开头的字节流,就知道这是 UTF-8 编码了。Windows 就是使用 BOM 来标记文本文件的编码方式的。

UTF-8

UTF-8的一个特别的好处是它与ISO-8859-1完全兼容。UTF是 UCS Transformation Format 的缩写。

UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。UTF-8 是 Unicode 的实现方式之一。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的位。

QQ截图20191116141405.png

下面,还是以汉字严为例,演示如何实现 UTF-8 编码。

严的 Unicode 是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5。

UCS-2、UCS-4、BMP

CS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。下面让我们做一些简单的数学游戏:

UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。

UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行 (rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。

group 0的plane 0被称作 Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。

将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。

Unicode 与 UTF-8 之间的转换

Windows平台,有一个最简单的转化方法,就是使用内置的记事本小程序notepad.exe。打开文件后,点击文件菜单中的另存为命令,会跳出一个对话框,在最底部有一个编码的下拉条。

QQ截图20191116155852.png

里面有四个选项:ANSI,Unicode,Unicode big endian和UTF-8。

1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对 Windows 简体中文版,如果是繁体中文版会采用 Big5 码)。

2)Unicode编码这里指的是notepad.exe使用的 UCS-2 编码方式,即直接用两个字节存入字符的 Unicode 码,这个选项用的 little endian 格式。

3)Unicode big endian编码与上一个选项相对应。

4)UTF-8编码,也就是上一节谈到的编码方法。

选择完”编码方式”后,点击”保存”按钮,文件的编码方式就立刻转换好了。

Little endian (LE) 和 Big endian (BE)

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(zero width no-break space),用FEFF表示。这正好是两个字节,而且FF比FE大1。

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

对照表:

QQ截图20191229134206.png

示例:

用文本编辑软件UltraEdit 中的”十六进制功能”,观察该文件的内部编码方式。

1)ANSI:文件的编码就是两个字节D1 CF,这正是严的 GB2312 编码,这也暗示 GB2312 是采用大头方式存储的。

2)Unicode:编码是四个字节FF FE 25 4E,其中FF FE表明是小头方式存储,真正的编码是4E 25

3)Unicode big endian:编码是四个字节FE FF 4E 25,其中FE FF表明是大头方式存储。

4)UTF-8:编码是六个字节EF BB BF E4 B8 A5,前三个字节EF BB BF表示这是UTF-8编码,后三个E4 B8 A5就是严的具体编码,它的存储顺序与编码顺序是一致的。

参考:

字符编码笔记:ASCII,Unicode 和 UTF-8

彻底弄懂 Unicode 编码

细说:Unicode, UTF-8, UTF-16, UTF-32, UCS-2, UCS-4

谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词