[论文笔记] SIGKDD HeavyGuardian: Separate and Guard Hot Items in Data Streams

先附上论文代码


读了一遍这篇文章之后有如下几个疑问,待进一步学习与思考:
1、概率化好神奇(需要数学去支撑)
2、heavy部分存储ID是因为heavy部分值得去关注吗? (正解)
3、按照所说的cold items有很多,后面直接hash的方式不会占用很大内存吗?light part不会用完吗?(所以light part设置的计数器的bit数会更小)
4、一个hot被踢出来了不会再进入light。来一个新的item会试图把它往heavy里面塞,light里面的cold数据实际上是会小于实际次数的(正解)
5、要是用这种算法的,感觉也很容易攻击
6、复习一下二项分布和泊松分布叭
Generally, these tasks are measured within fixed-size time windows.
除了五个提到的,还有这些stream processing tasks
Super-Spreader
DDoS victims
top-k frequent items
hierarchical heavy hitters
更关注hot items,实践中,大多数都是cold items,准确记录大量的cold items占用太多内存,内存紧张的时候还会对hot items的估计带来不可忽略的错误。但是又不能不记录啊,因为cold和hot是转换的过程。
这个人也写了阅读笔记,但是和我完全不是一个画风,我就是瞎逼逼,然后看到他给了算法流程我就不想写了,我是什么猫饼?等做报告前再补上去叭???做报告的时候还要看论文说明这个总结一点也不OK。下次一定一定不要犯懒!

为了实现高效性和有效性,一种巧妙(elegant)的方法是use a compact data structure to
keep and guard the information (item ID and frequency) of hot items and efficiently record the frequencies of cold items. 问题的关键在于如何实时判断来的item是hot还是cold,很难在小内存中以高准确度记录items

已有的数据结构采用record-all-evict-cold的策略,核心思想是一开始记录所有items的频次,然后剔除cold items。两个经典(notable)的算法是Space-Saving和Augmented sketch。

Space-Saving用一个有序列表Stream-Summary来维护每个到达的item,该数据结构可以在O(1)实现插入、删除、查找coldest item。来了一个e,如果在列表里,频次+1,否则,它替换掉coldest item,设它的频次是 f ^ m i n \hat{f}_{min} f^min,那么e的频次记做 f ^ m i n + 1 \hat{f}_{min}+1 f^min+1 ??凭啥哟,不是很有可能cold频次增长超过hot从来挤掉hot吗。许多hot items被存在Stream-Summary中,cold items被剔除。但是该算法有两个局限性:不能记录cold items的频次;一些后来的cold items会被“高估”,从而留在列表里,(我就说嘛)。因此要记录前k大,就必须存储m个,m远大于k

Augmented sketch是二层节后,以底层是一个小数组存 δ \delta δ和hot items,第二个是一个经典的sketch(CM sketch)存所有item的frequency,后来的hot items启初被插入第二个sketch,然后被交换到第一个stage。
Augment Sketch的问题在于,对于每一个来的item,filter中的查找是挨个查的,因此比较慢,作者建议存32个hot items。但是在流任务中,需要报告上千个hot items,会导致filter和sketch频繁地互换,first stage就像一个小的cache。

总的来说有两个缺点:

  • 内存利用率不高。Space-Saving额外消耗m-k。Augment Sketch的sketch需要大量的计数器去存储所有items的频次。
  • hot items的信息没有很好地保存。SS的频次比实际值要大很多,AS只能精确记录一开始的几个hot items

our proposed solution

将数据流问题简化成:给定数据流,如何用很少的cells来准确测量最热门的item的频次。
分离hot和cold,用大的计数器保存hot的频次信息,用小的计数器来记录cold items。
算法基于 King(频次最高的) guardian(除了King以外在heavy part的) rebel(和King与guardian不一样的)这么个思想做了一些改进。 (这个思想直接看算法如何实现的)

  • 将数据流分成多个子流,每个子流选择一个King和guardian(实际上就是用函数选择一个bucket,bucket里分为heavy part和light part,即保皇党和反叛党)
  • 用小计数器去存储rebels的频次
    这个小故事讲的真的很花里胡哨。我一点也不想记录。
    在这里插入图片描述
    比最前沿的Space Saving误差小了 6.24 ∗ 1 0 4 ∼ 4.02 ∗ 1 0 6 6.24 * 10^{4} \sim 4.02 * 10^{6} 6.241044.02106倍,每个不同的item只用了0.005bit(这一点我并没有从figure 10(a)中看出来)?

main contributions

  • intelligently separate and guard the information of hot items and approximately record the frequencies of cold items in a data stream
  • derive the formula of the error bound 推导了误差界限的公式
  • deploy HeavyGuardian on five typical tasks ; much higher accuracy and higher processing speed than the state-of-the-art at the same time. 将HG用在五种典型的任务中

Background

在这里插入图片描述
实时频次分布可以支持以下强大的功能:
IP services provider可以根据估计的频次分布来推断the usage of the network,可以用来调整服务策略。实时的可以让服务提供商立即调整,给用户带来更好的体验。
熵的变化可以检测出流中的异常行为,可以用来做异常检测。

Frequency Estimation

Sketch用小小的误差就可以实现高速、内存利用高效,因此常用。
Typical sketches include Count sketches [22], Count-min (CM) sketches [23], CU sketches [15], Augmented sketch framework [12], Pyramid sketch framework [13], and more [21, 24].

Count,CM,CU使用equal-Sized计数器去记录频次,但是hot items需要足够大的counter,而cold items数量多,counters记录值小,造成了很多bit的浪费。
Augmented sketch framework 用过滤器来准确记录32个hot items。提高有限。
Pyramid sketch framework,是最前沿的算法,可以根据来的item自动增大计数器的大小,也被证明有更高的准确度和速度。
但是hot items需要不少memory accesses,PS的插入速度在最坏情况是很慢的。

heavy hitter detection

sketch based algorithms:使用sketch去记录所有items的频次,最小堆去找前K大,需要很多内存去记录所有频次(主要是cold 太多了,用的counter还和hot的一样大)。当memory space比较小的时候,accuracy会迅速下降
counter based algorithms:Space-Saving,Frequent [29], and Lossy counting,Space Saving最常用,这三个算法类似。

heavy change detection

k-ary sketch基于CM sketch,在memory space很大的时候有很高的准确度。但是需要知道所有item的ID。
reversible sketch基于k-ary sketch,解决了上述问题,以一定复杂度对item ID进行解码?

Real-time Frequency Distribution & Real-time Entropy

frequency distribution常用算法是MRAC,FlowRadar。但是他们不能实时估计。
在流中熵被定义为 ∑ e f e N log ⁡ 2 f e N \sum_{e} \frac{f_{e}}{N} \log _{2} \frac{f_{e}}{N} eNfelog2Nfe,其中 f e f_{e} fe是e的频次,n是所有items的总数。The most notable algorithm is proposed by Lall et al.[43], which uses sampling and simple mathematical derivation to estimate the entropy.具有很高的准确度、内存利用、处理速度。
本文是第一个支持实时估计的。

算法实现

先把东西都尝试往heavy part塞。如果已经在heavy part里,直接加1,如果不在,有空位置就放入空位置,没用空位置就以一定的概率去把weakest guardian -1,如果减到0了就自己进去,置为1,否则插入light part,直接在hash得到的位置+1。
在这里插入图片描述
在这里插入图片描述
概率性减一,如果减为0 了踢掉,这里如果踢掉了信息就会丢失的,被踢者不会进入light part。
在heavy part和light part的hash函数是一样的,来一个item只需要做一次hash。
查询的时候,先hash得到heavy part的桶,遍历cell寻找,如果找不到,再根据hash值直接找light part,找不到就是没有。

由于hot items被踢出的概率很小,因此他们的频次很接近准确值
the heavy part of each bucket in HeavyGuardian seldom records cold items, but records and guards the frequencies ofhot items.

关于处理速度:

  • 处理hot item只需要在heavy part里遍历cells,this is fast because these cells can fit into a cache line
  • 处理cold item 只需要再多访问一次light part
    因此时间复杂度是O(1)

optimization

当item ID比较大的时候,用指纹来代替ID。这时HG的内存使用与item ID无关。但是哈希函数选的合适指纹碰撞的概率是很低的

数学分析

Proof of no Over-estimation Error

纯文字,很好想

The Error Bound of the Heavy Part of HeavyGuardian

纯数学

heavyguardian deployment

frequency estimation

heavy部分记录hot,light部分记录cold

Heavy Hitter Detection and Heavy Change Detection

用HeavyGuardian在heavy part存储hot items,在备用列表里记录hot items的ID。在这两种应用场景下,不需要记录code item,因此 λ l = 0 \lambda_l=0 λl=0

Heavy Hitter Detection:

插入:对于来的item e,先把它插入HeavyGuardian,之后得到e的估计频次 f ^ e \hat{f}_e f^e,如果 f ^ e = T \hat{f}_{e}=\mathcal{T} f^e=T,就把e插入备用列表B
注意:条件是 f ^ e = T \hat{f}_{e}=\mathcal{T} f^e=T,而不是大于。HG记录了每个hot items的指纹和频次,当不考虑指纹碰撞的时候,频次是一点一点加上去的,我们在B中只存储列表一次,在最坏的情况下,一个cold item和a heavy hitter指纹碰撞被映射搭配同一个bucket中,当且仅当cold item到达时,存的频次是 T − 1 \mathcal{T}-1 T1,cold item才会被存进去。这种概率非常小。如果条件是大于的话,就有可能存入了cold item。
查询:遍历列表B。从HG上获得 f ^ e \hat{f}_e f^e f ^ e > = T \hat{f}_{e}>=\mathcal{T} f^e>=T,就输出e

Detecting Heavy Changes

在数据流的两个相邻的时间窗中,每个时间窗用一次HG,如果两次计算的heavy hitter的 f ^ e \hat{f}_e f^e差值大于给定阈值,就认为是a heavy change。

Real-time Frequency Distribution & Real-time Entropy

用HG去维护所有items的频次,用备用counters数组an auxiliary array Dist of counters维护频次分布。Dist有y个counters, i t h i_{th} ithcounter记录了频次为i的items的数量。每来一个item e,先插入HG,之后得到 f ^ e \hat{f}_e f^e,将 f ^ e t h \hat{f}_e^{th} f^ethcounter数值+1, ( f ^ e − 1 ) t h (\hat{f}_e-1)^{th} (f^e1)thcounter数值-1。这样实现了实时的频次估计。
∑ i = 1 y Dist ⁡ [ y ] ⋅ y N log ⁡ 2 y N \sum_{i=1}^{y} \operatorname{Dist}[y] \cdot \frac{y}{N} \log _{2} \frac{y}{N} i=1yDist[y]Nylog2Ny就能实时获得熵。

HG是通过增加一个备用数组来实现实时地预测,并且具有很高的accuracy。

数据流大概是100w

heavy hitter部分代码

//该代码用于实现heavy hitter detection 无light part部分
#ifndef _HeavyGuarding_H
#define _HeavyGuarding_H

#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <string>
#include <cstring>
#include "BOBHash32.h"
#include "BOBHash64.h"
#define G 8
#define HK_b 1.08
using namespace std;
class HeavyGuarding
{
    private:
        struct node {int C; unsigned int FP;} HK[1000005][G+2]; //结构体二维数组 
        BOBHash32 * bobhash;
        int M,p;												//M应该是不同文件的种数?不对啊,那这样要提前知道有多少种文件,根据main函数来看是可以开的数组大小?
    public:
        int cnt;
        string ans[1005];
        void ADD(string x) {ans[++cnt]=x;}
        HeavyGuarding(int M,int p,int prm):M(M),p(p) {cnt=0; bobhash=new BOBHash32(prm);}
        void Insert(string x)
        {
            unsigned int H=bobhash->run(x.c_str(),x.size());
            unsigned int FP=(H>>16),Hsh=H % M;
            bool FLAG=false;
            for (int k=0; k<G; k++)	  							//heavy part 先hash算出是哪个桶,再遍历桶里的位置去找存在了哪。
            {
                int c=HK[Hsh][k].C;  							//Hsh是映射到的桶是哪一个
                if (HK[Hsh][k].FP==FP)  					    //case 1:如果已经在heavypart,直接加1
                {
                    HK[Hsh][k].C++;   						   //显然c是频次,FP是hash后字符串对应的码 FP fingerprint
                    if (HK[Hsh][k].C==p) ADD(x); 			   //到达阈值,添加入答案
                    FLAG=true;
                    break;
                }
            }
            if (!FLAG)                                        //不在heavy part中
            {
                int X,MIN=1000000000;
                for (int k=0; k<G; k++)                       //如果c=0,那么一定这个位置会以1的概率换上新的item,这里是把case 2 和 3写在一起了
                {
                    int c=HK[Hsh][k].C;
                    if (c<MIN) {MIN=c; X=k;} //找到weakest guardian 
                }
                if (!(rand()%int(pow(HK_b,HK[Hsh][X].C))))    //case 2,3:Exponential Decay 
                {
                    HK[Hsh][X].C--;
                    if (HK[Hsh][X].C<=0)
                    {
                        HK[Hsh][X].FP=FP;
                        HK[Hsh][X].C=1;
                    }										//找不到就不处理了呗
                }
            }
        }
        int Query(string x)
        {
            unsigned int H=bobhash->run(x.c_str(),x.size());
            unsigned int FP=(H>>16),Hsh=H % M;			//
            for (int k=0; k<G; k++)
            {
                int c=HK[Hsh][k].C;
                if (HK[Hsh][k].FP==FP) return HK[Hsh][k].C;
            }
            return 0;
        }
};
#endif

real-time distribution部分代码

//此部分用于实现real-time distribution
#ifndef _HG_distribution_H
#define _HG_distribution_H

#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <string>
#include <cstring>
#include "BOBHASH32.h"
#include "BOBHASH64.h"
#define HK_b 1.08
#define G 8
#define ct 32  // the number of cold items for each bucket
using namespace std;
class HG_distribution
{
    private:
        BOBHash32 * bobhash;
        int M;
    public:
        int SUM[32769];
        struct node {int C,FP;} HK[1000005][20]; 
        int ext[1000005][40];	
        HG_distribution(int M,int prm):M(M) {for (int i=0; i<32768; i++) SUM[i]=0; bobhash=new BOBHash32(prm);}	//SUM用来存储频次i出现的次数
        void Insert(string x)
        {
            unsigned int H=bobhash->run(x.c_str(),x.size());
            unsigned int FP=(H>>16),Hsh=H % M;
            bool FLAG=false;
            for (int k=0; k<G; k++)
            {
                int c=HK[Hsh][k].C;
                if (HK[Hsh][k].FP==FP) 
                {
                    if (HK[Hsh][k].C<32768 && SUM[HK[Hsh][k].C]) SUM[HK[Hsh][k].C]--; //频次最大是32768啊
                    HK[Hsh][k].C++;
                    if (HK[Hsh][k].C<32768) SUM[HK[Hsh][k].C]++;
                    FLAG=true;
                    break;
                }
                if (FLAG) break;
            }
            if (!FLAG)	
            {
                int X,MIN=1000000000;
                for (int k=0; k<G; k++)
                {
                    int c=HK[Hsh][k].C;
                    if (c<MIN) {MIN=c; X=k;}
                }
                if (!(rand()%int(pow(HK_b,HK[Hsh][X].C))))
                {
                    if (HK[Hsh][X].C<32768 && SUM[HK[Hsh][X].C]) SUM[HK[Hsh][X].C]--;
                    HK[Hsh][X].C--;
                    if (HK[Hsh][X].C>0 && HK[Hsh][X].C<32768) SUM[HK[Hsh][X].C]++;
                    if (HK[Hsh][X].C<=0)
                    {
                        HK[Hsh][X].FP=FP;
                        HK[Hsh][X].C=1;
                    } else													//如果没能替换掉weakest guardian,那么就加入到light part
                    {
                        int p=Hsh % ct;										//the number of cold items for each bucket
                        if (SUM[ext[Hsh][p]]) SUM[ext[Hsh][p]]--;
                        if (ext[Hsh][p]<16) ext[Hsh][p]++;					//ext看来存的是cold item,最大频次是16
                        SUM[ext[Hsh][p]]++;
                    }
                }
            }
        }
        int Query(string x)
        {
            unsigned int H=bobhash->run(x.c_str(),x.size());
            unsigned int FP=(H>>16),Hsh=H % M;
            for (int k=0; k<G; k++)
            {
                int c=HK[Hsh][k].C;
                if (HK[Hsh][k].FP==FP) return max(1,HK[Hsh][k].C);
            }
            int p=Hsh % ct;
            return max(1,ext[Hsh][p]);
        }
};
#endif
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <string>
#include <cstring>
#include <fstream>
#include <map>
#include "BOBHASH32.h"
#include "BOBHASH64.h"
#include "HG_distribution.h"
#include "CMSketch.h"
using namespace std;
int True[100005],HG_now[100005],CM_now[100005];
ifstream fin("u1",ios::in|ios::binary);
char a[105];
int HG_M,CM_M;
string Read()
{
    fin.read(a,13);
    a[13]='\0';
    string tmp=a;
    return tmp;
}
string ss[10000005];
map <string ,int> B,C;
int main()
{
    //read
    int m=10000000;  //10M表项
    B.clear();
    for (int i=1; i<=m; i++) {ss[i]=Read(); B[ss[i]]++;}

    //set
    int MEM=512;
    for (HG_M=1; (32*G+ct*4)*HG_M+32768*16<=MEM*1024*8; HG_M++); //HG_M是给定内存,能开多大的HK数组? 32768*16是什么? 感觉应该是bit数,但是咋算的啊,2^32=32768 2^4=16是heavy和light counter 占用的bit数
    HG_distribution * HG=new HG_distribution(HG_M,1000);         //后面的1000是BobHash32的参数
    for (CM_M=1; 16*CM_M+32768*16<=MEM*1024*8; CM_M++);
    CMSketch * CM=new CMSketch(CM_M);

    //Insert
    for (int i=1; i<=m; i++)
    {
        string s=ss[i];
        HG->Insert(s);
        CM->Insert(s);
    }

    //answer
    for (int i=1; i<32768; i++) True[i]=HG_now[i]=CM_now[i]=0;
    for (map<string,int> :: iterator sit=B.begin(); sit!=B.end(); sit++)
    {
        True[sit->second]++;
        HG_now[HG->Query(sit->first)]++;
        CM_now[CM->Query(sit->first)]++;
    }
    return 0;
}

代码中的困惑

Q :那个HG_M是啥?M?
Q :开那么多bucket不会占内存吗?A:“利用开辟二维地址空间,多重散列等技术减少散列冲突,提高测量结果的准确度。”

代码没我想的那么复杂,heavy part和light part用两个二维数组实现的,手动设定阈值来决定它们的counter大小
二维数组第一维是mapped bucket,用bobhash32的值mod M得到的
二维数组第二维是在该bucket中是第几个hot / cold item

heavy part 二维数组结构体,存的值是fingerprint和counter,fingerprint是bobhash32前16位,counter阈值是32768
light part 存的就是cold item的counter,counter阈值是16
还是先看后面的论文叭,I will return someday.

I come back. 2019/12/5

experimental results

experiment setup

Dataset

  • CAIDA :猜哒?由IP包组成,每个IP包由源IP地址和目的IP地址作为标识。数据集中包含10M表项,大概有4.2M不同的表项
  • Web page :数据集是爬一个HTML文档得到的,10M items,大概有0.4M不同的
    同时也测试了人工生成的不同偏度的数据集。结果展示在技术报告中。

Implementation

C++实现,用不同的hash函数性能都差不多,这里用了Bob hash
All the programs are run on a server with dual 6-core CPUs (24 threads, Intel Xeon CPU E5-2620 @2 GHz) and 62 GB total system memory. The cache size is typically several megabytes. To preserve cache memory to other important tasks, we keep the memory size in our experiment under 1000KB.

指标

  • precision:真正例/(真正例+假正例)
  • recall:真正例/(真正例+假反例)
  • ARE(average relative error) 1 M ∑ e j ∈ Ω ∣ f ^ j − f j ∣ f j \frac{1}{M} \sum_{e_{j} \in \Omega} \frac{\left|\hat{f}_{j}-f_{j}\right|}{f_{j}} M1ejΩfjf^jfj, M是不同文件数 。描述估计的频次的准确度。
  • AAE (average absolute error)描述估计频次的准确度或是数据流两个相邻时间窗的不同。 1 M ∑ e j ∈ Ω ∣ f ^ j − f j ∣ \frac{1}{M} \sum_{e_{j} \in \Omega}\left|\hat{f}_{j}-f_{j}\right| M1ejΩf^jfj 1 M ∑ e j ∈ Ω ∣ d ^ j − d j ∣ \frac{1}{M} \sum_{e_{j} \in \Omega}\left|\hat{d}_{j}-d_{j}\right| M1ejΩd^jdj,其中 d j d_j dj d ^ j \hat{d}_{j} d^j只用在heavy change detection, d j d_j dj时候真实的频次差, d ^ j \hat{d}_{j} d^j是预估的频次差
  • Throughput: 衡量算法处理速度。 N / T N / T N/T,N是items的条数,T是消耗的时间。用一秒多少百万条插入来衡量throughput。We use Million of insertions algorithm to measure the throughput.

系统参数实验

实验测试不同HG参数对算法性能的影响,聚焦于指数函数的底数b,每个桶的guardian数目 λ h \lambda_h λh,指纹长度 l l l
这三个参数只和heavy part有关,只关注hot items的准确度忽略cold items。
将内存大小设置为100KB使用CAIDA数据集,AAE来测量误差
只用了一个数据集,是不是换了就不一样了
Effects of b (Figure 4(a)):b=1.08
在这里插入图片描述
Effects of λ h \lambda_h λh (Figure 4(b)) λ h = 8 \lambda_h=8 λh=8
在这里插入图片描述
Effects of l : l=16
在这里插入图片描述
至于 λ l \lambda_l λl,随着它的增大,cold items的准确度提高,但是hot items准确度降低?为了平衡,将heavy part和light part内存设置的一样大。在我们的实现中,分别16bit和4bit,也就是 λ l = 64 \lambda_l=64 λl=64 (644=816*2)(*2是因为计数器16bit,footprint 16bit)
我想知道有什么数学依据吗?换个数据集是不是就不一样了?
这里插入一下
在这里插入图片描述

w是bucket的个数
b是指数函数的底数
N是所有item数

experiments on Stream Processing Tasks

Experiments on Frequency Estimation

指标: AAR,ARE,throughput
比较对象:CM sketch,基于Pyramid Sketch 框架的CU sketch(速度和准确率都比别的高)
控制变量:memory size 100K-1000K (ACM里一般都给32768K)
在这里插入图片描述
厉害厉害,每个图大概100个词

在这里插入图片描述
内存大小设为1000K,速度也很快

Experiments on Heavy Hitter Detection & Heavy Change Detection

指标: precision,recall,ARE
比较对象:CM sketch + min-heap d=4最小堆大小为30KB,Space-Saving, ASketch (Heavy Hitter Detection)
k-ary sketch, ASketch (heavy change detection) d=4
Moreover, the width of the sketch, the width of the HeavyGuardian, and the number of buckets in Space-Saving, are computed based on the total memory size.
控制变量:memory size
20KB to 100KB for Space-Saving and HeavyGuardian, but vary the memory size from 40KB to 100KB for sketch+min-heap and ASketch, because they require 30KB for the min-heap

在这里插入图片描述
precision=100%,只要它说是heavy hitter,就一定是heavy hitter
在这里插入图片描述
recall 预测出来的heavy hitter总数随着memory size的增大增至100%
在这里插入图片描述
Heavy change detection
vary the memory size from 20KB to 100KB for Space-Saving and HeavyGuardian, but vary the memory size from 40KB to 100KB for sketch+min-heap and ASketch, because they require 30KB for the min-heap.
在这里插入图片描述
The insertion speed of HeavyGuardian for heavy change detection is same as that for heavy hitter detection. Due to space limitation, we omit speed evaluation for heavy change detection.

Experiments on Real-time Frequency Distribution Estimation & Entropy Estimation

由于本文是首次实现的,只和naive algorithms相比,use a CM sketch or an ASketch to record frequencies ofall items, and update the frequency distribution and the entropy based on counters in the CM sketch or the ASketch in real time.

set the memory size to 128KB, 256KB, and 512KB for HeavyGuardian, and 512KB for the CM sketch and ASketch
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值