Redis大Key删除真的会造成主线程阻塞吗(做实验来证明!)

背景

  • 网上很多关于redis的话题都谈到了要避免造成大key,因为删除会造成主线程阻塞。看到过一个评论说测试删除2G的一个大key,系统阻塞了大概80秒的时间。
  • 曾经面试时被问到如何删除一个大key。
  • 最近在阅读redis5.0.8源码,看到其中关于大key的删除,实际上只是在主线程删除了key相关的数据,而实际的value及其内存释放是放在异步删除的线程进行的。这种操作应该不会像网上所说,造成80秒主线程阻塞那么恐怖。
测试准备
  • redis5.0.x服务端程序一份(方便起见,此处使用docker构建)。
  • php7.3(无他,系统自带7.3版本),也可使用其它脚本语言。
  • c编译器,添加数据的程序使用的c开发(因为自带的php没安装pcntl扩展=,=,所以使用c的多线程来并发的构造和添加数据),这里主要用于构造数据,可以使用其它语言。
构造数据
  • 编写数据构造代码,此处基于macos平台编写,可根据实际操作系统稍作调整。
  • 写的比较粗糙,添加不同数量的数据以及修改服务端信息都需要修改宏定义,然后重新编译。
  • #include #include 【Redis大Key删除真的会造成主线程阻塞吗(做实验来证明!)】#include #include #include #include #include #include #include #include #include #include #include #include #define RESV_BUFF_SIZE 512 #define SEND_BUFF_SIZE 1024 /** * redis服务端的IP地址 */ #define DEST_ADDR "172.20.23.83" /** * redis服务端的端口号 */ #define DEST_PORT 6379 /** * 每个线程总共执行插入动作的次数 */ #define TRANS_PER_THREAD 10000 /** * 每隔多少次插入进行一次日志打印 */ #define LOG_STEP 255 /** * 总共开启线程数量 */ #define THREADS_NUM 100int numLen(int num) { int i = 0; do { num /= 10; i++; } while (num != 0); return i; }/** * 线程执行代码 * @param p 从0-99的线程编号,用于确定当前线程执行插入的数据段 * 比如编号为1,则插入TRANS_PER_THREAD*1 至 TRANS_PER_THREAD*1 + TRANS_PER_THREAD 区间的数据 */ void threadMain(void *p) { int sockfd,*index=p,rlen; *index *= TRANS_PER_THREAD; int end = *index + TRANS_PER_THREAD; struct sockaddr_in dest_addr; bzero(&(dest_addr), sizeof(dest_addr)); char resvbuf[RESV_BUFF_SIZE]; char sendbuf[SEND_BUFF_SIZE]; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { printf("socket create failed:%d!\n",sockfd); }dest_addr.sin_family = AF_INET; dest_addr.sin_port = htons(DEST_PORT); inet_pton(AF_INET,DEST_ADDR,&dest_addr.sin_addr); if (connect(sockfd,(struct sockaddr*)&dest_addr, sizeof(struct sockaddr)) == -1) { printf("connect failed:%d!\n",errno); perror("error: "); } else { printf("connect success!\n"); struct timeval tv,ltv; for (int i = *index; i < end; ++i) { rlen = numLen(i); memset(sendbuf,0,SEND_BUFF_SIZE); /** * 构造当次请求发送的数据 * 需满足RESP协议规范 */ sprintf(sendbuf,"*4\r\n$4\r\nHSET\r\n$6\r\nmigkey\r\n$%d\r\nmest%d\r\n$%d\r\nmest67890%d\r\n",rlen+4,i,rlen+9,i); write(sockfd,sendbuf,strlen(sendbuf)); /** * 虽然我们并不关心服务端的返回 * 此处依然进行了读取操作,避免缓冲堆满 */ read(sockfd,resvbuf,RESV_BUFF_SIZE); /** * 每执行LOG_STEP+1次就打印一条日志信息 * LOG_STEP需满足(2^n - 1) */ if ((i & LOG_STEP) == 0) { gettimeofday(&tv, NULL); if (i > *index) { printf("used %ld.%d seconds.\n",tv.tv_sec - ltv.tv_sec,tv.tv_usec - ltv.tv_usec); fflush(stdout); } ltv = tv; } } }printf("finished index:%d.\n",*index); close(sockfd); }void handle_pipe(int sig) { //printf("sig %d ignore.\n",sig); }int main() { /** * 由于tcp的client关闭后,服务端仍然有可能向我们发送数据 * 这样会造成我们的进程收到SIGPIPE信号 * 所以此处注册信号处理函数 */ struct sigaction sa; sa.sa_handler = handle_pipe; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE,&sa,NULL); pthread_t pts[THREADS_NUM]; int indexs[THREADS_NUM]; struct timeval tv,ltv; gettimeofday(<v, NULL); /** * 创建线程 */ for (int i = 0; i < THREADS_NUM; ++i) { indexs[i] = i; if (pthread_create(&pts[i], NULL, threadMain, &indexs[i]) != 0) { printf("create thread error!\n"); } else { printf("thread %d created!\n",i); } } /** * 等待线程 */ for (int i = 0; i < THREADS_NUM; ++i) { pthread_join(pts[i], NULL); }gettimeofday(&tv, NULL); printf("\n------All finished!------\n""used %ld.%d seconds.\n",tv.tv_sec - ltv.tv_sec,tv.tv_usec - ltv.tv_usec); return 0; }

  • 编译并执行,此处通过修改代码以及多次执行,总共向服务端添加了3个大key。分别为:
  • bigkey,数据长度100万,占用内存约58M。
  • migkey,数据长度100万,占用内存约58M。
  • sigkey,数据长度10万,占用内存5.5M。
准备测试程序
  • 测试代码,一个简单的php脚本,每100ms向服务端发送一个get命令。观察在执行了删除大key的命令后,这个get命令执行的延时间隔判断对主线程的阻塞程度。
  • 脚本代码如下(此处是查询一个key为m的string类型数据。):
  • 0.13) { echo "-------------------xxx---------------------\n"; } echo microtime().'--'.$s; usleep(100000); $st = $et; } socket_close($socket);

执行测试
  • 使用redis-cli连接redis服务端。
  • 设置一个key为m的string类型数据。
  • 执行用于观测的php脚本。
  • redis-cli执行删除命令。
  • 关闭php脚本。
  • 查找-------------------xxx---------------------标记。
  • 观测标记前后两次输出的时间间隔长度。
测试结果
  • migkey:
  • 0.27018300 1643271634--$4 mack -------------------xxx--------------------- 0.66599900 1643271634--$4 mack

  • bigkey(后面的两个标记时间间隔虽然超过了0.13秒,但是相差不远,可以确定为网络波动造成。这里应该观察第一个标记的前后时间差。):
  • 0.23842800 1643271538--$4 mack -------------------xxx--------------------- 0.76545200 1643271538--$4 mack -------------------xxx--------------------- 0.90037200 1643271538--$4 mack 0.00909800 1643271539--$4 mack -------------------xxx--------------------- 0.48198300 1643271539--$4 mack

  • sigkey:未观测到波动。
初步结果总结
  • 5.5M的key未造成明显阻塞。
  • 58M左右的key造成约50ms左右的阻塞。
继续构造更大的数据
  • 与网上所说的2G数据差距太大,需要构建更大的数据。
  • 由于通过网络客户端构建数据,即使是多线程,构建效率依旧不高,所以此次大数据采用特别的方式(利用redis-cli带pipe参数)进行构建。
  • 使用php脚本构建一个包含3000万个redis命令文件0.txt,脚本如下:

  • 执行脚本生成0.txt文件。
  • 使用命令 cat 0.txt | redis-cli --pipe 导入数据。
再次观察
  • 通过redis命令查看到此次数据约为1.8G。
  • 执行观测脚本。
  • 删除数据ligkey。
大数据观测结果
  • 0.31861400 1643279405--$4 mack -------------------xxx--------------------- 0.39795600 1643279424--$4 mack

  • 删除1.8G数据耗时约19.08s。
结论
  • 删除大key确实会造成主线程长时间阻塞,且随着数据的增大阻塞时间也变长。
  • 下次笔记中需要从源码的角度理解为何会阻塞主线程以及是否有解决方案。

    推荐阅读