吃透Redis(十一):Redis原子性的保证

redis原子性保证

Redis server 一旦和一个客户端建立连接后,就会在事件驱动框架中注册可读事件,这就对应了客户端的命令请求。而对于整个命令处理的过程来说,我认为主要可以分成四个阶段:

  • 命令读取
  • 命令解析
  • 命令执行
  • 结果返回

这四个阶段在 Redis 6.0 版本前都是由主 IO 线程来执行完成的。虽然 Redis 使用了 IO 多路复用机制,但是该机制只是一次性获取多个就绪的 socket 描述符,对应了多个发送命令请求的客户端。而 Redis 在主 IO 线程中,还是逐一来处理每个客户端上的命令的,所以命令执行的原子性依然可以得到保证。

而当使用了 Redis 6.0 版本后,命令处理过程中的读取、解析和结果写回,就由多个 IO 线程来处理了。不过你也不用担心,多个 IO 线程只是完成解析第一个读到的命令,命令的实际执行还是由主 IO 线程处理。当多个 IO 线程在并发写回结果时,命令就已经执行完了,不存在多 IO 线程冲突的问题。所以,使用了多 IO 线程后,命令执行的原子性仍然可以得到保证。

为什么并发IO线程读写还能保证处理的原子性?

答:主线程负责把read pending队列中的数据放入到这些IO线程的io_threads_list队列,并且处理io_threads_list[0]也就是主线程处理IO操作,处理完成之后,主线程自旋等待IO线程处理完之后,才开始一个个执行命令,所以保证了原子性。
看源码:

void beforeSleep(struct aeEventLoop *eventLoop) {
    ...
    // 处理read pending队列的客户端队列
    handleClientsWithPendingReadsUsingThreads();
    ...
}

int handleClientsWithPendingReadsUsingThreads(void) {
    // 获取clients_pending_read队列列表迭代器
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_read,&li);
    int item_id = 0;
    
    // 一,放入不同的IO线程中
    // 遍历所有待读取的客户端,并将其散列到不同IO线程处理列表中
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        // 通过取余方式散列获取IO线程下标
        int target_id = item_id % server.io_threads_num;
        // 将该客户端放入该下标列表中
        listAddNodeTail(io_threads_list[target_id],c);
        item_id++;
    }
    // 所有连接放入到IO线程处理列表后将IO线程操作标识为IO_THREADS_OP_READ读操作
    io_threads_op = IO_THREADS_OP_READ;
    for (int j = 1; j < server.io_threads_num; j++) {
        // 设置io_threads_pending为非零数,也即当前需要处理的客户端数量,这时线程将会响应该操作,开始处理客户端连接
        int count = listLength(io_threads_list[j]);
        io_threads_pending[j] = count;
    }
    
    // 二、处理主线程的IO读写时间
    //io_threads_list数组0下标处为main线程处理,也即main线程处理一部分读IO
    listRewind(io_threads_list[0],&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        readQueryFromClient(c->conn);
    }
    // 清空主线程负责的下标为0的客户端列表,其他的下标由IO线程自己处理
    listEmpty(io_threads_list[0]);
    
    // 三、自旋等待所有IO线程全部处理完
    // 自旋等嗲其他线程处理IO完毕
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += io_threads_pending[j];
        if (pending == 0) break;
    }
    
    // 四、执行命令
    // 当所有IO线程将clients_pending_read的客户端读IO处理完毕后,在主线程中处理客户端命令
    while(listLength(server.clients_pending_read)) {
        ln = listFirst(server.clients_pending_read);
        client *c = listNodeValue(ln);
        // 去掉CLIENT_PENDING_READ标志位,并将其从clients_pending_read队列中移除
        c->flags &= ~CLIENT_PENDING_READ;
        listDelNode(server.clients_pending_read,ln);
        // 如果设置暂停客户端请求那么继续循环
        if (clientsArePaused()) continue;
        // 处理客户端命令
        if (processPendingCommandsAndResetClient(c) == C_ERR) {
            continue;
        }
        processInputBuffer(c);
        // 如果处理完毕且有数据需要写回,那么将客户端放入clients_pending_write队列等待IO线程完成写操作
        if (!(c->flags & CLIENT_PENDING_WRITE) && clientHasPendingReplies(c))
            clientInstallWriteHandler(c);
    }
    server.stat_io_reads_processed += processed;
    return processed;
}
  • 客户端A 先发起请求1,后客户端B发起请求2,服务端【无法保证】先接收到 请求1后接收到请求2,因为网络传输时间不同。
  • 客户端A 先发起请求1,后客户端A再次发起请求2,服务端 【可以保证】 先接收到请求1后接收到请求2,这个由TCP来保证。
  • 服务端先接收到请求1,后接收到请求2,在多io环境下,redis【可以保证】先执行请求1后执行请求2。请求会先放到列表里,多IO线程从列表依次获取请求,进行命令读取及解析,待所有IO线程都处理完成之后,主线程才开始按序执行命令。

我们可以简单地用一段话来描述Redis的请求处理流程:Redis主线程一次性获取最大为1000个客户端连接,将其放入到read pending队列中,在下一次aeMain主循环中调用beforeSleep函数,该函数将read pending队列和write pending队列中的客户端散列到IO线程中执行读写操作,并且自身负责下标为0处的客户端,然后等待IO线程执行 read、write 完毕后再执行。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃透Java

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值