{"id":483,"date":"2021-10-05T15:51:09","date_gmt":"2021-10-05T15:51:09","guid":{"rendered":"https:\/\/fde.cat\/index.php\/2021\/10\/05\/lessons-learned-using-spring-data-redis\/"},"modified":"2021-10-05T15:51:09","modified_gmt":"2021-10-05T15:51:09","slug":"lessons-learned-using-spring-data-redis","status":"publish","type":"post","link":"https:\/\/fde.cat\/index.php\/2021\/10\/05\/lessons-learned-using-spring-data-redis\/","title":{"rendered":"Lessons Learned using Spring Data Redis"},"content":{"rendered":"<h3>Context<\/h3>\n<p>Our Commerce Cloud team that is in charge of the Omnichannel Inventory service uses <a href=\"https:\/\/redis.io\/\">Redis<\/a> as a remote cache to store data that lends itself for caching. The remote cache allows our multiple processes to get a synchronized and single view of the cached data. (See our previous blog post, <a href=\"https:\/\/engineering.salesforce.com\/coordinated-rate-limiting-in-microservices-bb8861126beb\">Coordinated Rate Limiting in Microservices <\/a>for an example of how we used Redis for Rate Limiting access to our system by tenants.) The usage pattern is entries that were <strong>shorter lived, high cache-hit and shared among instances<\/strong>. To interact with Redis, we utilized <a href=\"https:\/\/spring.io\/projects\/spring-data-redis\">Spring Data Redis <\/a>(with <a href=\"https:\/\/lettuce.io\/\">Lettuce<\/a>), which has been helping us to share our data entries among our instances and provide a low-code solution to interact with\u00a0Redis.<\/p>\n<p>A subsequent deployment of our applications showed an oddity, where the memory consumption on Redis was continuously increasing, with no indication of reducing.<\/p>\n<p>The memory growth on the Redis Server is reflected in the following graph:<\/p>\n<p>The memory consumption showed an almost linear increase over time, with increased throughput on the system and no significant reclamation over time. The scenario reached such extremes that it warranted a manual <em>flush<\/em> of the Redis database as the memory increased and neared 100%. The above seemed to point toward a memory leak happening with the Redis\u00a0entries.<\/p>\n<h3>Investigation<\/h3>\n<p>The first suspect was that the Redis entries were either configured without a Time to Live (TTL) or with a TTL value that was beyond the one intended. This was indicative that the Redis Repository entity class we had for Rate Limiting was devoid of any TTL configuration:<\/p>\n<p>@RedisHash(&#8220;rate&#8221;)<br \/>public class RateRedisEntry implements Serializable {<br \/>    @Id<br \/>    private String tenantEndpointByBlock; \/\/ A HTTP end point<br \/>    &#8230;<br \/>}<\/p>\n<p>\/\/ CRUD repository.<br \/>@Repository<br \/>public interface RateRepository extends CrudRepository&lt;RateRedisEntry, String&gt; {}<\/p>\n<p>To validate this on Data that the TTL was indeed not set, a connection was made to the Redis Server instance and the Redis command TTL &lt;name entry key&gt; was used to check the value for TTL on some of the entries that were\u00a0listed.<\/p>\n<p>TTL &#8220;rate:block_00001&#8221;<br \/>-1<\/p>\n<p>As shown above, there were entries that had a TTL of -1 indicating non-expiring. While this clearly appeared a suspect for the issue at hand, and fixing it to explicitly set TTL values to practice good software hygiene seemed the direction forward, there was some skepticism that this were the real cause of the problem due to the relatively small amount of entries and memory used being reported.<\/p>\n<p>With the TTL added, the entry code is show\u00a0below.<\/p>\n<p>@RedisHash(&#8220;rate&#8221;)<br \/>public class RateRedisEntry implements Serializable {<br \/>    @Id<br \/>    private String tenantEndpointByBlock;<\/p>\n<p>   <strong> @TimeToLive<\/strong><br \/><strong>    <\/strong>private Integer expirationSeconds;<br \/>    &#8230;<br \/>}<\/p>\n<p>When doing this exercise, we also noticed that on the entities that was, there was an <strong>extra entry<\/strong> created using the hash value that we declared, and this was happening in all of our interfaces that extended from CRUDRepository.<\/p>\n<p>An example was the following hash declared for <em>rate<\/em>\u00a0entries.<\/p>\n<p>@RedisHash(&#8220;rate&#8221;)<\/p>\n<p>In order to check this, we used the following Redis commands:<\/p>\n<p>KEYS *<br \/>1) &#8220;rate&#8221;<br \/>2) &#8220;block_00001&#8221;<\/p>\n<p>As you can see, there are <strong>two<\/strong> entries. One is an entry with key name \u201crate:block_00001\u201d and an extra entry with key \u201crate\u201d. The \u201crate:block_00001\u201dwas expected, but the other entry was surprising to\u00a0find.<\/p>\n<p>Monitoring the system over time also showed that the memory associated with the \u201crate\u201d key was steadily increasing.<\/p>\n<p>&gt;MEMORY USAGE &#8220;rate&#8221;<br \/>(integer) <strong>153034<\/strong><br \/>.<br \/>.<br \/>.<br \/>&gt; MEMORY USAGE &#8220;rate&#8221;<br \/>(integer) <strong>153876<\/strong><br \/>.<br \/>.<strong><br \/>&gt; <\/strong>MEMORY USAGE<strong> <\/strong>&#8220;rate&#8221;<br \/>(integer)<strong> 163492<\/strong><\/p>\n<p>In addition the to increased memory growth, the TTL on the \u201crate\u201d entry was -1 as shown by the\u00a0below:<\/p>\n<p>&gt;TTL &#8220;rate&#8221;<br \/>-1<br \/>&gt;TYPE &#8220;rate&#8221;<br \/>set<\/p>\n<p>It clearly pointed to the most plausible suspect where its growth showed no sign of reducing over\u00a0time.<\/p>\n<p>So, what was this entry and why was it\u00a0growing?<\/p>\n<p>Spring Data Redis creates a SET data type in Redis for every <em>@RedisHash<\/em>. The entries of the SET act as an index for many of the Spring Data Redis operations used by the CRUD repository.<\/p>\n<p>The SET entries, for example, look like the\u00a0below:<\/p>\n<p>&gt;SMEMBERS &#8220;rate&#8221;<br \/>1) &#8220;block_00001&#8221;<br \/>2) &#8220;block_00002&#8221;<br \/>3) &#8220;block_00003&#8221;<br \/>&#8230;<\/p>\n<p>We decided to <a href=\"https:\/\/stackoverflow.com\/questions\/68854213\/spring-data-redis-issue-with-crudrepository\/68894912\">post our situation in Stack Overflow<\/a> and on <a href=\"https:\/\/github.com\/spring-projects\/spring-data-redis\/issues\/2146\">Spring Data Redis\u2019s GitHub page<\/a> requesting some assistance from the community on the issue on how best to address this issue\u200a\u2014\u200aeither to prevent the growth of this SET, or how to prevent its creation, as we really did not need any other indexing\u00a0feature.<\/p>\n<p>While awaiting a response from the community, we discovered that <a href=\"https:\/\/docs.spring.io\/spring-data\/data-redis\/docs\/current\/reference\/html\/#redis.repositories.expirations\">enabling a property<\/a> of the Spring Data Redis annotation <em>EnableRedisRepositories<\/em> will actually make Spring Data Redis listen for <strong>KEY<\/strong> Events and clean up the Set over time as it receives <a href=\"https:\/\/docs.spring.io\/spring-data\/data-redis\/docs\/current\/reference\/html\/#redis.repositories.expirations\">KEY expired\u00a0events<\/a>.<\/p>\n<p>@EnableRedisRepositories(<strong>enableKeyspaceEvents <br \/>    = EnableKeyspaceEvents.ON_STARTUP)<\/strong><\/p>\n<p>With this setting enabled, Spring Data Redis will ensure that the memory of the Set does not keep increasing and that expired entries are purged out (See this<a href=\"https:\/\/stackoverflow.com\/questions\/41693774\/spring-redis-indexes-not-deleted-after-main-entry-expires\/41695902\"> Stack Overflow Question<\/a> on details).<\/p>\n<p>&#8220;rate&#8221;<br \/> &#8220;rate:block_00001&#8221;<br \/><strong> &#8220;rate:block_00001:phantom&#8221;<\/strong> &lt;&#8211; <strong>Phantom entry in addition to base<\/strong><br \/> &#8230;<\/p>\n<p>The Phantom Keys are created so that Spring Data Redis can propagate a <a href=\"https:\/\/docs.spring.io\/spring-data\/redis\/docs\/current\/api\/org\/springframework\/data\/redis\/core\/RedisKeyExpiredEvent.html\"><em>RedisKeyExpiredEvent<\/em><\/a> with relevant data to Spring Framework\u2019s <a href=\"https:\/\/docs.spring.io\/spring-framework\/docs\/current\/javadoc-api\/org\/springframework\/context\/ApplicationEvent.html\">ApplicationEvent<\/a> subscribers. The Phantom (or Shadow) entry is longer lived than the entry it is shadowing, so when the primary entry expired event is received by Spring Data Redis, it will obtain values from the Shadow entry to propagate the <em>RedisKeyExpiredEvent, <\/em>which will house a copy of the expired domain object in addition to the\u00a0key.<\/p>\n<p>The following code in Spring Data Redis receives the phantom entry expiration and purges the item from the\u00a0index:<\/p>\n<p>static class MappingExpirationListener extends KeyExpirationEventMessageListener {<\/p>\n<p> private final RedisOperations&lt;?, ?&gt; ops;<br \/> &#8230;<br \/> @Override<br \/> public void onMessage(Message message, @Nullable byte[] pattern) {<br \/>    &#8230;<br \/>    RedisKeyExpiredEvent event = new RedisKeyExpiredEvent(channel, key, value);<\/p>\n<p>    ops.execute((RedisCallback&lt;Void&gt;) connection -&gt; {<br \/>        \/\/ Removes entry from the Set<br \/>        connection.sRem(converter.getConversionService()<br \/>            .convert(event.getKeyspace(), byte[].class), event.getId());<br \/>        &#8230;<br \/>    });<br \/> }<br \/>..<br \/>}<\/p>\n<p>The primary concern with this approach is the additional processing overhead that Spring Data Redis incurs for having to consume the expired event stream and perform the clean up. It should also be noted that, since the Redis Pub\/Sub messages are not persistent, if entries expire while the application is down, then expired events are not processed, and those entries will not get cleaned up from the\u00a0SET.<\/p>\n<p>Using CRUDRepository effectively means more shadow\/support entries are created for each entry, causing more consumption of memory from the Redis server\u2019s database. If one does not need the details of the expiration in the Spring Boot Application when an entry expired, you can disable the generation of the Phantom entries with the following change to the <em>EnableRedisRespositories<\/em> annotation.<\/p>\n<p>@EnableRedisRepositories(..<strong> shadowCopy = ShadowCopy.OFF<\/strong>)<\/p>\n<p>The net effect of the above is that Spring Data Redis will no longer create the Shadow copy but will still subscribe for the Keyspace events and purge the SET of the entry. Spring Boot Application Events propagated will only contain the KEY and not the full Domain\u00a0object.<\/p>\n<p>With all the above discovered around performance and additional memory storage, we decided that it felt that for the use cases we were dealing with, this additional overhead added by Redis CRUDRepository and KEY Space events was not something that was appealing to us. For this reason, we decided to explore a more leaner approach.<\/p>\n<p>We made a proof-of-concept application to test the differences in response time between using CrudRepository or working directly with the RedisTemplate class that exposes the Redis server operations. Through testing we observed RedisTemplate to be more favorable.<\/p>\n<p>This graph is from the metrics in our system using <em>CRUDRepository<\/em>, which includes the overhead of handling the\u00a0indexes.This graph represents metrics from the proof-of-concept application that doesn\u2019t use <em>CRUDRepository<\/em> and works directly with <em>RedisTemplate<\/em>.<\/p>\n<p>The comparison was made by executing GET operations non-stop for five minutes and taking an average of the time it took to complete the operation. What we saw is that the almost all GET operations using CRUDRepository were in the milliseconds range, while the proof-of-concept without CRUDRepository was mostly on the nano-seconds realm. Another thing we noticed was that CRUDRepository also has a tendency to have more upticks when performing the operations, increasing the latency of executing its operations.<\/p>\n<h3>Solution<\/h3>\n<p>Based on the research, we were down to the following paths\u00a0forward:<\/p>\n<p><strong>Spring Data Redis CrudRepository: <\/strong>Enable Redis Repository\u2019s key space events and enable Spring Data Redis to purge the Set type of expired entries. The upside of this approach is that it is a low-code solution achieved by setting a value on an annotation, letting Spring Data Redis subscribe for KEY expired events and clean up behind the scenes. The downside is, for our case, the additional usage of memory for something we never utilize, i.e., the SET index and the processing overhead occurred by Spring Data Redis subscribing for Keyspace events and performing the\u00a0cleanup.<strong>Custom Repository using RedisTemplate:<\/strong> Handle the Redis I\/O Operation without using the CRUD Repository, use the <em>RedisTemplate,<\/em> and build the basic needed operations. The upside is that it results in creating only what data we need in Redis, i.e., the hash entries, and not other artifacts like the SET index. We avoid the processing overhead of Spring Data Redis subscribing and processing Keyspace events for clean up. The downside is that we stop availing the low-code magic of Spring Data Redis\u2019s CRUD repository and the work it does behind the scenes and instead do all the work with\u00a0code.<\/p>\n<p>After considering all of our findings, especially the metrics around the proof-of-concept application and our system, and the needs that we have on the team, which are more about quick response times and low memory usage, the direction we adopted was not to use the <em>CrudRepository<\/em> but to use the <a href=\"https:\/\/docs.spring.io\/spring-data\/redis\/docs\/current\/api\/org\/springframework\/data\/redis\/core\/RedisTemplate.html\">RedisTemplate<\/a> to interact with the Redis server. It presents a solution that includes far less unknown behavior, due to the code being more transparent and the functionality more straight\u00a0forward.<\/p>\n<p>Our code ended up looking like\u00a0this:<\/p>\n<p>public class RateRedisEntry implements Serializable {<br \/>   private String tenantEndpointByBlock;<br \/>   private Integer expirationSeconds;<br \/>    &#8230;<br \/>}<br \/>@Bean<br \/>public RedisTemplate&lt;String, RateRedisEntry&gt; redisTemplate() {<br \/>   RedisTemplate&lt;String, RateRedisEntry&gt; template = new RedisTemplate&lt;&gt;();<\/p>\n<p>   template.setConnectionFactory(getLettuceConnectionFactory());<\/p>\n<p>   return template;<br \/>}<br \/>public class RedisCachedRateRepositoryImpl implements RedisCachedRateRepository {<\/p>\n<p>    private final RedisTemplate&lt;String, RateRedisEntry&gt; redisTemplate;<\/p>\n<p>    public RedisCachedRateRepositoryImpl(RedisTemplate&lt;String, RateRedisEntry&gt; redisTemplate) {<br \/>        this.redisTemplate = redisTemplate;<br \/>    }<\/p>\n<p>    public Optional&lt;RateRedisEntry&gt; find(String key, Tags tags) {<br \/>        return Optional.ofNullable(this.redisTemplate.opsForValue()<br \/>        .get(composeHeader(key)));<br \/>    }<\/p>\n<p>    public void put(final @NonNull RateRedisEntry rateEntry, Tags tags) {<br \/>        this.redisTemplate.opsForValue().set(composeHeader(rateEntry.getTenantEndpointByBlock()),<br \/>            rateEntry, Duration.ofSeconds(rateEntry.getRedisTTLInSeconds()));<br \/>    }<\/p>\n<p>    private String composeHeader(String key) {<br \/>        return String.format(&#8220;rate:%s&#8221;, key);<br \/>    }<br \/>}<\/p>\n<p>By using it this way, we worked directly with the entries, so there are no risks of unwanted indexes or structures being\u00a0stored.<\/p>\n<p>Once our solution got deployed, our memory usage totally dropped and remained stable, with any peaks going down after the TTL of the entries reached\u00a00.<\/p>\n<p>Another benefit that we saw of using the RedisTemplate direct approach was an improvement in our response times of the operations we were performing, as shown on the comparison metrics above from running the GET operation for some time on our proof-of-concept and our system. Before the change, we saw values that averaged in the <strong>milliseconds<\/strong>; after deploying the change, we started seeing that most operations were being completed in <strong>nanoseconds<\/strong>.<\/p>\n<h3>Conclusion<\/h3>\n<p>The magic of Spring Data Redis Crud Operations is achieved by the creation of additional data structures like the SET for Indexes. These additional data structures are not cleaned up when items expire <strong>without enabling<\/strong> Spring Data Redis to listen for KEY space events. For caching patterns where entries are very long-lived or where the set of entries is tractable and finite, Spring Data Redis with CrudRepositories provides a low-code solution for CRUD operations for\u00a0Redis.<\/p>\n<p>However, for caching patterns where the data is cached and shared by multiple processes and where the entries have a smaller window where they can be cached, avoiding listening for KEY events and using the <em>RedisTemplate<\/em> to perform Redis operations for the CRUD operations needed seems\u00a0optimal.<\/p>\n<p>Share your thoughts in the comments below; we\u2019d love to hear of your experiences with Spring Data Redis or Redis in\u00a0general.<\/p>\n<p>Appreciation Note: Thanks to <a href=\"https:\/\/www.linkedin.com\/in\/sanjayvacharya\">Sanjay Acharya<\/a> and Balachandar Mariappan for all the help given with the reviewing and refining. You guys are awesome\u2026\u00a0\ud83d\ude42<\/p>\n<p><a href=\"https:\/\/engineering.salesforce.com\/lessons-learned-using-spring-data-redis-f3121f89bff9\">Lessons Learned using Spring Data Redis<\/a> was originally published in <a href=\"https:\/\/engineering.salesforce.com\/\">Salesforce Engineering<\/a> on Medium, where people are continuing the conversation by highlighting and responding to this story.<\/p>\n<p><a href=\"https:\/\/engineering.salesforce.com\/lessons-learned-using-spring-data-redis-f3121f89bff9?source=rss----cfe1120185d3---4\">Read More<\/a><\/p>","protected":false},"excerpt":{"rendered":"<p>Context Our Commerce Cloud team that is in charge of the Omnichannel Inventory service uses Redis as a remote cache to store data that lends itself for caching. The remote cache allows our multiple processes to get a synchronized and single view of the cached data. (See our previous blog post, Coordinated Rate Limiting in&hellip; <a class=\"more-link\" href=\"https:\/\/fde.cat\/index.php\/2021\/10\/05\/lessons-learned-using-spring-data-redis\/\">Continue reading <span class=\"screen-reader-text\">Lessons Learned using Spring Data Redis<\/span><\/a><\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"spay_email":"","footnotes":""},"categories":[7],"tags":[],"class_list":["post-483","post","type-post","status-publish","format-standard","hentry","category-technology","entry"],"jetpack_featured_media_url":"","jetpack-related-posts":[{"id":513,"url":"https:\/\/fde.cat\/index.php\/2021\/12\/09\/using-redis-hash-instead-of-set-to-reduce-cache-size-and-operating-costs\/","url_meta":{"origin":483,"position":0},"title":"Using Redis HASH instead of SET to reduce cache size and operating costs","date":"December 9, 2021","format":false,"excerpt":"What if we told you that there was a way to dramatically reduce the cost to operate on cloud providers? That\u2019s what we found when we dug into the different data structures offered in Redis. Before we committed to one, we did some research into the difference in memory usage\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":337,"url":"https:\/\/fde.cat\/index.php\/2021\/08\/31\/coordinated-rate-limiting-in-microservices\/","url_meta":{"origin":483,"position":1},"title":"Coordinated Rate Limiting in Microservices","date":"August 31, 2021","format":false,"excerpt":"The ProblemAny multitenant service with public REST APIs needs to be able to protect itself from excessive usage by one or more tenants. Additionally, as the number of instances that support these services is dynamic and varies based on load, the need arrises to perform coordinated rate limiting on a\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":299,"url":"https:\/\/fde.cat\/index.php\/2021\/08\/31\/caching-with-the-salesforce-commerce-sdk\/","url_meta":{"origin":483,"position":2},"title":"Caching with the Salesforce Commerce SDK","date":"August 31, 2021","format":false,"excerpt":"Co-written by Brian\u00a0RedmondEvery e-commerce application is going to need caching. For some of our customers, millions of shoppers may look at the same product information and, if you have to request that information from the service every time, your application will not scale. This is why we built the Commerce\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":466,"url":"https:\/\/fde.cat\/index.php\/2021\/09\/16\/autonomous-monitoring-and-healing-networks\/","url_meta":{"origin":483,"position":3},"title":"Autonomous Monitoring and Healing Networks","date":"September 16, 2021","format":false,"excerpt":"Autonomous Monitoring and Self-Healing Networks Occasional failure is inevitable in any network system. The need of the hour is a robust, self-reliant automated monitoring tool that provides great insight and a lesser degree of manual intervention. We need autonomous interventions that save us time and enhance system availability. What Salesforce\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":693,"url":"https:\/\/fde.cat\/index.php\/2023\/03\/27\/sre-weekly-issue-365\/","url_meta":{"origin":483,"position":4},"title":"SRE Weekly Issue #365","date":"March 27, 2023","format":false,"excerpt":"View on sreweekly.com A message from our sponsor, Rootly: Manage incidents directly from Slack with Rootly\u00a0\ud83d\ude92. Rootly automates manual tasks like creating an incident channel, Jira ticket and Zoom rooms, inviting responders, creating statuspage updates, postmortem timelines and more. Want to see why companies like Canva and Grammarly love us?:\u2026","rel":"","context":"In &quot;SRE&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":864,"url":"https:\/\/fde.cat\/index.php\/2024\/05\/13\/sre-weekly-issue-424\/","url_meta":{"origin":483,"position":5},"title":"SRE Weekly Issue #424","date":"May 13, 2024","format":false,"excerpt":"View on sreweekly.com A message from our sponsor, FireHydrant: FireHydrant is now AI-powered for faster, smarter incidents! Power up your incidents with auto-generated real-time summaries, retrospectives, and status page updates. https:\/\/firehydrant.com\/blog\/ai-for-incident-management-is-here\/ My Availability Investment Playbook Here\u2019s an ultra-practical guide to pushing for reliability investments at your company, formatted as a\u2026","rel":"","context":"In &quot;SRE&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"_links":{"self":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/posts\/483","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/comments?post=483"}],"version-history":[{"count":0,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/posts\/483\/revisions"}],"wp:attachment":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/media?parent=483"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/categories?post=483"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/tags?post=483"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}