Skip to main content

Caching_System Design Notes 13

Free2020-03-29#Back-End#read-write splitting#read-through#write-through#refresh-ahead#缓存驱逐策略

Theoretically, adding cache at any layer before the data layer can block traffic and reduce operation requests that eventually reach the database

Preface

Read-Write Separation, Database Partitioning, Denormalization, Using NoSQL... If all these scaling methods are applied and data response is still getting slower and slower, is there any other solution?

Yes, add cache. Use cache layer to absorb uneven loads and traffic spikes:

Popular items can skew the distribution, causing bottlenecks. Putting a cache in front of a database can help absorb uneven loads and spikes in traffic.

1. Where to Add?

Theoretically, adding cache at any layer before the data layer can block traffic, reduce operation requests that eventually reach the database:

Divided into 4 types according to cache location:

  • Client-side cache: Including [HTTP Cache](/articles/http 缓存/), browser cache, etc.

  • Web cache: Such as CDN, Reverse Proxy Service, etc.

  • Application layer cache: Such as Memcached, Redis and other Key-Value Storage

  • Database cache: Some databases provide built-in cache support, such as query cache

To reduce database load, we add a key-value storage as a buffer layer between the application and data storage:

A cache is a simple key-value store and it should reside as a buffering layer between your application and your data storage.

Respond to some requests through data cached in memory, without actually executing database query operations, thereby improving data response speed

2. What to Store?

There are two common cache modes:

  • Cached Database Queries: Cache original database query results

  • Cached Objects: Cache data model objects in the application, such as reassembled datasets, or entire data model class instances

Cache Original Database Query Results

Generate key based on query statement, cache the database query results, for example:

key = "user.%s" % user_id
user_blob = memcache.get(key)
if user_blob is None:
    user = mysql.query("SELECT * FROM users WHERE user_id=\"%s\"", user_id)
    if user:
        memcache.set(key, json.dumps(user))
    return user
else:
    return json.loads(user_blob)

The main defect of this mode lies in the difficulty of handling cache expiration, because there is no clear association between data and key (i.e., query statement), after data changes, it is difficult to precisely delete all related entries in the cache. Think about it, when a cell changes, which query statements will be affected?

Nevertheless, this is still the most commonly used cache mode, because compromises can be made, such as:

  • Only cache data directly associated with query statements, sorting, statistics, filtering and other calculation results are not stored

  • Don't pursue precision, delete all cache entries that may be affected

Cache Data Objects

Another approach is to cache data model objects in the application, so there is a logical association between raw data and cache, thereby easily solving the difficult problem of cache updates

Regardless of how data is queried, processed and transformed, only cache the final data model objects obtained. When raw data changes, directly remove the corresponding data object entirely

For applications, data objects are easier to manage and maintain than raw data, therefore, it is recommended to cache data objects, rather than raw data

3. How to Query?

There are 6 common cache data access strategies:

  • Cache-aside/Lazy loading: Reserved cache

  • Read-through: Direct read

  • Write-through: Direct write

  • Write-behind/Write-back: Write-back

  • Write-around: Bypass write

  • Refresh-ahead: Refresh

Cache-aside

In reserved cache mode, there is no direct relationship between cache and database (cache is located aside, hence called Cache-aside), the application reads required data from the database and fills it into the cache

Data requests prioritize the cache, only query the database when cache miss occurs, and cache the results, so the cache is on-demand (Lazy loading), only data that has been actually accessed will be cached

The main problems are:

  • When cache miss occurs, 3 steps are needed, delay cannot be ignored (for cold start, can manually preheat)

  • Cache may become old (generally forced update by setting TTL)

Read-through

In direct read mode, cache blocks in front of the database, the application does not directly interact with the database, but reads data directly from the cache

When cache miss occurs, the cache is responsible for querying the database and caching it itself. The only difference from reserved cache is that the database query work is completed by the cache, not the application

Write-through

Similar to direct read mode, cache also blocks in front of the database, data is written to cache first, then written to database. That is to say, all write operations must go through cache first

Generally combined with direct read cache, although write operations have an extra layer of cache (there is additional delay), but it ensures consistency of cached data (avoiding cache becoming old). At this time, cache is like a database proxy, both read and write go through cache, then cache queries database or synchronizes write operations to database

Write-behind/Write-back

Write-back cache is very similar to direct write, write operations also need to go through cache first, the only difference is asynchronous write to database, thereby allowing batch processing and write operation merging

Can also be used in combination with direct read cache, and there is no performance problem of write operations in direct write, but only guarantees eventual consistency

Write-around

So-called bypass write cache means write operations do not go through (bypass) cache, the application writes directly to database, only cache read operations. Can be used in combination with reserved cache or direct read cache:

Refresh-ahead

Refresh in advance, before cache expires, automatically refresh (reload) recently accessed entries. Can even reduce latency through preloading, but if prediction is inaccurate it may instead lead to performance degradation

4. What to Do When Full?

Of course, cache space is extremely limited, so there must also be an Eviction Policy, remove some entries that are unlikely to be used from cache, common strategies are as follows:

  • LRU (Least Recently Used): One of the most commonly used strategies, according to the locality principle of program runtime, within a period of time, there is a high probability of accessing the same data, so remove data that hasn't been used recently, for example, booking airline tickets, within a period of time there is a high probability of querying the same route

  • LFU (Least Frequently Used): According to usage frequency, remove the least frequently used data, for example, input methods mostly associate based on word frequency

  • MRU (Most Recently Used): In some scenarios, need to delete recently used entries, such as read, no longer remind, not interested, etc.

  • FIFO (First In, First Out): First in first out, remove earliest accessed data

These strategies can also be used in combination, such as LRU + LFU comprehensive consideration, depending on specific scenarios

References

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment