Backend Development 33 min read

Why Is Redis Fast? Unveiling the Real Power of BIO, NIO, and Reactor Models

This article demystifies the true reasons behind Redis's speed by exploring low‑level I/O mechanisms—from basic BIO to NIO and the Reactor model—explaining socket creation, connection handling, blocking behavior, and how Java’s non‑blocking APIs and system calls work together to achieve high‑throughput networking.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Why Is Redis Fast? Unveiling the Real Power of BIO, NIO, and Reactor Models

Hello, I am SanYou. When interviewers ask why Redis is so fast, the simple answer is that Redis is an in‑memory database, but interviewers usually expect a deeper discussion of data structures and network models that sit on top of the memory layer.

1. What Happens During an I/O Operation

Network I/O is implemented via sockets, but before that we review local file I/O. The following Java code copies a file to illustrate a basic I/O operation:

<code>public static void main(String[] args) throws Exception {
    FileInputStream in = new FileInputStream("/tmp/file1.txt");
    FileOutputStream out = new FileOutputStream("/tmp/file2.txt");

    byte[] buf = new byte[in.available()];
    in.read(buf);
    out.write(buf);
}
</code>

The underlying steps are:

in.read(buf) triggers a read() system call.

The OS switches from user mode to kernel mode and copies data into a kernel buffer.

The kernel copies the data from kernel space back to user space.

out.write(buf) writes the data back to the file.

Another context switch moves the data from user space to kernel space for the final write.

This example shows that local I/O also follows a specific I/O model, namely BIO (Blocking I/O).

Local I/O diagram
Local I/O diagram

2. What Is a Socket

Any communication with a remote device goes through the operating system’s protocol stack. The stack implements TCP/IP and other network details. The socket library is the OS‑provided collection of functions that let applications invoke the stack.

Network I/O
Network I/O

The protocol stack code implements TCP/IP and other protocols:

Protocol stack implementation
Protocol stack implementation

The socket library calls the stack; the relationship is shown below:

Socket library and protocol stack relationship
Socket library and protocol stack relationship

When a client creates a socket, the OS allocates a descriptor that uniquely identifies the socket, similar to a “ticket” you receive at a reception desk.

Socket descriptor number plate
Socket descriptor number plate

A TCP connection is a logical bidirectional channel between two computers:

TCP logical channel
TCP logical channel

The four‑tuple (source IP, source port, destination IP, destination port) uniquely identifies a socket.

Redis server socket info
Redis server socket info

3. Blocking I/O (BIO)

3.1 Client Socket Flow

<code>public class BlockingClient {
    public static void main(String[] args) {
        try {
            // Create socket & establish connection
            Socket socket = new Socket("localhost", 8099);
            // Send data to server
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("我是客户端,收到请回答!!\n");
            bufferedWriter.flush();

            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line = bufferedReader.readLine();
            System.out.println("收到服务端返回的数据:" + line);
        } catch (IOException e) {
            // error handling
        }
    }
}
</code>

The call new Socket("localhost", 8099) actually performs two steps:

<code>&lt;descriptor&gt; = socket(&lt;IPv4&gt;, &lt;TCP&gt;, ...);
connect(&lt;descriptor&gt;, &lt;server IP and port&gt;, ...);
</code>

After the socket is created, the OS allocates a descriptor and the connect() call initiates the three‑way TCP handshake.

3.1.1 What Is a Socket?

The socket is the logical endpoint created by the OS; the socket() function returns a descriptor that the kernel uses to identify the endpoint.

3.2 Server Socket Flow

<code>public class BIOServerSocket {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8099);
            System.out.println("启动服务:监听端口:8099");
            while (true) {
                // Wait for client connection (blocking)
                Socket socket = serverSocket.accept();
                System.out.println("客户端:" + socket.getPort());
                // Read client request
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String clientStr = bufferedReader.readLine();
                System.out.println("收到客户端发送的消息:" + clientStr);

                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                bufferedWriter.write("ok\n");
                bufferedWriter.flush();
            }
        } catch (IOException e) {
            // error handling
        }
    }
}
</code>

The server performs four essential steps:

Create a socket.

Bind the socket to a port (listen state).

Accept a client connection.

Read/write data.

<code>// Create socket
&lt;Server descriptor&gt; = socket(&lt;IPv4&gt;, &lt;TCP&gt;, ...);
// Bind port
bind(&lt;Server descriptor&gt;, ...);
// Listen for connections
listen(&lt;Server descriptor&gt;, ...);
// Accept connection
&lt;new descriptor&gt; = accept(&lt;Server descriptor&gt;, ...);
// Read data
&lt;read length&gt; = read(&lt;new descriptor&gt;, &lt;buffer&gt;, ...);
// Write data
write(&lt;new descriptor&gt;, &lt;data&gt;, ...);
</code>

The listen() call marks the socket as passive, allowing the kernel to queue incoming connections (half‑open and fully‑established). The kernel maintains two queues: a half‑connection queue (SYN‑RECEIVED) and an established‑connection queue (ESTABLISHED). When accept() is called, the kernel returns the first socket from the established queue; if the queue is empty, the process sleeps.

listen and 3-way handshake
listen and 3-way handshake

3.3 BIO Blocking Points

The server blocks on serverSocket.accept() and on bufferedReader.readLine() . If no client connects, the accept call never returns; if a client connects but does not send data, readLine() blocks, preventing the server from accepting new connections.

3.4 Improving BIO with Threads

<code>public class BIOServerSocketWithThread {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8099);
            System.out.println("启动服务:监听端口:8099");
            while (true) {
                Socket socket = serverSocket.accept(); // blocking
                System.out.println("客户端:" + socket.getPort());
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            String clientStr = bufferedReader.readLine();
                            System.out.println("收到客户端发送的消息:" + clientStr);

                            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                            bufferedWriter.write("ok\n");
                            bufferedWriter.flush();
                        } catch (Exception e) {
                            // ...
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            // error handling
        }
    }
}
</code>

Each client now gets its own thread, so the main thread can continue accepting new connections. However, the underlying system calls ( accept() , readLine() ) are still blocking; Java cannot change the kernel behavior at the language level.

BIO after threading
BIO after threading

3.5 System Call Tracing with strace

Running strace -ff -o out java BIOServerSocketWithThread shows many system calls. The key calls are:

Socket creation (fd 7).

Binding port 8099.

listen() to set the socket passive.

Printing to stdout (two write calls).

Blocking on poll() , which underlies accept() .

When a client connects with nc localhost 8099 , the poll() returns, accept() yields a new descriptor (fd 8), and the kernel forks a lightweight process (thread) to handle the connection.

Thread handling after accept
Thread handling after accept

The child thread then blocks on recvfrom() , which corresponds to bufferedReader.readLine() in Java.

4. Non‑Blocking I/O (NIO)

Non‑blocking I/O returns immediately from system calls. If no data is ready, the call returns an error code instead of putting the process to sleep.

4.1 Java Non‑Blocking API

<code>public class NoBlockingServer {
    public static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8099));
            while (true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel != null) {
                    socketChannel.configureBlocking(false);
                    channelList.add(socketChannel);
                } else {
                    System.out.println("没有客户端连接!!!");
                }

                for (SocketChannel client : channelList) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int num = client.read(byteBuffer); // non‑blocking read
                    if (num > 0) {
                        System.out.println("收到客户端【" + client.socket().getPort() + "】数据:" + new String(byteBuffer.array()));
                    } else {
                        System.out.println("等待客户端【" + client.socket().getPort() + "】写数据");
                    }
                }
                Thread.sleep(1000); // avoid massive strace output
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
</code>

Calling serverSocketChannel.configureBlocking(false) makes the listening socket non‑blocking; socketChannel.configureBlocking(false) does the same for each accepted connection. The accept() call returns null when no client is pending, and read() returns 0 or -1 when no data is available.

NIO model
NIO model

Java itself does not implement non‑blocking sockets; it merely exposes the kernel’s capability (e.g., SOCK_NONBLOCK flag or fcntl() manipulation).

4.2 Why Non‑Blocking Helps

With a single thread, the server can handle many connections because it never blocks on accept() or read() . The kernel still performs the actual I/O, but the process can continue to poll other sockets.

4.3 System Call Trace for NIO

Running strace on the non‑blocking server shows frequent poll() calls for the listening socket and read() calls for each client socket. The kernel returns immediately when no events are ready.

NonBlocking IO system call analysis
NonBlocking IO system call analysis

4.4 Limitations of Pure Polling

Although non‑blocking I/O avoids thread‑per‑connection overhead, it still incurs CPU waste because the process repeatedly calls accept() and read() even when no data is available. Each system call involves a user‑kernel context switch, which can degrade performance.

4.5 Towards I/O Multiplexing

The natural next step is to let the kernel notify the process when a socket becomes readable or writable, eliminating busy polling. This is the idea behind I/O multiplexing mechanisms such as select , poll , and epoll , which will be covered in later articles.

NIO model (repeated)
NIO model (repeated)

In summary, BIO relies on blocking system calls ( accept() , readLine() ) and typically requires a thread per connection. Non‑blocking I/O uses the kernel’s ability to return immediately, allowing a single thread to manage many connections, but pure polling still wastes CPU cycles. I/O multiplexing solves this by letting the kernel inform the application exactly which sockets are ready.

BIO model
BIO model
JavanetworkNIOI/Onon-blockingblockingSocketsBIO
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.