Chapter 5. Intelligent caching
A cache is a high-speed data storage layer in front of the primary storage location which stores a subset of data so that future requests for that data served up as fast as possible in computer terminology. Primary storage could be any database or a file system that usually stores data on non-volatile storage. Caching allows you to reuse previously retrieved or computed data efficiently, and it is one of the secrets of high-scalability and performance of any enterprise level application.
You may wonder why we named the chapter intelligent caching! Because, from the last decades, the unbounded changes of the software architecture need not only correctly used of a caching strategy but also properly configured (cache eviction, expiration) and sizing the cache layer to achieve the maximum performance and high-scalability of an application. Caching can be used for speeding up requests on five main different layers or environments of your application architecture:
- Client
- Network
- Web server
- Application
- Database
So, you should consider caching strategies for each layer of your application architecture to accomplish the high-performance of an application, and implements it’s correctly. It should be noted that none of the caching platforms or framework are a silver bullet. Cache usages vary for different data sizes and scenarios. Firstly, you should measure the data sizes and requests on each layer, doing various tests to find out the bottleneck and then with the way of experiments you have to define a tool or framework for caching data before implementing any caching platform such as Ignite, Ehcache, Redis or Hazelcast on any application layer.
In this chapter, we want to focus primarily on things you need to know about data caching and demonstrate the use of Apache Ignite for accelerating application performance without changing any business logic code. So, we are going to cover the following topics throughout the entire chapter:
- Different caching strategies and usage methods as a smart in-memory caching.
- Read/Write through and write behind strategies examples based on Hibernate and MyBatis for database caching.
- Memoization or application level caching.
- Web session clustering.
- Moreover, a list of recommendations to correctly prepare the caching layer.
Smart caching
I often hear suggestion like this when it comes to a matter of performance: Need for speed - Caching. However, I believe that in-memory caching is the last lines of defense when all current optimization tricks reach a bottleneck. We have a lot of points for optimizing before considering a separate layer for caching data, such as:
- Optimizing SQL queries; runs a few SQL queries plans to define the bottleneck on the database level.
- Adding necessary indexes on tables.
- Optimizing and configuring connection pools on Application servers.
- Optimizing application code, such as fetching data by paging.
- Caching static data such as Java script, Images and CSS files on the Web server and the client side.
Consider the regular N-Tier JEE architecture for optimizing and caching data as shown in figure 5.1.
Caches can be applied and leveraged throughout the various layers of technology including browsers, network layers (Content delivery network and DNS), web applications and databases as shown in figure 5.1. Cached information can include the result of database queries, computationally intensive calculation, API request/responses, and web artifacts such as HTML, JavaScript, and multi-media files. Therefore, for getting a high throughput of an entire application, you should consider optimization and caches on other layers, and not only the use of in-memory caching layer. These will give you the maximum benefits of caching data and improves the overall performance of the application.
Caching best practices
It’s essential to consider a few best practices for using cache smartly when implementing a cache on any application layer. A smart caching ensure that you implement all (or most of them) the best practices whenever designing a cache. This subsection describes a few considerations for using a cache
- Decide when and which data to cache. Caching can improve performance; the more data you can cache, the higher the chance to reduce the latency and contention that’s associated with handling large volumes of concurrent requests in the original data store. However, server resources are finite, and unfortunately, you could not cache all the resources you want. Consider caching data that are read frequently but modified rarely.
- The resilience of the caching layer. Your application can continue to operate by using the primary data storage if the cache is unavailable, and you won’t lose any critical piece of information.
- Determine how to cache data effectively. Most often, caching is less useful for dynamic data. The key to using a cache successfully lies in determining the most appropriate data to cache and caching it in the proper time. The data can be added to the cache on demand the first time it is fetched from the store by the application. Subsequent access to this data can be satisfied by using this cache. On the other hand, you can upload the data into the cache during the application startup, and sometimes it’s called cache warm up.
- Managing data expiration in caches. You can maintain a cache entry up-to-date by expiring a cache entry into the cache. When a cached data expires, it’s removed from the cache, and a new cache entry will be added into the cache at the next time when it will be fetched from the primary data. You can set a default expiration policy when you configure the cache. However, consider the expiration period for the cache carefully. Cache entry expires too quickly if you make it too short, and you will reduce the benefits of using the cache. On the other hand, you risk the data becoming stale if you make the period too long. Additionally, you should also consider to configure the cache eviction policy which will help you to evict cache entries from the cache whenever the cache is full and no more places exists to add a new entry.
- Update the caches when data changes on the primary data store. Generally, a middle-tier caching layer duplicates some data from the central database server. Its goal is to avoid redundant queries to the database. The cache entry has to be updated or invalidated when the data updates in the database. You should consider the possibility to maintain a cache entry as up-to-date as possible when designing a caching layer. Many database vendors allow getting a notification whenever any entity updates into the database and updates the caches.
- Invalidate data in a client-side cache. Data that is stored in a client-side cache (browser or any standalone application) is generally considered to be auspices of the service that provides the data to the client. A service cannot directly force a client to add or remove information from a client-side cache. This means that it’s possible for a client that poorly configured the cache to continue using the staled information. However, a service that provides cache needs to ensure that each server response provides the correct HTTP header directives to instruct the browser on when and for how long the browser can cache the response.
Design patterns
There might be two different strategies in a distributed computing environment when caching data:
- Local or private cache. The data is stored locally on the server that’s running an instance of an application or service. Application performance is very high in this strategy, because, most often the cached data stored in the same JVM along with application logic. However, when the cache is resident on the same node as the application utilizing it, scaling may affect the integrity of the cache. Additionally, when local caches are used, they only benefit the local application that consuming the data.
- Shared or distributed cache. The cache served as the common caching layer that can be accessed from any application instances and architecture topology. cached data can span multiple cache servers in this strategy, and be stored in a central location for the benefit of all the consumers of the data. This is especially relevant in a system where application nodes can be dynamically scaled in and out.
Basic terms
There are a few basic terms related to caching, frequently used throughout this book. I strongly believe that you are already familiar with these terms. However, it will be useful for those who are not familiar with these terms and getting all the information in a single place.
| Terms | Description |
|---|---|
| Cache entry | A single cache value, consists of a key and its mapped data value within the cache. |
| Cache Hit | When a data entry is requested from the cache, and the entry exists for the given key. A more cache hit means that most of the requests are satisfied by the cache. |
| Cache Miss | When a data entry is requested from the cache, and the entry does not exists for the given key. |
| Hot data | Data that has recently been used by an application is very likely to be reassessed soon. Such data is considered hot. A cache may attempt to keep the hottest data most quickly available while trying to choose the least hot data for eviction. |
| Cache eviction | The removal of entries from the cache in order to make room for newer entries, typically when the cache has run out of data storage capacity. |
| Cache expiration | The removal of entries from the cache after some amount of time has passed, typically as a strategy to avoid stale data in the cache. |
Database caching
There are many challenges that disk-based databases (especially RDBMS) can pose to your application when developing a distributed system that requires low latency and horizontal scaling. A few common challenges are as follows:
- Expensive query processing. Database queries can be slow and require serious system resources because the database system needs to perform some computation to fulfill the query request.
- Database hotspots. It’s likely that a small subset of data such as a celebrity profile or popular product (before Christmas) will be accessed more frequently than others in many applications. The SQL queries on such favorite products can result in hot spots in your database and maybe overprovisioning of database resources (CPU, RAM) based on the throughput requirements for the most frequently used data.
- The cost to scale. Most often, RDBMS are only scaling vertically (anyway, Oracle 18c and Postgres-XL can scaling horizontally but needs tremendous effort to configure). Scaling databases for extremely high reads can be costly and may require many databases read replicas to match the current business needs.
Most database servers are configured by default for optimal performance. However, each database vendors provides various optimizations tips and tricks to help engineers get the most out of their databases. These guidelines for database optimization observe a law similar to the funnel law that is illustrated in figure 5.2 and described below:
- Reducing data access.
- Returning less data.
- Reducing interaction with the underlayer.
- Reducing CPU overhead and using more machine resources.
Architects and engineers should make a great effort in squeezing as much performance as they can out of their database as mentioned earlier, because database caching should be implemented when all existing optimization tools reach a database bottleneck. A database cache supplements your primary database by removing unnecessary pressure on it (very close to the reduce data access layer), typically in the form of frequently accessed read data. The cache itself can live in some areas including your database, application or as a standalone layer.
The basic paradigm when querying data from a relational database from an application includes executing SQL statement through ORM tools or JDBC API and iterating over the returned ResultSet object cursor to retrieve the database rows. There are a few techniques you can apply based on your data access tools and patterns when wanting to cache the returned data.
We are going to discuss how Ignite in-memory cache can be used as a 2nd level caches in different data access tools such as Hibernate and Mybatis in this section, which can significantly reduce the data access times of your application and improve overall application performance.
A 2nd level cache is a local or distributed data store of entity data managed by the persistence provider to improve application performance.
A second level cache can improve application performance by avoiding expensive database calls, keeping the data bounded (locally available) to the application. A 2nd level cache is fully managed by the persistence provider and typically transparent to the application. That is, application reads, writes and commits data through the entity manager without knowing about the cache.
There is also a Level 1 cache based on the persistence provider, such as MyBatis or Hibernate. Level 1 is used to cache objects retrieved from the database within the current database session. An HTTP session is opened and reused until the service method returns when client-side (web page or web service) invokes a service. All operations performed until the service method return will share the L1 cache, so the same object will not retrieve twice from the database. Objects retrieved from the database will not be available after closing the database session.
So, in a nutshell, the 2nd level cache provides the following benefits:
- Boost performance by avoiding expensive database calls.
- Data are kept transparent to the application.
- CRUD operation can be performed through standard persistence manager functions.
- You can accelerate applications performance by using 2nd level cache, without changing the code.