23 How Lateral Cache Redis Works

23 How Lateral Cache Redis Works #

We know that Redis provides high-performance data access capabilities, so it is widely used in caching scenarios. It can effectively improve the response speed of business applications and avoid sending high-concurrency requests to the database layer.

However, if there is a problem with Redis caching, such as cache expiration, a large number of requests will be directly piled up to the database layer, inevitably putting tremendous pressure on the database. This can lead to database downtime or failures, which means that the business application will not be able to access data or respond to user requests. This kind of production incident is definitely not what we want to see.

Because Redis is commonly used as a cache and plays an important role in business applications, we need to systematically understand a series of cache-related content, including its working principles, replacement strategies, exception handling, and expansion mechanisms. Specifically, we need to address four key questions:

  • How does Redis caching work exactly?
  • What should we do if Redis cache is full?
  • Why do cache consistency, cache penetration, cache avalanche, and cache breakdown occur, and how should we deal with them?
  • Since Redis memory is limited, can we use fast solid-state drives to save data and increase the amount of cache?

In this lesson, we will learn about the characteristics of caching, the natural advantages of Redis for caching, and the specific workings of Redis caching.

Characteristics of Caches #

To understand why Redis is suitable for use as a cache, we need to understand the characteristics of caches.

First, you need to know that there are different speeds of access between different layers in a system, which is why we need caches. This allows us to put frequently accessed data in the cache to speed up their access.

To better understand this, let me use the example of a computer system. The following diagram shows the three layers of storage structure in a computer system, as well as their respective capacities and access performance. The top layer is the processor, the middle layer is the memory, and the bottom layer is the disk.

From the diagram, we can see that the access speeds of the CPU, memory, and disk layers range from tens of nanoseconds to 100 nanoseconds, and then to a few milliseconds, showing a significant difference in performance.

Imagine that if every time the CPU processes data, it has to read data from the slow disk at the millisecond level and then process it, the CPU can only wait for the disk data transfer to complete. In this case, the high-speed CPU is slowed down by the slow disk, and the overall speed of the computer system will become very slow.

Therefore, in a computer system, there are two default caches:

  • The last-level cache (LLC) inside the CPU, which caches data from memory to avoid accessing data from memory every time.
  • The high-speed page cache in memory, which caches data from the disk to avoid accessing data from the disk every time.

Compared to memory, LLC has a faster access speed, and compared to the disk, memory has a faster access speed. Therefore, we can see the first characteristic of caches: In a hierarchical system, a cache is always a fast subsystem, and when data is stored in a cache, it can avoid accessing data from a slow subsystem every time. In terms of internet applications, Redis is the fast subsystem, while the database is the slow subsystem.

Knowing this, you can understand why we must make Redis provide high-performance access because if the access speed is slow, Redis as a cache will not be very valuable.

Let’s take another look at the computer’s layered structure mentioned earlier. The size of LLC is in the megabyte range, the size of the page cache is in the gigabyte range, and the size of the disk is in the terabyte range. This actually includes the second characteristic of caches: The capacity of a cache system is always smaller than that of the slower backend system, and we cannot put all the data in the cache system.

This is interesting because it indicates that the capacity of caches is ultimately limited, and the amount of data in the cache is also limited. It is impossible to always meet the access requirements. Therefore, there must be an interaction process of writing back and reading data between the cache and the backend slow system. In simple terms, the data in the cache needs to be evicted according to certain rules, written back to the backend system, and new data needs to be read from the backend system and written into the cache.

Speaking of this, you must have realized that Redis itself supports evicting data according to certain rules, which is equivalent to implementing data eviction in caches. In fact, this is also an important reason why Redis is suitable for use as a cache.

Okay, now that we understand the two important characteristics of caches, let’s learn how caches handle requests. In actuality, when a business application accesses data in the Redis cache, the data may not exist, so the way it is handled may be different.

Two scenarios for processing requests with Redis cache #

When Redis is used as a cache, we deploy Redis in front of the database. When the business application accesses data, it will first query whether the corresponding data is saved in Redis. At this point, there are two scenarios based on whether the data exists in the cache.

  • Cache hit: The data is available in Redis, so it can be directly read from Redis, which is very fast.
  • Cache miss: The data is not saved in Redis, so it needs to be retrieved from the backend database, which could slow down the performance. Furthermore, once a cache miss occurs, in order for subsequent requests to read the data from the cache, we need to write the missing data into Redis. This process is called cache update. Cache update operations involve ensuring data consistency between the cache and the database, which I will discuss in more detail in Chapter 25.

I have created a diagram that clearly shows the scenarios of fetching data when cache hit or miss occurs. You can take a look at this image.

Redis Cache

Let’s assume we are using Redis as a cache in a web application. User requests are sent to Tomcat, which is responsible for processing the business logic. To access data, we need to read and write data from MySQL. Therefore, we can deploy Redis in front of MySQL. If the requested data is in Redis, it is a cache hit, and Tomcat can directly read the data from Redis, accelerating the application’s access. Otherwise, Tomcat needs to read the data from the slow database.

At this point, you may have noticed that when using Redis cache, we have three main operations:

  • When the application reads data, it needs to read from Redis first.
  • When a cache miss occurs, we need to read data from the database.
  • When a cache miss occurs, we also need to update the cache.

So, who performs these operations? This depends on the usage pattern of Redis cache. Next, I will discuss the operational strategies of using Redis as a side cache.

Using Redis as a Sidecar Cache #

Redis is a standalone system software that operates separately from the business application. Once we deploy a Redis instance, it passively waits for client requests and processes them accordingly. Therefore, if an application wants to use Redis caching, we need to add the corresponding cache operation code to the program. That’s why we refer to Redis as a sidecar cache, meaning that reading cache, reading the database, and updating the cache operations all need to be completed in the application program.

This is different from the LLC (Last Level Cache) and page cache in a computer system that I just mentioned. You can recall that when developing programs, we don’t explicitly create instances of LLC or page cache in the code, nor do we explicitly call their GET interfaces. This is because when building a computer hardware system, we have already placed the LLC and page cache on the data access path of the application program, allowing the application program to directly utilize the cache when accessing data.

So, when using Redis caching, specifically, we need to add code in the application program in three aspects:

  • When the application program needs to read data, we need to explicitly call the GET operation interface of Redis to query.
  • If the cache is missing, the application program needs to connect with the database to retrieve the data.
  • When the data in the cache needs to be updated, we also need to explicitly call the SET operation interface in the application program to write the updated data to the cache.

So, how should we add the code? Let me show you a pseudocode example of using Redis caching in a web application.

String cacheKey = "productid_11010003";
String cacheValue = redisCache.get(cacheKey);
// Cache hit
if (cacheValue != NULL)
   return cacheValue;
// Cache miss
else
   cacheValue = getProductFromDB();
   redisCache.put(cacheValue);  // Cache update

As you can see, in order to use the cache, the web application program needs an instance object redisCache representing the cache system and actively calls the GET interface of Redis. It also needs to handle the logic of cache hit and cache miss, such as updating the cache when a cache miss occurs.

With this understanding, there is one thing to note when using Redis caching: because we need to add program code to use the cache, Redis is not suitable for applications where the source code cannot be obtained, such as old applications that are no longer maintained or applications developed by third-party vendors that do not provide the source code. Therefore, we cannot perform cache operations in these applications.

When using the sidecar cache, we need to add operation code in the application program, which increases the extra workload of using Redis caching. However, precisely because Redis is a sidecar cache and operates as a separate system, we can independently scale or optimize the Redis cache. Also, as long as we keep the operation interface unchanged, the code we added to the application program does not need to be modified.

Okay, up to this point, we know that by adding Redis operation code in the application program, we can make the application program use Redis to cache data. However, besides querying and reading data from the Redis cache, the application program may also modify the data. In this case, we can choose to modify the cache or the backend database. How should we make this choice?

In fact, this involves the two types of Redis caching: read-only cache and read-write cache. The read-only cache can accelerate read requests, while the read-write cache can accelerate both read and write requests. Moreover, the read-write cache has two data writeback strategies, allowing us to choose between performance and data reliability according to business needs. Therefore, next, let’s specifically understand the Redis cache types and the corresponding writeback strategies.

Types of cache #

Based on whether Redis cache accepts write requests, we can divide it into read-only cache and read-write cache. Let’s start by understanding read-only cache.

Read-only cache #

When Redis is used as a read-only cache, when the application needs to read data, it will first call the Redis GET interface to query if the data exists. All write requests will be directly sent to the backend database for the data to be inserted, updated, or deleted. For the data that is updated or deleted, if Redis has already cached the corresponding data, the application needs to remove these cached data so that Redis no longer holds this data.

When the application reads these data again, a cache miss occurs, and the application retrieves these data from the database and writes them into the cache. This way, when these data are accessed in the future, they can be directly obtained from the cache, which accelerates the access process.

Let me give you an example. Suppose the business application wants to modify data A. At this point, data A is also cached in Redis. The application will first modify A directly in the database and delete A from Redis. When the application needs to read data A again, a cache miss occurs, and the application retrieves A from the database and writes it into Redis so that subsequent requests can directly read it from the cache. The diagram below shows this process:

The advantage of directly updating data in the database for read-only cache is that all the latest data is in the database, which provides data reliability. There is no risk of data loss in the database. Read-only cache is suitable for caching user-readable data such as images and short videos.

Read-write cache #

Knowing about read-only cache, it is easy to understand read-write cache.

For read-write cache, in addition to read requests being sent to the cache (directly querying if the data exists in the cache), all write requests are also sent to the cache for data insertion, updating, or deletion. With Redis’ high-performance access capabilities, data insertion, updating, and deletion operations can be quickly completed in the cache, and the results are returned to the business application quickly, enhancing the responsiveness of the application.

However, unlike read-only cache, in read-write cache, the latest data resides in Redis, which is an in-memory database. Once a power outage or system failure occurs, the data in memory will be lost. This means that the application’s latest data might be lost, posing a risk to the business.

Therefore, depending on the business application’s requirements for data reliability and cache performance, we have two strategies: synchronous write-through and asynchronous write-back. Among them, synchronous write-through strategy prioritizes data reliability, while asynchronous write-back strategy prioritizes quick response. Understanding these two strategies can help us make the right design choices based on business needs.

Next, let’s take a closer look at these two strategies.

Synchronous write-through refers to the process of sending write requests to both the cache and the backend database simultaneously. The response is only sent to the client when both the cache and the database have finished writing the data. This ensures that even if the cache fails or experiences a fault, the latest data is still saved in the database, providing data reliability.

However, synchronous write-through reduces the cache’s access performance. This is because the cache processes write requests quickly, while the database processes write requests at a slower pace. Even if the cache quickly handles the write requests, it still needs to wait for the database to finish processing all the write requests before returning the results to the application, increasing the cache’s response latency.

On the other hand, asynchronous write-back strategy prioritizes response latency. In this case, all write requests are first processed in the cache. When the data to be evicted from the cache is about to be written back, the cache writes the data back to the backend database. This way, the operation of processing this data is performed in the cache, which is fast. However, if a power outage occurs before the data is written back to the database, there is a risk of data loss.

To help you understand better, I have also created the following diagram for your reference.

Whether to choose read-only cache or read-write cache mainly depends on whether we need to accelerate write requests.

  • If we need to accelerate write requests, we choose read-write cache.
  • If write requests are few or we only need to improve the response speed of read requests, we choose read-only cache.

Let’s take an example. In a scenario where there is a major promotion for products, the inventory information of the products will be constantly modified. If each modification needs to be processed in the database, it will slow down the entire application. In this case, we usually choose the read-write cache mode. On the other hand, in the scenario of a short video app, although there are many attributes of the videos, they are generally determined and not frequently modified. In this case, modifying in the database does not have a significant impact on the cache, so the read-only cache mode is a suitable choice.

Summary #

Today, we learned about the two features of caching. First, data temporarily stored in a fast subsystem in a layered system helps accelerate access. Second, caching has limited capacity, and when the cache is full, data needs to be evicted. Redis naturally has a high-performance access and data eviction mechanism, which perfectly meets the requirements of caching and is therefore very suitable for use as a cache.

In addition, we also learned about Redis as a side-cache. Side-caching means that new cache logic processing code needs to be added to the application. Of course, if the source code cannot be modified, Redis cannot be used as a cache.

When using Redis as a cache, there are two modes: read-only cache and read-write cache. Among them, read-write cache also provides two modes: synchronous direct write and asynchronous write-back. The synchronous direct write mode focuses on ensuring data reliability, while the asynchronous write-back mode focuses on providing low-latency access. We need to choose according to the actual business scenario requirements.

In this lesson, although I mentioned that Redis has a data eviction mechanism, I didn’t go into detail about the specific eviction strategy. So, how does Redis actually evict data? I will give you a detailed introduction in the next lesson.

One Question per Class #

As usual, I have a small question for you. In this class, I mentioned both Redis read-only cache and read-write cache with write-through strategy. These two types of cache both synchronize data writes to the backend database. Do you think there are any differences between them?

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