39 New Features of Redis 6 Multi Threaded Client Caching and Security

39 New Features of Redis 6 Multi-threaded Client Caching and Security #

Redis officially released version 6.0 in May this year, which includes many new features. Therefore, shortly after its release, it gained wide attention in the industry.

So, for the last class, I have specially arranged this session to talk about several key new features in Redis 6.0, namely multi-IO thread for network processing, client-side caching, fine-grained access control, and the use of the RESP 3 protocol.

Among these, the multi-IO thread for network processing can improve the speed of handling network requests, while client-side caching allows applications to directly read data from the client’s local cache. These two features can enhance the performance of Redis. In addition, the fine-grained access control allows Redis to control access permissions for different users based on the command level, which enhances the security protection of Redis. The RESP 3 protocol enhances the functionality of the client, making it more convenient to use different data types in Redis.

Only by understanding the principles of these features in detail can you better judge whether to use version 6.0. If you are already using 6.0, you can also learn how to use it better and avoid pitfalls.

Firstly, let us understand the newly introduced multi-threading feature in version 6.0.

From Single-Threaded Network Request Processing to Multi-Threaded Processing #

In Redis 6.0, the first highly anticipated new feature is multi-threading. This is because Redis has always been known for its single-threaded architecture, where all tasks, from network IO handling to actual command processing, are handled by a single thread, although some commands can be executed by background threads or processes (such as data deletion, snapshot generation, and AOF rewriting).

With the improvement in network hardware performance, Redis performance bottlenecks can sometimes occur in network IO processing, meaning that the speed at which a single main thread handles network requests cannot keep up with the speed of the underlying network hardware.

To address this issue, there are generally two approaches.

The first approach is to use a user-space network protocol stack (such as DPDK) instead of the kernel network protocol stack, allowing network request processing to be performed in user space without involving the kernel.

For high-performance Redis, avoiding frequent network request processing by the kernel can significantly improve request processing efficiency. However, this approach requires adding support for user-space network protocol stacks in the overall architecture of Redis, which involves modifying the parts of the Redis source code related to networking (e.g., modifying all network send/receive functions). This would introduce a lot of development work, and the addition of new code could potentially introduce new bugs, leading to system instability. Therefore, Redis 6.0 did not adopt this approach.

The second approach is to use multiple IO threads to handle network requests, thereby increasing the parallelism of network request processing. This is the approach adopted in Redis 6.0.

However, Redis’s multi-IO threads are only used for handling network requests, and Redis still uses a single thread for read and write commands. This is because network processing is often the bottleneck for Redis when handling requests, and allowing multiple IO threads to handle network operations in parallel can improve the overall performance of the instance. Continuing to use a single thread to execute command operations eliminates the need to develop additional multi-threaded mutual exclusion mechanisms to ensure the atomicity of Lua scripts and transactions. This simplifies the implementation of Redis’s thread model.

Let’s take a look at how the main thread and IO threads work together to handle requests in Redis 6.0. Understanding the specific principles will help you truly understand how to use multi-threading. For convenience, we can divide the cooperation between the main thread and multiple IO threads into four stages.

Stage One: Server and Client Establish Socket Connection and Allocate Processing Threads

First, the main thread is responsible for accepting connection requests. When a client requests to establish a socket connection with the instance, the main thread creates a connection with the client and puts the socket in a global waiting queue. Then, the main thread uses a polling method to assign the socket connection to an IO thread.

Stage Two: IO Threads Read and Parse Requests

Once the main thread assigns the socket to an IO thread, it enters a blocking state and waits for the IO thread to complete reading and parsing the client’s request. Because multiple IO threads are processing in parallel, this process can be completed quickly.

Stage Three: Main Thread Executes Request Operations

When the IO thread finishes parsing the request, the main thread still executes the command operations in a single-threaded manner. The following diagram shows these three stages, which you can refer to for a better understanding.

Stage Four: IO Threads Write Back to Socket and Main Thread Clears Global Queue

After the main thread completes the command operations, it writes the results that need to be returned to the buffer. Then, the main thread blocks and waits for the IO thread to write these results back to the socket and return them to the client.

Similarly to the IO threads reading and parsing requests, when the IO threads write back to the socket, multiple threads are executing concurrently, so the speed of writing back to the socket is also fast. When the IO threads finish writing back to the socket, the main thread clears the global queue and waits for subsequent requests from the client.

I have also drawn a diagram showing the operations of the main thread and IO threads in this stage, which you can refer to.

Now that you understand how the main thread and IO threads in Redis cooperate, how do you enable multi-threading? In Redis 6.0, the multi-threading mechanism is disabled by default. If you want to use multi-threading, you need to configure two settings in redis.conf.

  1. Set the io-threads-do-reads configuration option to yes to enable multi-threading.

    io-threads-do-reads yes
    
  2. Set the number of threads. Generally, the number of threads should be less than the number of CPU cores on the machine where Redis instance is located. For example, for an 8-core machine, Redis recommends configuring 6 IO threads.

    io-threads 6
    

If, in your actual application, you find that the CPU overhead of the Redis instance is minimal, but the throughput has not improved, you may consider using Redis 6.0’s multi-threading mechanism to accelerate network processing and improve the throughput of the instance.

Implementing client-side caching with server-side assistance #

Redis 6.0 introduces an important feature called client-side caching with server-side assistance, also known as tracking. With this feature, Redis clients in the application can cache the read data locally, allowing the application to quickly access data directly from the local cache.

However, when data is cached locally in the client, a problem arises: How can the client be notified when the cached data is modified or invalidated?

The Tracking feature implemented in 6.0 provides two modes to address this issue.

The first mode is the ordinary mode. In this mode, the Redis server keeps a record of the keys that the client has read and monitors whether those keys are modified. When a key’s value changes, the server sends an invalidate message to the client to inform it that the cache has become invalid.

When using the ordinary mode, it is important to note that the server only reports an invalidate message for each recorded key once. In other words, after sending an invalidate message to the client once, the server will not send another invalidate message if the key is modified again.

The server will only monitor the keys that have been read again when the client executes a read command. If a key is modified, the server will send an invalidate message. This design is intended to save limited memory space. After all, if the client is no longer accessing a key but the server continues to record its modification, it would waste memory resources.

You can enable or disable the Tracking feature in ordinary mode by executing the following command:

CLIENT TRACKING ON|OFF

The second mode is the broadcast mode. In this mode, the server broadcasts the invalidation information of all keys to the clients. However, if a key is frequently modified, the server will send a large number of invalidate broadcast messages, consuming a significant amount of network bandwidth resources.

Therefore, in practical applications, we allow clients to register prefixes of the keys they want to track. When a key with a registered prefix is modified, the server will broadcast the invalidation message to all registered clients. Unlike the ordinary mode, in the broadcast mode, even if a client has not read a key yet, as long as it has registered to track the key, the server will notify this client of the key’s invalidation message.

Let me give you an example to show you how the client receives invalidation messages using the broadcast mode. If we execute the following command on the client, the client will receive an invalidate message if the server updates the user:id:1003 key:

CLIENT TRACKING ON BCAST PREFIX user

This broadcast mode of monitoring keys with a prefix is very compatible with our naming conventions for keys. In practical applications, we usually set the same business name prefix for keys under the same business, so we can conveniently use the broadcast mode.

However, please note that the ordinary mode and the broadcast mode I introduced earlier require clients to use the RESP 3 protocol. The RESP 3 protocol is a communication protocol newly introduced in 6.0, and I will provide you with more detailed information in a moment.

For clients that use the RESP 2 protocol, another mode, called the redirect mode, needs to be used. In the redirect mode, clients that want to receive invalidation messages need to execute the subscribe command SUBSCRIBE to subscribe to the channel _redis_:invalidate, which is used to send invalidate messages. At the same time, another client needs to execute the CLIENT TRACKING command to set up the server to forward invalidation messages to clients using the RESP 2 protocol.

Let me give you another example to explain how clients using the RESP 2 protocol can also receive invalidation messages. Suppose client B wants to receive invalidation messages, but it only supports the RESP 2 protocol, while client A supports the RESP 3 protocol. We can execute the following commands on clients B and A, respectively:

# Executed on client B, with client B's ID being 303
SUBSCRIBE _redis_:invalidate

# Executed on client A
CLIENT TRACKING ON BCAST REDIRECT 303

After setting this up, if any key-value pair is modified, client B can receive the invalidation message through the _redis_:invalidate channel.

Now that you have learned about the client caching feature in version 6.0, let’s move on to the third key feature, the Access Control List (ACL) functionality, which effectively enhances the security of Redis usage.

From simple password-based access to fine-grained permission control #

Before Redis 6.0, the only way to implement secure access to instances was through password-based control. For example, clients needed to enter a password before connecting to the instance.

In addition, for high-risk commands such as KEYS, FLUSHDB, and FLUSHALL, before Redis 6.0, we could only rename these commands using the rename-command directive to prevent direct invocation by clients.

Redis 6.0 introduces more fine-grained access control, which is reflected in two main aspects.

Firstly, in version 6.0, it is possible to create different users to access Redis. Before 6.0, all clients could use the same password to log in, but there was no concept of users. In 6.0, we can use the ACL SETUSER command to create users. For example, we can execute the following command to create and enable a user named normaluser with the password “abc”:

ACL SETUSER normaluser on > abc

Additionally, version 6.0 also supports setting command operation access permissions based on users. I have listed the specific operations in the following table for you to see. Plus (+) and minus (-) signs represent granting or revoking the calling permission for the command, respectively.

To help you understand, let me give you an example. Suppose we want to set the user normaluser to only have access to hash-related commands and not string-related commands. We can execute the following command:

ACL SETUSER normaluser +@hash -@string

In addition to setting access control for a specific command or a class of commands, version 6.0 also supports setting access permissions based on keys.

To do this, we use the tilde (~) and key prefix to represent the controlled keys. For example, by executing the following command, we can set the user normaluser to only operate on keys with the prefix “user:”:

ACL SETUSER normaluser ~user:* +@all

So far, you have learned that Redis 6.0 allows setting different users to access instances, and it allows granting or denying command operation permissions based on users and keys.

With this functionality, in Redis applications with multiple users, it becomes very convenient and flexible to set different levels of command operation permissions for different users. This is very helpful for providing secure access to Redis.

Enabling RESP 3 Protocol #

Redis 6.0 introduces the RESP 3 communication protocol, replacing the previous RESP 2. In RESP 2, the communication between the client and server is encoded in byte arrays, requiring the client to manually decode the transmitted data based on the command or data type. This increases the complexity of client development.

On the other hand, RESP 3 directly supports differentiated encoding for various data types, including null values, floating-point numbers, boolean values, ordered dictionaries, and unordered sets.

Differentiated encoding means that different data types are distinguished by different initial characters. In this way, clients can directly perform data conversion operations by judging the initial character of the transmitted message, improving client efficiency. In addition, the RESP 3 protocol also supports client-side caching in both normal mode and broadcast mode.

Summary #

In this lesson, I introduced the new features of Redis 6.0. I summarized these new features in a table, which you can review and consolidate.

Finally, I have a small suggestion for you: Since Redis 6.0 has just been released, the new feature needs to be deployed and verified in practical applications. If you want to try Redis 6.0, you can start by using it in non-core business scenarios. This approach can not only validate the performance or functional advantages of the new features but also avoid affecting the core business due to the instability of the new features.

One question per class #

Which new feature(s) in Redis 6.0 do you think will be helpful to you?

Please write your thoughts and answers in the comments section. Let’s discuss and exchange ideas together. If you found today’s content helpful, feel free to share it with your friends or colleagues. See you in the next class.