专业网络营销推广——跟随大平台节奏
电话+V:159999-78052 ,欢迎咨询标准熔断器代理品牌排行榜,[专业新媒体运营推广],[各种商圈业内交流],[抖音运营推广课程],[微信运营推广课程],[小红书运营推广课程],[让你站在风口忘记焦虑]
一、施耐德中国十大经销商
1.东莞市施耐德经销商——东莞唯策电气有限公司
位于广东省东莞市莞城区八达路1号,该公司作为施耐德电气等知名品牌电气产品的代理商,长期以良好的信誉经营,专业提供优质产品和技术服务。
2.产品销售范围
公司不仅服务于珠三角地区,还广泛将产品销售至全国各地,满足不同客户的需求,并获得了用户的广泛认可和好评。
3.代理品牌
东莞唯策电气有限公司代理销售的品牌包括ABB电气、天正电气、正泰电气、上海蒙崎双电源、上海雷神浪涌保护器、上海双科按钮、茗熔熔断器、长江长信转换制、三菱机电、富士机电、士林机电、台安机电、omron欧姆龙、ANLY(安良)日本和泉、RKC、天得等,为客户提供多样化的选择。
4.贡献于高品质生活
二、请问,进口的熔断器什么牌子好?帮忙提供一下信息,我在广东省,最好是...
进口的我知道最好的就是库柏BUSSMANN的熔断器了,深圳市博汇通达科技有限公司是BUSSMANN一级代理商,货很全,而且货期较快!我们家用的就是库柏的熔断器。服务也很好哟
三万字长文:服务器开发设计之算法宝典
原创2021-12-2919:44·腾讯技术工程
作者:lynhlzou,腾讯IEG后台开发工程师孙子云:“上兵伐谋,其次伐交,其次伐兵,其下攻城”,最上乘行军打仗的方式是运用谋略,下乘的方式才是与敌人进行惨烈的厮杀。同样的,在程序设计中,解决问题的办法有很多种,陷入到与逻辑进行贴身肉搏的境况实属下下之策,而能运用优秀合理的算法才是”伐谋”的上上之策。算法的思想精髓是值得深入研究和细细品味的,本宝典总结了服务器开发设计过程中涉及到的一些常用算法,试图尽量以简洁的文字和图表来解释和说明其中的思想原理,希望能给大家带来一些思考和启示。
思维导图1.调度算法在服务器逻辑开发设计中,调度算法随处可见,资源的调度,请求的分配,负载均衡的策略等等都与调度算法相关。调度算法没有好坏之分,最适合业务场景的才是最好的。1.1.轮询轮询是非常简单且常用的一种调度算法,轮询即将请求依次分配到各个服务节点,从第一个节点开始,依次将请求分配到最后一个节点,而后重新开始下一轮循环。最终所有的请求会均摊分配在每个节点上,假设每个请求的消耗是一样的,那么轮询调度是最平衡的调度(负载均衡)算法。1.2.加权轮询有些时候服务节点的性能配置各不相同,处理能力不一样,针对这种的情况,可以根据节点处理能力的强弱配置不同的的权重值,采用加权轮询的方式进行调度。加权轮询可以描述为:调度节点记录所有服务节点的当前权重值,初始化为配置对应值。当有请求需要调度时,每次分配选择当前权重最高的节点,同时被选择的节点权重值减一。若所有节点权重值都为零,则重置为初始化时配置的权重值。最终所有请求会按照各节点的权重值成比例的分配到服务节点上。假设有三个服务节点{a,b,c},它们的权重配置分别为{2,3,4},那么请求的分配次序将是{c,b,c,a,b,c,a,b,c},如下所示:请求序号当前权重选中节点调整后权重1{2,3,4}c{2,3,3}2{2,3,3}b{2,2,3}3{2,2,3}c{2,2,2}4{2,2,2}a{1,2,2}5{1,2,2}b{1,1,2}6{1,1,2}c{1,1,1}7{1,1,1}a{0,1,1}8{0,1,1}b{0,0,1}9{0,0,1}c{0,0,0}1.3.平滑权重轮询加权轮询算法比较容易造成某个服务节点短时间内被集中调用,导致瞬时压力过大,权重高的节点会先被选中直至达到权重次数才会选择下一个节点,请求连续的分配在同一个节点上的情况,例如假设三个服务节点{a,b,c},权重配置分别是{5,1,1},那么加权轮询调度请求的分配次序将是{a,a,a,a,a,b,c},很明显节点a有连续的多个请求被分配。为了应对这种问题,平滑权重轮询实现了基于权重的平滑轮询算法。所谓平滑,就是在一段时间内,不仅服务节点被选择次数的分布和它们的权重一致,而且调度算法还能比较均匀的选择节点,不会在一段时间之内集中只选择某一个权重较高的服务节点。平滑权重轮询算法可以描述为:调度节点记录所有服务节点的当前权重值,初始化为配置对应值。当有请求需要调度时,每次会先把各节点的当前权重值加上自己的配置权重值,然后选择分配当前权重值最高的节点,同时被选择的节点权重值减去所有节点的原始权重值总和。若所有节点权重值都为零,则重置为初始化时配置的权重值。同样假设三个服务节点{a,b,c},权重分别是{5,1,1},那么平滑权重轮询每一轮的分配过程如下表所示:最终请求分配的次序将是{a,a,b,a,c,a,a},相对于普通权重轮询算法会更平滑一些。1.4.随机随机即每次将请求随机地分配到服务节点上,随机的优点是完全无状态的调度,调度节点不需要记录过往请求分配情况的数据。理论上请求量足够大的情况下,随机算法会趋近于完全平衡的负载均衡调度算法。1.5.加权随机类似于加权轮询,加权随机支持根据服务节点处理能力的大小配置不同的的权重值,当有请求需要调度时,每次根据节点的权重值做一次加权随机分配,服务节点权重越大,随机到的概率就越大。最终所有请求分配到各服务节点的数量与节点配置的权重值成正比关系。1.6.最小负载实际应用中,各个请求很有可能是异构的,不同的请求对服务器的消耗各不相同,无论是使用轮询还是随机的方式,都可能无法准确的做到完全的负载均衡。最小负载算法是根据各服务节点当前的真实负载能力进行请求分配的,当前负载最小的节点会被优先选择。最小负载算法可以描述为:服务节点定时向调度节点上报各自的负载情况,调度节点更新并记录所有服务节点的当前负载值。当有请求需要调度时,每次分配选择当前负载最小(负载盈余最大)的服务节点。负载情况可以统计节点正在处理的请求量,服务器的CPU及内存使用率,过往请求的响应延迟情况等数据,综合这些数据以合理的计算公式进行负载打分。1.7.两次随机选择策略最小负载算法可以在请求异构情况下做到更好的均衡性。然而一般情况下服务节点的负载数据都是定时同步到调度节点,存在一定的滞后性,而使用滞后的负载数据进行调度会导致产生“群居”行为,在这种行为中,请求将批量地发送到当前某个低负载的节点,而当下一次同步更新负载数据时,该节点又有可能处于较高位置,然后不会被分配任何请求。再下一次又变成低负载节点被分配了更多的请求,一直处于这种很忙和很闲的循环状态,不利于服务器的稳定。为应对这种情况,两次随机选择策略算法做了一些改进,该算法可以描述为:服务节点定时向调度节点上报各自的负载情况,调度节点更新并记录所有服务节点的当前负载值。从所有可用节点列表中做两次随机选择操作,得到两个节点。比较这两个节点负载情况,选择负载更低的节点作为被调度的节点。两次随机选择策略结合了随机和最小负载这两种算法的优点,使用负载信息来选择节点的同时,避免了可能的“群居”行为。1.8.一致性哈希为了保序和充分利用缓存,我们通常希望相同请求key的请求总是会被分配到同一个服务节点上,以保持请求的一致性,既有了一致性哈希的调度方式。关于一致性哈希算法,笔者曾在km发表过专门的文章《一致性哈希方案在分布式系统中应用对比》,详细介绍和对比了它们的优缺点以及对比数据,有兴趣的同学可以前往阅读。1.8.1.划段最简单的一致性哈希方案就是划段,即事先规划好资源段,根据请求的key值映射找到所属段,比如通过配置的方式,配置id为[1-10000]的请求映射到服务节点1,配置id为[10001-20000]的请求映射到节点2等等,但这种方式存在很大的应用局限性,对于平衡性和稳定性也都不太理想,实际业务应用中基本不会采用。1.8.2.割环法割环法的实现有很多种,原理都类似。割环法将N台服务节点地址哈希成N组整型值,该组整型即为该服务节点的所有虚拟节点,将所有虚拟节点打散在一个环上。请求分配过程中,对于给定的对象key也哈希映射成整型值,在环上搜索大于该值的第一个虚拟节点,虚拟节点对应的实际节点即为该对象需要映射到的服务节点。如下图所示,对象K1映射到了节点2,对象K2映射到节点3。割环法实现复杂度略高,时间复杂度为O(log(vn)),(其中,n是服务节点个数,v是每个节点拥有的虚拟节点数),它具有很好的单调性,而平衡性和稳定性主要取决于虚拟节点的个数和虚拟节点生成规则,例如ketamahash割环法采用的是通过服务节点ip和端口组成的字符串的MD5值,来生成160组虚拟节点。1.8.3.二次取模取模哈希映射是一种简单的一致性哈希方式,但是简单的一次性取模哈希单调性很差,对于故障容灾非常不好,一旦某台服务节点不可用,会导致大部分的请求被重新分配到新的节点,造成缓存的大面积迁移,因此有了二次取模的一致性哈希方式。二次取模算法即调度节点维护两张服务节点表:松散表(所有节点表)和紧实表(可用节点表)。请求分配过程中,先对松散表取模运算,若结果节点可用,则直接选取;若结果节点已不可用,再对紧实表做第二次取模运算,得到最终节点。如下图示:二次取模算法实现简单,时间复杂度为O(1),具有较好的单调性,能很好的处理缩容和节点故障的情况。平衡性和稳定性也比较好,主要取决于对象key的分布是否足够散列(若不够散列,也可以加一层散列函数将key打散)。1.8.4.最高随机权重最高随机权重算法是以请求key和节点标识为参数进行一轮散列运算(如MurmurHash算法),得出所有节点的权重值进行对比,最终取最大权重值对应的节点为目标映射节点。可以描述为如下公式:散列运算也可以认为是一种保持一致性的伪随机的方式,类似于前面讲到的普通随机的调度方式,通过随机比较每个对象的随机值进行选择。这种方式需要O(n)的时间复杂度,但换来的是非常好的单调性和平衡性,在节点数量变化时,只有当对象的最大权重值落在变化的节点上时才受影响,也就是说只会影响变化的节点上的对象的重新映射,因此无论扩容,缩容和节点故障都能以最小的代价转移对象,在节点数较少而对于单调性要求非常高的场景可以采用这种方式。1.8.5.Jumpconsistenthashjumpconsistenthash通过一种非常简单的跳跃算法对给定的对象key算出该对象被映射的服务节点,算法如下:intJumpConsistentHash(unsignedlonglongkey,intnum_buckets){longlongb=-1,j=0;while(j<num_buckets){b=j;key=key*2862933555777941757ULL+1;j=(b+1)*(double(1LL<<31)/double((key>>33)+1));}returnb;}这个算法乍看难以理解,它其实是下面这个算法的一个变种,只是将随机函数通过线性同余的方式改造而来的。intch(intkey,intnum_buckets){random.seed(key);intb=-1;//bucketnumberbeforethepreviousjumpintj=0;//bucketnumberbeforethecurrentjumpwhile(j<num_buckets){b=j;doubler=random.next();//0<r<1.0j=floor((b+1)/r);}returnb;}它也是一种伪随机的方式,通过随机保证了平衡性,而这里随机函数用到的种子是各个请求的key值,因此保证了一致性。它与最高随机权重的差别是这里的随机不需要对所有节点都进行一次随机,而是通过随机值跳跃了部分节点的比较。jumpconsistenthash实现简单,零内存消耗,时间复杂度为O(log(n))。具有很高的平衡性,在单调性方面,扩容和缩容表现较好,但对于中间节点故障,理想情况下需要将故障节点与最后一个节点调换,需要将故障节点和最后的节点共两个节点的对象进行转移。###1.8.6.小结一致性哈希方式还有很多种类,通常结合不同的散列函实现。也有些或为了更简单的使用,或为了更好的单调性,或为了更好的平衡性等而对以上这些方式进行的改造等,如二次Jumpconsistenthash等方式。另外也有结合最小负载方式等的变种,如有限负载一致性哈希会根据当前负载情况对所有节点限制一个最大负载,在一致性哈希中对hash进行映射时跳过已达到最大负载限制的节点,实际应用过程中可根据业务情况自行做更好的调整和结合。
2.不放回随机抽样算法不放回随机抽样即从n个数据中抽取m个不重复的数据。关于不放回随机抽样算法,笔者曾在km发表过专门的文章详细演绎和实现了各种随机抽样算法的原理和过程,以及它们的优缺点和适用范围,有兴趣的同学可以前往阅读。2.1.Knuth洗牌抽样不放回随机抽样可以当成是一次洗牌算法的过程,利用洗牌算法来对序列进行随机排列,然后选取前m个序列作为抽样结果。Knuth洗牌算法是在Fisher-Yates洗牌算法中改进而来的,通过位置交换的方式代替了删除操作,将每个被删除的数字交换为最后一个未删除的数字(或最前一个未删除的数字)。Knuth洗牌算法可以描述为:生成数字1到n的随机排列(数组索引从1开始)forifrom1ton-1doj←随机一个整数值i≤j<n交换a[j]和a[i]运用Knuth洗牌算法进行的随机抽样的方式称为Knuth洗牌随机抽样算法,由于随机抽样只需要抽取m个序列,因此洗牌流程只需洗到前m个数据即可。2.2.占位洗牌随机抽样Knuth洗牌算法是一种in-place的洗牌,即在原有的数组直接洗牌,尽管保留了原数组的所有元素,但它还是破坏了元素之间的前后顺序,有些时候我们希望原数组仅是可读的(如全局配置表),不会因为一次抽样遭到破坏,以满足可以对同一原始数组多次抽样的需求,如若使用Knuth抽样算法,必须对原数组先做一次拷贝操作,但这显然不是最好的做法,更好的办法在Knuth洗牌算法的基础上,不对原数组进行交换操作,而是通过一个额外的map来记录元素间的交换关系,我们称为占位洗牌算法。占位洗牌算法过程演示如下:最终,洗牌的结果为3,5,2,4,1。运用占位洗牌算法实现的随机抽样的方式称为占位洗牌随机抽样,同样的,我们依然可以只抽取到前m个数据即可。这种算法对原数组不做任何修改,代价是增加不大于的临时空间。2.3.选择抽样技术抽样洗牌算法是对一个已经预初始化好的数据列表进行洗牌,需要在内存中全量缓存数据列表,如果数据总量n很大,并且单条记录的数据也很大,那么在内存中缓存所有数据记录的做法会显得非常的笨拙。而选择选择抽样技术算法,它不需要预先全量缓存数据列表,从而可以支持流式处理。选择抽样技术算法可以描述为:生成1到n之间的随机数U如果U≥m,则跳转到步骤4把这个记录选为样本,m减1,n减1。如果m>0,则跳转到步骤1,否则取样完成,算法终止跳过这个记录,不选为样本,n减1,跳转到步骤1选择抽样技术算法过程演示如下:最终,抽样的结果为2,5。可以证明,选择选择抽样技术算法对于每个数被选取的概率都是。选择抽样技术算法虽然不需要将数据流全量缓存到内存中,但他仍然需要预先准确的知道数据量的总大小即n值。它的优点是能保持输出顺序与输入顺序不变,且单个元素是否被抽中可以提前知道。2.4.蓄水池抽样很多时候我们仍然不知道数据总量n,上述的选择抽样技术算法就需要扫描数据两次,第一次先统计n值,第二次再进行抽样,这在流处理场景中仍然有很大的局限性。AlanG.Waterman给出了一种叫蓄水池抽样(ReservoirSampling)的算法,可以在无需提前知道数据总量n的情况下仍然支持流处理场景。蓄水池抽样算法可以描述为:数据游标i←0,将i≤m的数据一次放入蓄水池,并置pool[i]←i生成1到i之间的随机数j如果j>m,则跳转到步骤5把这个记录选为样本,删除原先蓄水池中pool[j]数据,并置pool[j]←i游标i自增1,若i<n,跳转到步骤2,否则取样完成,算法终止,最后蓄水池中的数据即为总样本蓄水池抽样算法过程演示如下:最终,抽样的结果为1,5。可以证明,每个数据被选中且留在蓄水池中的概率为。2.5.随机分值排序抽样洗牌算法也可以认为就是将数据按随机的方式做一个排序,从n个元素集合中随机抽取m个元素的问题就相当于是随机排序之后取前m排名的元素,基于这个原理,我们可以设计一种通过随机分值排序的方式来解决随机抽样问题。随机分值排序算法可以描述为:系统维护一张容量为m的排行榜单对于每个元素都给他们随机一个(0,1]区间的分值,并根据随机分值插入排行榜所有数据处理完成,最终排名前m的元素即为抽样结果尽管随机分值排序抽样算法相比于蓄水池抽样算法并没有什么好处,反而需要增加额外的排序消耗,但接下来的带权重随机抽样将利用到它的算法思想。2.6.朴素的带权重抽样很多需求场景数据元素都需要带有权重,每个元素被抽取的概率是由元素本身的权重决定的,诸如全服消费抽奖类活动,需要以玩家在一定时间段内的总消费额度为权重进行抽奖,消费越高,最后中奖的机会就越大,这就涉及到了带权重的抽样算法。朴素的带权重随机算法也称为轮盘赌选择法,将数据放置在一个假想的轮盘上,元素个体的权重越高,在轮盘上占据的空间就越多,因此就更有可能被选中。假设上面轮盘一到四等奖和幸运奖的权重值分别为5,10,15,30,40,所有元素权重之和为100,我们可以从[1,100]中随机得到一个值,假设为45,而后从第一个元素开始,不断累加它们的权重,直到有一个元素的累加权重包含45,则选取该元素。如下所示:由于权重45处于四等奖的累加权重值当中,因此最后抽样结果为四等奖。若要不放回的选取m个元素,则需要先选取一个,并将该元素从集合中踢除,再反复按同样的方法抽取其余元素。这种抽样算法的复杂度是,并且将元素从集合中删除破坏了原数据的可读属性,更重要的是这个算法需要多次遍历数据,不适合在流处理的场景中应用。2.7.带权重的A-Res算法蓄水池抽样朴素的带权重抽样算法需要内存足够容纳所有数据,破坏了原数据的可读属性,时间复杂度高等缺点,而经典的蓄水池算法高效的实现了流处理场景的大数据不放回随机抽样,但对于带权重的情况,就不能适用了。A-Res(AlgorithmAWithaReservoir)是蓄水池抽样算法的带权重版本,算法主体思想与经典蓄水池算法一样都是维护含有m个元素的结果集,对每个新元素尝试去替换结果集中的元素。同时它巧妙的利用了随机分值排序算法抽样的思想,在对数据做随机分值的时候结合数据的权重大小生成排名分数,以满足分值与权重之间的正相关性,而这个A-Res算法生成随机分值的公式就是:其中为第i个数据的权重值,是从(0,1]之间的一个随机值。A-Res算法可以描述为:对于前m个数,计算特值,直接放入蓄水池中对于从m+1,m+2,...,n的第i个数,通过公式计算特征值,如若特征值超过蓄水池中最小值,则替换最小值该算法的时间复杂度为,且可以完美的运用在流式处理场景中。2.8.带权重的A-ExpJ算法蓄水池抽样A-Res需要对每个元素产生一个随机数,而生成高质量的随机数有可能会有较大的性能开销,《Weightedrandomsamplingwithareservoir》论文中给出了一种更为优化的指数跳跃的算法A-ExpJ抽样(AlgorithmAwithexponentialjumps),它能将随机数的生成量从减少到,原理类似于通过一次额外的随机来跳过一段元素的特征值的计算。A-ExpJ算法蓄水池抽样可以描述为:对于前m个数,计算特征值,其中为第i个数据的权重值,是从(0,1]之间的一个随机值,直接放入蓄水池中对于从m+1,m+2,...,n的第i个数,执行以下步骤计算阈值,,其中r为(0,1]之间的一个随机值,为蓄水池中的最小特征值跳过部分元素并累加这些元素权重值,直到第i个元素满足计算当前元素特征值,其中为(,1]之间的一个随机值,,为蓄水池中的最小特征值,为当前元素权重值使用当前元素替换蓄水池中最小特征值的元素更新阈值,有点不好理解,showyouthecode:functionaexpj_weight_sampling(data_array,weight_array,n,m)localresult,rank={},{}fori=1,mdolocalrand_score=math.random()^(1/weight_array[i])localidx=binary_search(rank,rand_score)table.insert(rank,idx,{score=rand_score,data=data_array[i]})endlocalweight_sum,xw=0,math.log(math.random())/math.log(rank[m].score)fori=m+1,ndoweight_sum=weight_sum+weight_array[i]ifweight_sum>=xwthenlocaltw=rank[m].score^weight_array[i]localrand_score=(math.random()*(1-tw)+tw)^(1/weight_array[i])localidx=binary_search(rank,rand_score)table.insert(rank,idx,{score=rand_score,data=data_array[i]})table.remove(rank)weight_sum=0xw=math.log(math.random())/math.log(rank[m].score)endendfori=1,mdoresult[i]=rank[i].dataendreturnresultend3.排序算法3.1.基础排序基础排序是建立在对元素排序码进行比较的基础上进行的排序算法。3.1.1.冒泡排序冒泡排序是一种简单直观的排序算法。它每轮对每一对相邻元素进行比较,如果相邻元素顺序不符合规则,则交换他们的顺序,每轮将有一个最小(大)的元素浮上来。当所有轮结束之后,就是一个有序的序列。过程演示如下:3.1.2.插入排序插入排序通过构建有序序列,初始将第一个元素看做是一个有序序列,后面所有元素看作未排序序列,从头到尾依次扫描未排序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。过程演示如下:3.1.3.选择排序选择排序首先在未排序序列中找到最小(大)元素,存放到已排序序列的起始位置。再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。直到所有元素处理完毕。过程演示如下:插入排序是每轮会处理好第一个未排序序列的位置,而选择排序是每轮固定好一个已排序序列的位置。冒泡排序也是每轮固定好一个已排序序列位置,它与选择排序之间的不同是选择排序直接选一个最小(大)的元素出来,而冒泡排序通过依次相邻交换的方式选择出最小(大)元素。3.1.4.快速排序快速排序使用分治法策略来把一串序列分为两个子串序列。快速排序是一种分而治之的思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。快速排序从数列中挑出一个元素,称为"基准",所有元素比基准值小的摆放在基准前面,比基准值大的摆在基准的后面。一轮之后该基准就处于数列的中间位置。并递归地把小于基准值元素的子数列和大于基准值元素的子数列进行排序。过程演示如下:3.1.5.归并排序归并排序是建立在归并操作上的一种有效的排序算法,也是采用分治法的一个非常典型的应用。归并排序首先将序列二分成最小单元,而后通过归并的方式将两两已经有序的序列合并成一个有序序列,直到最后合并为一个最终有序序列。过程演示如下:3.1.6.堆排序堆排序(Heapsort)是利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:子结点的键值或索引总是小于(或者大于)它的父节点。堆排序首先创建一个堆,每轮将堆顶元素弹出,而后进行堆调整,保持堆的特性。所有被弹出的元素序列即是最终排序序列。过程演示如下:3.1.7.希尔排序希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本,但希尔排序是非稳定排序算法。插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。过程演示如下:3.2.分配排序基础排序是建立在对元素排序码进行比较的基础上,而分配排序是采用“分配”与“收集”的办法。3.2.1.计数排序计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。计数排序的特征:当输入的元素是n个0到k之间的整数时,它的运行时间是。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和空间。过程演示如下:3.2.2.桶排序桶排序是计数排序的升级版,它利用了函数的映射关系,桶排序高效与否的关键就在于这个映射函数的确定。比如我们可以将排序数据进行除10运算,运算结果中具有相同的商值放入相同的桶中,即每十个数会放入相同的桶中。过程演示如下:为了使桶排序更加高效,我们需要做到这两点:在额外空间充足的情况下,尽量增大桶的数量使用的映射函数能够将输入的所有数据均匀的分配到所有桶中计数排序本质上是一种特殊的桶排序,当桶的个数取最大值(max-min+1)的时候,桶排序就变成了计数排序。3.2.3.基数排序基数排序的原理是将整数按位数切割成不同的数字,然后对每个位数分别比较。基数排序首先按最低有效位数字进行排序,将相同值放入同一个桶中,并按最低位值顺序叠放,然后再按次低有效位排序,重复这个过程直到所有位都进行了排序,最终即是一个有序序列。过程演示如下:基数排序也是一种桶排序。桶排序是按值区间划分桶,基数排序是按数位来划分,基数排序可以看做是多轮桶排序,每个数位上都进行一轮桶排序。3.3.多路归并排序多路归并排序算法是将多个已经有序的列表进行归并排序,合成为一组有序的列表的排序过程。k路归并排序可以描述为:初始时取出k路有序列表中首个元素放入比较池。从比较池中取最小(大)的元素加入到结果列表,同时将该元素所在有序列表的下一个元素放入比较池(若有)。重新复进行步骤2,直到所有队列的所有元素都已取出。每次在比较池中取最小(大)的元素时,需要进行一次k个数据的比较操作,当k值较大时,会严重影响多路归并的效率,为提高效率,可以使用“败者树”来实现这样的比较过程。败者树是完全二叉树,败者树相对的是胜者树,胜者树每个非终端结点(除叶子结点之外的其它结点)中的值都表示的是左右孩子相比较后的胜者。如下图所示是一棵胜者树:而败者树双亲结点表示的是左右孩子比较之后的失败者,但在上一层的比较过程中,仍然是拿前一次的胜者去比较。如下图所示是一颗败者树:叶子节点的值是:{7,4,8,2,3,5,6,1},7与4比较,7是败者,4是胜者,因此他们的双亲节点是7,同样8与2比较,8是败者,表示在他们双亲节点上,而7与8的双亲节点需要用他们的胜者去比较,即用4与2比较,4是败者,因此7与8的双亲节点记录的是4,依此类推。假设k=8,败者树归并排序的过程演示如下所示:首先构建起败者数,最后的胜者是1,第二次将1弹出,取1所在的第8列的第二个数15放入1所在的叶子节点位置,并进行败者树调整,此时只需调整原1所在分支的祖先节点,最后胜者为2,后续过程依此类推。最后每轮的最终胜者序列即是最后的归并有序序列。胜者树和败者树的本质是利用空间换时间的做法,通过辅助节点记录两两节点的比较结果来达到新插入节点后的比较和调整性能。笔者曾经基于lua语言利用败者树实现多路归并排序算法,有兴趣可以前往阅读。3.4.跳跃表排序跳跃表(SkipLists)是一种有序的数据结构,它通过在每个节点中随机的建立上层辅助查找节点,从而达到快速访问节点的目的(与败者树的多路归并排序有异曲同工之妙)。跳跃列表按层建造,底层是一个普通的有序链表,包含所有元素。每个更高层都充当下面列表的“快速通道”,第i层中的元素按某个固定的概率p(通常为1/2或1/4)随机出现在第i+1层中。每个元素平均出现在1/(1-p)个列表中,而最高层的元素在个列表中出现。如下是四层跳跃表结构的示意:在查找目标元素时,从顶层列表、头元素起步,沿着每层链表搜索,直至找到一个大于或等于目标的元素,或者到达当前层列表末尾。如果该元素等于目标元素,则表明该元素已被找到;如果该元素大于目标元素或已到达链表末尾,则退回到当前层的上一个元素,然后转入下一层进行搜索。依次类推,最终找到该元素或在最底层底仍未找到(不存在)。当p值越大,快速通道就越稀疏,占用空间越小,但查找速度越慢,反之,则占用空间大查找速度快,通过选择不同p值,就可以在查找代价和存储代价之间获取平衡。由于跳跃表使用的是链表,加上增加了近似于以二分方式的辅助节点,因此查询,插入和删除的性能都很理想。在大部分情况下,跳跃表的效率可以和平衡树相媲美,它是一种随机化的平衡方案,在实现上比平衡树要更为简单,因而得到了广泛的应用,如redis的zset,leveldb,我司的apollo排行榜等都使用了跳跃表排序方案。3.5.百分比近似排序在流处理场景中,针对大容量的排序榜单,全量存储和排序需要消耗的空间及时间都很高,不太现实。实际应用中,对于长尾数据的排序,一般也只需要显示百分比近似排名,通过牺牲一定的精确度来换取高性能和高实时性。3.5.1.HdrHistogram算法HdrHistogram使用的是直方图统计算法,直方图算法类似于桶排序,原理就是创建一个直方图,以一定的区间间隔记录每个区间上的数据总量,预测排名时只需统计当前值所在区间及之前区间的所有数量之和与总数据量之间的比率。区间分割方式可以采用线性分割和指数分割方式:线性分割,数据以固定长度进行分割,假设数据范围是[1-1000000],以每100的间隔划分为1个区间,总共需要划分10000个区间桶。指数分割,基于指数倍的间隔长度进行分割,假设数据范围是[1-1000000],以2的幂次方的区间[,]进行划分,总共只需要划分20个区间桶。HdrHistogram为了兼顾内存和估算的准确度,同时采用了线性分割和指数分割的方式,相当于两层的直方图算法,第一层使用指数分割方式,可以粗略的估算数据的排名范围位置,第二层使用线性分割方式,更加精确的估算出数据的排名位置。线性区间划分越小结果越精确,但需要的内存越多,可以根据业务精确度需求控制线性区间的大小。直方图算法需要预先知道数据的最大值,超过最大值的数据将存不进来。HdrHistogram提供了一个自动扩容的功能,以解决数据超过预估值的问题,但是这个自动扩容方式存在一个很高的拷贝成本。3.5.2.CKMS算法HdrHistogram是一种静态分桶的算法,当数据序列是均匀分布的情况下,有比较好的预测效果,然而实际应用中数据有可能并不均匀,很有可能集中在某几个区间上,CKMS采用的是动态分桶的方式,在数据处理过程中不断调整桶的区间间隔和数量。CKMS同时引入一个可配置的错误率的概念,在抉择是否开辟新桶时,根据用户设置的错误率进行计算判定。判定公式为:区间间隔=错误率*数据总量。下图是一个桶合并的例子:如上所示,假设错误率设置为0.1,当数据总量大于10个时,通过判定公式计算出区间间隔为1,因此将会对区间间隔小于等于1的相邻桶进行合并。CKMS算法不需要预知数据的范围,用户可以根据数据的性质设置合适的错误率,以控制桶的空间占用和精确度之间的平衡关系。3.5.3.TDigest算法Tdigest算法的思想是近似算法常用的素描法(Sketch),用一部分数据来刻画整体数据集的特征,就像我们日常的素描画一样,虽然和实物有差距,但是却看着和实物很像,能够展现实物的特征。它本质上也是一种动态分桶的方式。TDigest算法估计具体的百分位数时,都是根据百分位数对应的两个质心去线性插值计算的,和精准百分位数的计算方式一样。首先我们根据百分位q和所有质心的总权重计算出索引值;其次找出和对应索引相邻的两个质心;最终可以根据两个质心的均值和权重用插值的方法计算出对应的百分位数。(实际的计算方法就是加权平均)。由此我们可以知道,百分位数q的计算误差要越小,其对应的两个质心的均值应该越接近。TDigest算法的关键就是如何控制质心的数量,质心的数量越多,显然估计的精度就会越高,但是需要的内存就会越多,计算效率也越低;但是质心数量越少,估计的精度就很低,所以就需要一个权衡。一种TDigest构建算法buffer-and-merge可以描述为:将新加入的数据点加入临时数组中,当临时数组满了或者需要计算分位数时,将临时数组中的数据点和已经存在的质心一起排序。(其中数据点和质心的表达方式是完全一样的:平均值和权重,每个数据点的平均值就是其本身,权重默认是1)。遍历所有的数据点和质心,满足合并条件的数据点和质心就进行合并,如果超出权重上限,则创建新的质心数,否则修改当前质心数的平均值和权重。假设我们有200个质心,那么我们就可以将0到1拆分200等份,则每个质心就对应0.5个百分位。假如现在有10000个数据点,即总权重是10000,我们按照大小对10000个点排序后,就可以确定每个质心的权重(相当于质心代表的数据点的个数)应该在10000/200=500左右,所以说当每个质心的权重小于500时,我们就可以将当前数据点加入当前的质心,否则就新建一个质心。实际应用中,我们可能更加关心90%,95%,99%等极端的百分位数,所以TDigest算法特意优化了q=0和q=1附近的百分位精度,通过专门的映射函数K保证了q=0和q=1附近的质心权重较小,数量较多。另外一种TDigest构建算法是AVL树的聚类算法,与buffer-and-merge算法相比,它通过使用AVL二叉平衡树的方式来搜索数据点最靠近的质心数,找到最靠近的质心数后,将二者进行合并。
4.限流与过载保护复杂的业务场景中,经常容易遇到瞬时请求量的突增,很有可能会导致服务器占用过多资源,发生了大量的重试和资源竞争,导致响应速度降低、超时、乃至宕机,甚至引发雪崩造成整个系统不可用的情况。为应对这种情况,通常需要系统具备可靠的限流和过载保护的能力,对于超出系统承载能力的部分的请求作出快速拒绝、丢弃处理,以保证本服务或下游服务系统的稳定。4.1.计数器计数器算法是限流算法里最简单也是最容易实现的一种算法。计数器算法可以针对某个用户的请求,或某类接口请求,或全局总请求量进行限制。比如我们设定针对单个玩家的登录协议,每3秒才能请求一次,服务器可以在玩家数据上记录玩家上一次的登录时间,通过与本次登录时间进行对比,判断是否已经超过了3秒钟来决定本次请求是否需要继续处理。又如针对某类协议,假设我们设定服务器同一秒内总登录协议请求次数不超过100条,我们可以设置一个计数器,每当一个登录请求过来的时候,计数器加1,如果计数器值大于100且当前请求与第一个请求间隔时间还在1秒内,那么就判定为达到请求上限,拒绝服务,如果该请求与第一个请求间隔已经超过1秒钟,则重置计数器的值为0,并重新计数。计数器算法存在瞬时流量的临界问题,即在时间窗口切换时,前一个窗口和后一个窗口的请求量都集中在时间窗口切换的前后,在最坏的情况下,可能会产生两倍于阈值流量的请求。为此也可以使用多个不同间隔的计数器相结合的方式进行限频,如可以限制登录请求1秒内不超过100的同时1分钟内不超过1000次。4.2.漏桶漏桶算法原理很简单,假设有一个水桶,所有水(请求)都会先丢进漏桶中,漏桶则以固定的速率出水(处理请求),当请求量速率过大,水桶中的水则会溢出(请求被丢弃)。漏桶算法能保证系统整体按固定的速率处理请求。如下图所示:4.3.令牌桶对于很多应用场景来说,除了要求能够限制请求的固定处理速率外,还要求允许某种程度的突发请求量,这时候漏桶算法可能就不合适了。令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶算法大概描述如下:所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。根据限流大小,设置按照一定的速率往桶里添加令牌。桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃。请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。如下图所示:4.4.滑动窗口计数器,漏桶和令牌桶算法是在上游节点做的限流,通过配置系统参数做限制,不依赖于下游服务的反馈数据,对于异构的请求不太适用,且需要预估下游节点的处理能力。滑动窗口限频类似于TCP的滑动窗口协议,设置一个窗口大小,这个大小即当前最大在处理中的请求量,同时记录滑动窗口的左右端点,每次发送一个请求时滑动窗口右端点往前移一格,每次收到请求处理完毕响应后窗口左端点往前移一格,当右端点与左端点的差值超过最大窗口大小时,等待发送或拒绝服务。如下图所示:4.5.SRE自适应限流滑动窗口是以固定的窗口大小限制请求,而Google的SRE自适应限流相当于是一个动态的窗口,它根据过往请求的成功率动态调整向后端发送请求的速率,当成功率越高请求被拒绝的概率就越小;反之,当成功率越低请求被拒绝的概率就相应越大。SRE自适应限流算法需要在应用层记录过去两分钟内的两个数据信息:requests:请求总量,应用层尝试的请求数accepts:成功被后端处理的请求数请求被拒绝的概率p的计算公式如下:其中K为倍率因子,由用户设置(比如2),从算法公式可以看出:在正常情况下requests等于accepts,新请求被决绝的概率p为0,即所有请求正常通过当后端出现异常情况时,accepts的数量会逐渐小于requests,应用层可以继续发送请求直到requests等于,一旦超过这个值,自适应限流启动,新请求就会以概率p被拒绝。当后端逐渐恢复时,accepts逐渐增加,概率p会增大,更多请求会被放过,当accepts恢复到使得大于等于requests时,概率p等于0,限流结束。我们可以针对不同场景中处理更多请求带来的风险成本与拒绝更多请求带来的服务损失成本之间进行权衡,调整K值大小:降低K值会使自适应限流算法更加激进(拒绝更多请求,服务损失成本升高,风险成本降低)。增加K值会使自适应限流算法不再那么激进(放过更多请求,服务损失成本降低,风险成本升高)。如对于某些处理该请求的成本与拒绝该请求的成本的接近场景,系统高负荷运转造成很多请求处理超时,实际已无意义,然而却还是一样会消耗系统资源的情况下,可以调小K值。4.6.熔断熔断算法原理是系统统计并定时检查过往请求的失败(超时)比率,当失败(超时)率达到一定阈值之后,熔断器开启,并休眠一段时间,当休眠期结束后,熔断器关闭,重新往后端节点发送请求,并重新统计失败率。如此周而复始。如下图所示:4.7.Hystrix半开熔断器Hystrix中的半开熔断器相对于简单熔断增加了一种半开状态,Hystrix在运行过程中会向每个请求对应的节点报告成功、失败、超时和拒绝的状态,熔断器维护计算统计的数据,根据这些统计的信息来确定熔断器是否打开。如果打开,后续的请求都会被截断。然后会隔一段时间,尝试半开状态,即放入一部分请求过去,相当于对服务进行一次健康检查,如果服务恢复,熔断器关闭,随后完全恢复调用,如果失败,则重新打开熔断器,继续进入熔断等待状态。如下图所示:5.序列化与编码数据结构序列化是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络传输),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。经过依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。5.1.标记语言标记语言是一种将文本(Text)以及文本相关的其他信息结合起来,展现出关于文档结构和数据处理细节的计算机文字编码。5.1.1.超文本标记语言(HTML)HTML是一种用于创建网页的标准标记语言。HTML是一种基础技术,常与CSS、JavaScript一起被众多网站用于设计网页、网页应用程序以及移动应用程序的用户界面。网页浏览器可以读取HTML文件,并将其渲染成可视化网页。HTML描述了一个网站的结构语义随着线索的呈现,使之成为一种标记语言而非编程语言。5.1.2.可扩展标记语言(XML)XML是一种标记语言,设计用来传送及携带数据信息。每个XML文档都由XML声明开始,在前面的代码中的第一行就是XML声明。这一行代码会告诉解析器或浏览器这个文件应该按照XML规则进行解析。XML文档的字符分为标记(Markup)与内容(content)两类。标记通常以<开头,以>结尾;或者以字符开头,以;结尾。不是标记的字符就是内容。一个tag属于标记结构,以<开头,以>结尾。元素是文档逻辑组成,或者在start-tag与匹配的end-tag之间,或者仅作为一个empty-elementtag。属性是一种标记结构,在start-tag或empty-elementtag内部的“名字-值对”。例如:<imgsrc="madonna.jpg"alt="Madonna"/>每个元素中,一个属性最多出现一次,一个属性只能有一个值。5.1.3.MarkdownMarkdown是一种轻量级标记语言,创始人为约翰·格鲁伯。它允许人们使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档。这种语言吸收了很多在电子邮件中已有的纯文本标记的特性。由于Markdown的轻量化、易读易写特性,并且对于图片,图表、数学式都有支持,目前许多网站都广泛使用Markdown来撰写帮助文档或是用于论坛上发表消息。如GitHub、Reddit、Diaspora、StackExchange、OpenStreetMap、SourceForge、简书等,甚至还能被用来撰写电子书。当然还有咱们的KM平台,很强大。Markdown语法格式如下:5.1.4.JSONJSON是以数据线性化为目标的轻量级标记语言,相比于XML,JSON更加简洁、轻量和具有更好的可读性。JSON的基本数据类型和编码规则:数值:十进制数,不能有前导0,可以为负数,可以有小数部分。还可以用e或者E表示指数部分。不能包含非数,如NaN。不区分整数与浮点数。字符串:以双引号""括起来的零个或多个Unicode码位。支持反斜杠开始的转义字符序列。布尔值:表示为true或者false。数组:有序的零个或者多个值。每个值可以为任意类型。序列表使用方括号[,]括起来。元素之间用逗号,分割。形如:[value,value]对象:若干无序的“键-值对”(key-valuepairs),其中键只能是字符串。建议但不强制要求对象中的键是独一无二的。对象以花括号{开始,并以}结束。键-值对之间使用逗号分隔。键与值之间用冒号:分割。空值:值写为null5.2.TLV二进制序列化很多高效得数据序列化方式都是采用类TLV(Tag+Length+Value)的方式对数据进行序列化和反序列化,每块数据以Tag开始,Tag即数据标签,标识接下来的数据类型是什么,Length即长度,标识接下来的数据总长,Value即数据的实际内容,结合Tag和Length的大小即可获取当前这块数据内容。5.2.1.ProtocolBuffersProtocolBuffers(简称:ProtoBuf)是一种开源跨平台的序列化数据结构的协议,它是一种灵活,高效,自动化的结构数据序列化方法,相比XML和JSON更小、更快、更为简单。ProtocolBuffers包含一个接口描述语言.proto文件,描述需要定义的一些数据结构,通过程序工具根据这些描述产生.cc和.h文件代码,这些代码将用来生成或解析代表这些数据结构的字节流。ProtocolBuffers编码后的消息都是Key-Value形式,Key的值由field_number(字段标号)和wire_type(编码类型)组合而成,规则为:key=field_number<<3|wiretype。field_number部分指示了当前是哪个数据成员,通过它将cc和h文件中的数据成员与当前的key-value对应起来。wiretype为字段编码类型,有以下几类:ProtocolBuffers编码特征:整型数据采用varint编码(见5.4.1节),以节省序列化后数据大小。对于有符号整型,先进行zigzag编码(见5.4.2节)调整再进行varint数据编码,以减小负整数序列化后数据大小。string、嵌套结构以及packedrepeatedfields的编码类型是Length-delimited,它们的编码方式统一为tag+length+value。5.2.2.TDRTDR是腾讯互娱研发部自研跨平台多语言数据表示组件,主要用于数据的序列化反序列化以及数据的存储。TDR通过XML文件来定义接口和结构的描述,通过程序工具根据这些描述产生.tdr和.h文件代码,用于序列化和反序列化这些数据结构。TDR1.0的版本是通过版本剪裁方式来序列化反序列化,需要事先维护好字段版本号,序列化反序列化时通过剪裁版本号来完成兼容的方式,只支持单向的高版本兼容低版本数据。TDR2.0整体上与ProtocolBuffers相似,TDR2.0支持消息协议的前后双向兼容,整型数据同样支持varint编码和zigzag调整的方式,在对TLV中Length部分进行处理时,采用定长编解码方式,以浪费序列化空间的代价来获取更高性能,避免了类似ProtocolBuffers中不必要的内存拷贝(或者是预先计算大小)的过程。ProtocolBuffers和TDR都有接口描述语言,这使得它们的序列化更高效,数据序列化后也更加紧凑。5.2.3.Luna序列化luna库是开源的基于C++17的lua/C++绑定库,它同时也实现了针对lua数据结构的序列化和反序列化功能,用于lua结构数据的传输和存储。Lua语言中需要传输和存储的数据类型主要有:nil,boolean,number,string,table。因此在序列化过程中,luna将类型定义为以下九种类型。序列化方式如下:整体上也是类似于ProtocolBuffers和TDR的TLV编码方式,同时针对lua类型结构的特性做了一些效率上的优化。主要特性如下:Boolean类型区分为bool_true和bool_false,只需在增加2种type值就可以解决。整型integer同样采用varint压缩编码方式,无需额外字节记录长度。有符号整型,同样是先进行zigzag调整再进行varint数据编码。字符串类型分为string和string_idx,编码过程中会缓存已经出现过的字符串,对于后续重复出现的字符串记录为string_idx类型,value值记录该字符串第一次出现的序号,节约字符串占用的空间。对于小于246(255减去类型数量9)的小正整型数,直接当成不同类型处理,加上数值9之后记录在type中,节约空间。Table为嵌套结构,用table_head和table_tail两种类型表示开始和结束。key和value分别进行嵌套编码。5.2.4.Skynet序列化Skynet是一个应用广泛的为在线游戏服务器打造的轻量级框架。但实际上它也不仅仅使用在游戏服务器领域。skynet的核心是由C语言编写,但大多数skynet服务使用lua编写,因此它也实现了针对lua数据结构的序列化和反序列化功能。序列化方式如下:主要特性如下:Type类型通过低3位和高5位来区分主类型和子类型。Boolean单独主类型,子类型字段用1和0区分true和false。整型使用number主类型,子类型分为number_zero(0值),number_byte(小于8位的正整数),number_word(小于16位的正整数),number_dword(小于32位的正负整数),number_qword(其他整数),number_real(浮点数)。字符串类型分为短字符串short_string和长字符串long_string,小于32字节长度的字符串记录为short_string主类型,低5位的子类型记录长度。long_string又分为2字节(长度小于0x1000)和4字节(长度大于等于0x1000)长字符串,分别用2字节length和4字节length记录长度。Table类型会区分array部分和hash部分,先将array部分序列化,array部分又分为小array和大array,小array(0-30个元素)直接用type的低5位的子类型记录大小,大array的子类型固定为31,大小通过number类型编码。Hash部分需要将key和value分别进行嵌套编码。Table的结束没有像luna一样加了专门的table_tail标识,而是通过nil类型标识。5.3.压缩编码压缩算法从对数据的完整性角度分有损压缩和无损压缩。有损压缩算法通过移除在保真前提下需要的必要数据之外的其小细节,从而使文件变小。在有损压缩里,因部分有效数据的移除,恢复原文件是不可能的。有损压缩主要用来存储图像和音频文件,通过移除数据达到比较高的压缩率。无损压缩,也能使文件变小,但对应的解压缩功能可以精确的恢复原文件,不丢失任何数据。无损数据压缩被广泛的应用于计算机领域,数据的传输和存储系统中均使用无损压缩算法。接下来我们主要是介绍几种无损压缩编码算法。5.3.1.熵编码法一种主要类型的熵编码方式是对输入的每一个符号,创建并分配一个唯一的前缀码,然后,通过将每个固定长度的输入符号替换成相应的可变长度前缀无关(prefix-free)输出码字替换,从而达到压缩数据的目的。每个码字的长度近似与概率的负对数成比例。因此,最常见的符号使用最短的码。霍夫曼编码和算术编码是两种最常见的熵编码技术。如果预先已知数据流的近似熵特性(尤其是对于信号压缩),可以使用简单的静态码。5.3.2.游程编码又称行程长度编码或变动长度编码法,是一种与资料性质无关的无损数据压缩技术,基于“使用变动长度的码来取代连续重复出现的原始资料”来实现压缩。举例来说,一组资料串"AAAABBBCCDEEEE",由4个A、3个B、2个C、1个D、4个E组成,经过变动长度编码法可将资料压缩为4A3B2C1D4E(由14个单位转成10个单位)。5.3.3.MTF变换MTF(Move-To-Front)是一种数据编码方式,作为一个额外的步骤,用于提高数据压缩技术效果。MTF主要使用的是数据“空间局部性”,也就是最近出现过的字符很可能在接下来的文本附近再次出现。过程可以描述为:首先维护一个文本字符集大小的栈表,“recentlyusedsymbols”(最近访问过的字符),其中每个不同的字符在其中占一个位置,位置从0开始编号。扫描需要重新编码的文本数据,对于每个扫描到的字符,使用该字符在“recentlyusedsymbols”中的index替换,并将该字符提到“recentlyusedsymbols”的栈顶的位置(index为0的位置)。重复上一步骤,直到文本扫描结束。5.3.4.块排序压缩当一个字符串用该算法转换时,算法只改变这个字符串中字符的顺序而并不改变其字符。如果原字符串有几个出现多次的子串,那么转换过的字符串上就会有一些连续重复的字符,这对压缩是很有用的。块排序变换(Burrows-WheelerTransform)算法能使得基于处理字符串中连续重复字符的技术(如MTF变换和游程编码)的编码更容易被压缩。块排序变换算法将输入字符串的所有循环字符串按照字典序排序,并以排序后字符串形成的矩阵的最后一列为其输出。5.3.5.字典编码法由AbrahamLempel和JacobZiv独创性的使用字典编码器的LZ77/78算法及其LZ系列变种应用广泛。LZ77算法通过使用编码器或者解码器中已经出现过的相应匹配数据信息替换当前数据从而实现压缩功能。这个匹配信息使用称为长度-距离对的一对数据进行编码,它等同于“每个给定长度个字符都等于后面特定距离字符位置上的未压缩数据流。”编码器和解码器都必须保存一定数量的缓存数据。保存这些数据的结构叫作滑动窗口,因为这样所以LZ77有时也称作滑动窗口压缩。编码器需要保存这个数据查找匹配数据,解码器保存这个数据解析编码器所指代的匹配数据。LZ77算法针对过去的数据进行处理,而LZ78算法却是针对后来的数据进行处理。LZ78通过对输入缓存数据进行预先扫描与它维护的字典中的数据进行匹配来实现这个功能,在找到字典中不能匹配的数据之前它扫描进所有的数据,这时它将输出数据在字典中的位置、匹配的长度以及找不到匹配的数据,并且将结果数据添加到字典中。5.3.6.霍夫曼(Huffman)编码霍夫曼编码把文件中一定位长的值看作是符号,比如把8位长的256种值,也就是字节的256种值看作是符号。根据这些符号在文件中出现的频率,对这些符号重新编码。对于出现次数非常多的,用较少的位来表示,对于出现次数非常少的,用较多的位来表示。这样一来,文件的一些部分位数变少了,一些部分位数变多了,由于变小的部分比变大的部分多,所以整个文件的大小还是会减小,所以文件得到了压缩。要进行霍夫曼编码,首先要把整个文件读一遍,在读的过程中,统计每个符号(我们把字节的256种值看作是256种符号)的出现次数。然后根据符号的出现次数,建立霍夫曼树,通过霍夫曼树得到每个符号的新的编码。对于文件中出现次数较多的符号,它的霍夫曼编码的位数比较少。对于文件中出现次数较少的符号,它的霍夫曼编码的位数比较多。然后把文件中的每个字节替换成他们新的编码。5.3.7.其他压缩编码deflate是同时使用了LZ77算法与霍夫曼编码的一个无损数据压缩算法。gzip压缩算法的基础是deflate。bzip2使用Burrows-Wheelertransform将重复出现的字符序列转换成同样字母的字符串,然后用move-to-front变换进行处理,最后使用霍夫曼编码进行压缩。LZ4着重于压缩和解压缩速度,它属于面向字节的LZ77压缩方案家族。Snappy(以前称Zippy)是Google基于LZ77的思路用C++语言编写的快速数据压缩与解压程序库,并在2011年开源,它的目标并非最大压缩率或与其他压缩程序库的兼容性,而是非常高的速度和合理的压缩率。转自网络的压缩率和性能对比:FormatSizeBefore(byte)SizeAfter(byte)CompressExpend(ms)UnCompressExpend(ms)MAXCPU(%)bzip235984867711591236229.5gzip359848804217938926.5deflate35984970468034420.5lzo359841306958123022lz4359841635532714712.6snappy35984136024248811
5.4.其他编码5.4.1.Varint前文所提到的Varint整型压缩编码方式,它使用一个或多个字节序列化整数的方法,把整数编码为变长字节。Varint编码将每个字节的低7bit位用于表示数据,最高bit位表示后面是否还有字节,其中1表示还有后续字节,0表示当前是最后一个字节。当整型数值很小时,只需要极少数的字节进行编码,如数值9,它的编码就是00001001,只需一个字节。如上图所示,假设要编码的数据123456,二进制为:11110001001000000,按7bit划分后,每7bit添加高1位的是否有后续字节标识,编码为110000001100010000000111,占用3个字节。对于32位整型数据经过Varint编码后需要1~5个字节,小的数字使用1个字节,大的数字使用5个字节。64位整型数据编码后占用1~10个字节。在实际场景中小数字的使用率远远多于大数字,因此通过Varint编码对于大部分场景都可以起到很好的压缩效果。5.4.2.ZigZagzigzag编码的出现是为了解决varint对负数编码效率低的问题。对于有符号整型,如果数值为负数,二进制就会非常大,例如-1的16进制:0xffffffffffffffff,对应的二进制位全部是1,使用varint编码需要10个字节,非常不划算。zigzag编码的原理是将有符号整数映射为无符号整数,使得负数的二进制数值也能用较少的bit位表示。它通过移位来实现映射。由于补码的符号位在最高位,对于负数,符号位为1,这导致varint压缩编码无法压缩,需要最大变长字节来存储,因此首先将数据位整体循环左移1位,最低位空出留给符号位使用,另外,对于实际使用中,绝对值小的负数应用场景比绝对值大的负数应用场景大的多,但绝对值小的负数的前导1更多(如-1,全是1),因此对于负整数,再把数据位按取反规则操作,将前导1置换为0,以达到可以通过varint编码能有效压缩的目的。最终经过zigzag编码后绝对值小的正负整数都能编码为绝对值相对小的正整数,编码效果如下:5.4.3.Base系列有的字符在一些环境中是不能显示或使用的,比如,=等字符在URL被保留为特殊作用的字符,比如一些二进制码如果转成对应的字符的话,会有很多不可见字符和控制符(如换行、回车之类),这时就需要对数据进行编码。Base系列的就是用来将字节编码为ASCII中的可见字符的,以便能进行网络传输和打印等。Base系列编码的原理是将字节流按固定步长切片,然后通过映射表为每个切片找一个对应的、可见的ASCII字符,最终重组为新的可见字符流。Base16也称hex,它使用16个可见字符来表示二进制字符串,1个字符使用2个可见字符来表示,因此编码后数据大小将翻倍。Base32使用32个可见字符来表示二进制字符串,5个字符使用8个可见字符表示,最后如果不足8个字符,将用“=”来补充,编码后数据大小变成原来的8/5。Base64使用64个可见字符来表示二进制字符串,3个字符使用4个可见字符来表示,编码后数据大小变成原来的4/3。Base64索引表如下:笔者曾经用lua实现过Base64算法,有兴趣可以前往阅读。5.4.4.百分号编码百分号编码又称URL编码(URLencoding),是特定上下文的统一资源定位符(URL)的编码机制,实际上也适用于统一资源标志符(URI)的编码。百分号编码同样也是为了使URL具有可传输性,可显示性以及应对二进制数据的完整性而进行的一种编码规则。百分号编码规则为把字符的ASCII的值表示为两个16进制的数字,然后在其前面放置转义字符百分号“%”。URI所允许的字符分作保留与未保留。保留字符是那些具有特殊含义的字符,例如:斜线字符用于URL(或URI)不同部分的分界符;未保留字符没有这些特殊含义。以下是RFC3986中对保留字符和未保留字符的定义:百分号编码可描述为:未保留字符不需要编码如果一个保留字符需要出现在URI一个路径成分的内部,则需要进行百分号编码除了保留字符和未保留字符(包括百分号字符本身)的其它字符必须用百分号编码二进制数据表示为8位组的序列,然后对每个8位组进行百分号编码
6.加密与校验6.1.CRCCRC循环冗余校验(Cyclicredundancycheck)是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。生成的数字在传输或者存储之前计算出来并且附加到数据后面,然后接收方进行检验确定数据是否发生变化。它是一类重要的线性分组码,编码和解码方法简单,检错和纠错能力强,在通信领域广泛地用于实现差错控制。CRC是两个字节数据流采用二进制除法(没有进位,使用XOR来代替减法)相除所得到的余数。其中被除数是需要计算校验和的信息数据流的二进制表示;除数是一个长度为(n+1)的预定义(短)的二进制数,通常用多项式的系数来表示。在做除法之前,要在信息数据之后先加上n个0。CRC是基于有限域GF(2)(即除以2的同余)的多项式环。简单的来说,就是所有系数都为0或1(又叫做二进制)的多项式系数的集合,并且集合对于所有的代数操作都是封闭的。6.2.奇偶校验