<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akbar Septriyan</title>
    <description>The latest articles on DEV Community by Akbar Septriyan (@asepwhite).</description>
    <link>https://dev.to/asepwhite</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1838296%2F3fa292ae-9ccd-4f44-aa86-3eef4ac3e284.jpg</url>
      <title>DEV Community: Akbar Septriyan</title>
      <link>https://dev.to/asepwhite</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/asepwhite"/>
    <language>en</language>
    <item>
      <title>Redis for query caching</title>
      <dc:creator>Akbar Septriyan</dc:creator>
      <pubDate>Thu, 25 Jul 2024 12:29:36 +0000</pubDate>
      <link>https://dev.to/asepwhite/redis-for-query-caching-31o5</link>
      <guid>https://dev.to/asepwhite/redis-for-query-caching-31o5</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;In this article, we'll delve into a use case where we can use Redis for query caching, to increase the &lt;a href="https://en.wikipedia.org/wiki/Web_server#requests_per_second" rel="noopener noreferrer"&gt;RPS&lt;/a&gt; of our backend service. This article is inspired by &lt;a href="https://redis.io/learn/howtos/solutions/microservices/caching" rel="noopener noreferrer"&gt;this post&lt;/a&gt;, however instead of using nodejs, we will use Java Spring boot and in addition we will also instrument our code using &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; and observe the response time using &lt;a href="https://www.jaegertracing.io/" rel="noopener noreferrer"&gt;Jaeger&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use case
&lt;/h3&gt;

&lt;p&gt;Let’s say you are a backend engineer in an e-commerce company and are in charge of product discovery in the homepage. As your company grows, you start selling more products (more data in your database) and more users (more requests) are accessing your e-commerce site. This increase in data size and number of traffic is making your homepage lagging and in turn makes many users complain about your ecommerce website. How can you solve this problem?&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Let’s simulate above use case, below is a simplified diagram to illustrate our system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcpzpjbnzuliqzlfzgmq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcpzpjbnzuliqzlfzgmq.png" alt="Initial system design" width="494" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s break down the components of the above system.&lt;/p&gt;

&lt;h4&gt;
  
  
  Product DB
&lt;/h4&gt;

&lt;p&gt;We use mongodb as our primary database and we will use fashion product &lt;a href="https://www.kaggle.com/datasets/paramaggarwal/fashion-product-images-dataset/data" rel="noopener noreferrer"&gt;dataset from kaagle&lt;/a&gt; to populate our data. The dataset contains 44k rows. Below is the example of product data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   "id": "66a1de675d61dc885a0139f1",
   "gender": "Men",
   "masterCategory": "Apparel",
   "subCategory": "Topwear",
   "articleType": "Shirts",
   "baseColour": "Navy Blue",
   "season": "Fall",
   "year": 2011,
   "usage": "Casual",
   "productDisplayName": "Turtle Check Men Navy Blue Shirt"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Product Service
&lt;/h4&gt;

&lt;p&gt;We will use Spring Boot to build our product service. The product service will provide an endpoint to get list of products by it’s displayName:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /products/getByDisplayName?displayName={product display name}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the relevant code for above endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public List&amp;lt;Product&amp;gt; searchProductsByDisplayName(String displayName) {
        return productRepository.findProductByProductDisplayNameContainingIgnoreCase(displayName); 
    }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Repository
public interface ProductRepository extends MongoRepository&amp;lt;Product, String&amp;gt; {
   @Query("{ 'productDisplayName' : { $regex: ?0, $options: 'i' } }")
   List&amp;lt;Product&amp;gt; findProductByProductDisplayNameContainingIgnoreCase(String productDisplayName);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@GetMapping("/products/getByDisplayName")
   public ResponseEntity&amp;lt;Object&amp;gt; getMobileDataProvider(@RequestParam String displayName) {
       List&amp;lt;Product&amp;gt; searchedProduct = productService.searchProductsByDisplayName(displayName);
       return ResponseEntity.status(HttpStatus.OK).body(searchedProduct);
   }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to product service and mongodb, I have also added Java opentelemetry agent to instrument our code and Jaeger to monitor our application metrics. You can find the full source code in &lt;a href="https://github.com/asepwhite/learn-redis-patterns/tree/main/cache-aside" rel="noopener noreferrer"&gt;this github repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now let’s try to run and see the performance of our endpoint:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3fsl2dwb2kburt1ybv4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3fsl2dwb2kburt1ybv4.png" alt="Postman API call" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18x6u2rlymkhuzobvq51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18x6u2rlymkhuzobvq51.png" alt="Jaeger initial system" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4eo8uzu4042sd7lkekj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4eo8uzu4042sd7lkekj.png" alt="Jaeger initial system 2" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Based on the above data we see that our endpoint response time is acceptable (most of them are less than 100ms). &lt;/p&gt;

&lt;p&gt;Now let’s increase the traffic and see if our service is still able to handle it. To do that I am going to use &lt;a href="https://locust.io/" rel="noopener noreferrer"&gt;locust&lt;/a&gt;. In the &lt;a href="https://github.com/asepwhite/learn-redis-patterns/blob/main/cache-aside/src/main/java/learn/kvstore/cacheaside/scripts/locust/locustfile.py" rel="noopener noreferrer"&gt;locust file&lt;/a&gt; I have a list containing 30 popular product names and we will choose a random product from that list to be searched using our API endpoint. In addition, I will also set 60 concurrent user (at peak) to call our API and below is the locust script and result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feh8g91fofkn2kyni6rxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feh8g91fofkn2kyni6rxt.png" alt="Locust result" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Based on the above graphs we can see that we have a bottleneck in our system. The increase in RPS is not directly proportional to the increase of concurrent users. It stuck at 200 RPS. In addition, as the concurrent users number increased, the response time also increased.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Now to solve this problem, what can we do?&lt;br&gt;
Let’s first find where’s the problem lies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll5pepzvgnb3b49boml9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll5pepzvgnb3b49boml9.png" alt="Jaeger tracing 1" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq96twrg2w50114zngrmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq96twrg2w50114zngrmh.png" alt="Jaeger tracing 2" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnd2jx3zz49ibkz1kjq9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnd2jx3zz49ibkz1kjq9.png" alt="Jaeger tracing 3" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon inspecting the trace, we found that the call to the database took a long time, around 500ms. It’s 10 times more than our “normal” API response time. Now we know the bottleneck is caused by a call to the database, and to help our database in handling the load we can use cache. A cache is a high-speed data storage layer, the data in cache usually will be stored in memory (which makes it fast), compared to databases which store the data into persistent layers (which makes it slow). &lt;/p&gt;
&lt;h3&gt;
  
  
  Solution Implementation
&lt;/h3&gt;

&lt;p&gt;There are multiple common patterns of caching usage, one of them is Cache-Aside pattern. We will use the Cache-Aside pattern to solve the problem. At its core, Cache-Aside is a simple yet effective strategy for optimizing the performance of data retrieval by (lazy) loading the data into a cache layer. By doing so, the Cache-Aside pattern reduces the number of requests made to the primary data source and improves the overall responsiveness of the application. In this case, we will use Redis, one of the most popular in-memory storage. Below is the simplified diagram to illustrate our proposed solution&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfrjee8ow335bgpuzy7z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfrjee8ow335bgpuzy7z.png" alt="Proposed solution" width="461" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without further ado, let’s implement it in our code:&lt;br&gt;
The first step is to configure it into our apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Configuration
@EnableCaching
public class RedisConfig{
   @Value("${spring.data.redis.host}")
   public String redisHost;


   @Bean
   JedisConnectionFactory jedisConnectionFactory() {
       JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory();
       jedisConnFactory.setHostName(redisHost);
       return jedisConnFactory;
   }


   @Bean
   public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate() {
       RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
       template.setConnectionFactory(jedisConnectionFactory());
       return template;
   }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let’s add the cache logic to our code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public List&amp;lt;Product&amp;gt; searchProductsByDisplayName(String displayName) {
       //Get entry from redis
       ValueOperations&amp;lt;String, Object&amp;gt; ops = redisTemplate.opsForValue();
       String key = RedisUtil.generateHashSHA256(displayName);
       String redisEntry = (String) ops.get(key);


       //cache hit
       if (redisEntry != null) {
           try {
               return mapper.readValue(redisEntry, new TypeReference&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;() {});
           } catch (JsonProcessingException e) {
               log.atError()
                   .addKeyValue("redis_entry", redisEntry)
                   .setMessage("Failed to convert redisEntry into List&amp;lt;Product&amp;gt;")
                   .log();
               throw new InternalError();
           }
       }
       //cache miss
       else {
           List&amp;lt;Product&amp;gt; products = productRepository.findProductByProductDisplayNameContainingIgnoreCase(displayName);
           try {
               ops.set(key, mapper.writeValueAsString(products));
           } catch (JsonProcessingException e) {
               log.atError()
                   .setMessage("Failed to convert List&amp;lt;Product&amp;gt; into String")
                   .log();
               throw new InternalError();
           }
           return productRepository.findProductByProductDisplayNameContainingIgnoreCase(displayName);
       }
   }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the explanation for above code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, we generate a hash from the input param.&lt;/li&gt;
&lt;li&gt;Second, we get an entry from redis using hashed value from #1 as the key. &lt;/li&gt;
&lt;li&gt;If it turns out there is an entry (cache hit), then we simply return it.&lt;/li&gt;
&lt;li&gt;If it turns out there is no entry, then we query from our primary database, and add the query result into cache, then return the query result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now after adding cache into our code, let’s run the same locust test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnhfg7cst9afrxveok27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnhfg7cst9afrxveok27.png" alt="Jaeger test" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hd9zo9nt49h5aawkv4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hd9zo9nt49h5aawkv4p.png" alt="Locust test" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2pr3ivlo8rjrjbyepc6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2pr3ivlo8rjrjbyepc6.png" alt="Locust test 2" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Based on the above result we can see that the RPS that our service can handle increases significantly from 200 to 1500 RPS or 750% increase. In addition the response time also dramatically reduced to below 60ms. However, based on the above data, we can still find bottlenecks in our system since the increase in RPS is not directly proportional to the increase of concurrent users. It stuck at 1500 RPS. This means that we still need to apply another strategy to truly make our service scalable (e.g to distribute the load to another computer since this experiment is run on a local machine).&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;In this article we have learned how to use redis for query caching to improve our system responsiveness. We first describe the problem, and then find the bottleneck (database call). After that we solve it by introducing cache which helps us reduce the number of requests made to the primary data source and improves the overall responsiveness of the application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
The result of this simple experiment can not be used as reference, because in this experiment it's assumed that the cache size is unlimited (no eviction policy set yet), In addition, since we only randomized 30 items to be called in our API, the cache hit rate is very high. In production settings, the capacity of cache will be limited and user input will be more varied, making the cache hit rate lower.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>locust</category>
      <category>springboot</category>
      <category>java</category>
    </item>
  </channel>
</rss>
