{"id":14909,"date":"2023-08-08T10:00:00","date_gmt":"2023-08-08T17:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cse\/?p=14909"},"modified":"2024-07-18T11:48:56","modified_gmt":"2024-07-18T18:48:56","slug":"cosmos-with-integrated-cache","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/cosmos-with-integrated-cache\/","title":{"rendered":"Using Azure Cosmos DB with Integrated Cache"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>Have you ever had a use case in which an application receives a high volume of repeated requests against a rarely-changing data set? Caching is the obvious choice to prevent your database from being overwhelmed by too many requests at once, but adding an external cache such as <a href=\"https:\/\/azure.microsoft.com\/en-us\/products\/cache\/\">Azure Redis<\/a> means creating and maintaining an additional dependency. What if you could benefit from caching without having to directly manage your cache?<\/p>\n<p>My team recently had the opportunity to build a project using Azure Cosmos DB with integrated cache. We worked on a customer engagement for a use case that involved heavy reads, low writes, and repeated queries. For our backing datastore we used Azure Cosmos DB, a horizontally-scaled document database. We decided to try out the integrated cache feature because it looked like it might be a good fit for our customer&#8217;s use case. We spoke with members of Azure Cosmos DB product team to gain a better understanding of how the integrated cache operates and we applied those learnings to our project.<\/p>\n<h2>What is Azure Cosmos DB with integrated cache?<\/h2>\n<p>The <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/integrated-cache\">Azure Cosmos DB integrated cache<\/a> is essentially a turn-key cache that is managed for you. While Azure Cosmos DB does support multiple APIs, the integrated cache is currently only available for the <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/choose-api#coresql-api\">NoSQL API<\/a>. With the integrated cache you don\u2019t need to set up any additional caching resources or dependencies and your code can simply interact with Cosmos DB without having to explicitly manage the cache.<\/p>\n<p>When you create your Azure Cosmos DB account and enable the <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/dedicated-gateway\">dedicated gateway<\/a>, the integrated cache is configured automatically. As part of the process to <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/how-to-configure-integrated-cache?tabs=dotnet\">provision the dedicated gateway<\/a>, you select the number of gateway nodes you\u2019d like to create. One node is sufficient for development purposes, but for use in production it is recommended to have a minimum of three. The <a href=\"https:\/\/azure.microsoft.com\/en-in\/pricing\/details\/cosmos-db\/autoscale-provisioned\/\">cost for the dedicated gateway<\/a> is calculated hourly and is determined by the number of nodes and the region selected. Since queries that hit the integrated cache incur no request unit (RU) charges, the overall cost of the database usage will be primarily driven by this hourly charge.<\/p>\n<h2>When is Azure Cosmos DB with Integrated Cache a good fit?<\/h2>\n<p>Because the integrated cache is managed for you, it does not give you explicit controls over its operations the way a stand-alone cache would. Therefore it is best suited for applications that:<\/p>\n<ul>\n<li>Are read-heavy<\/li>\n<li>Have data that changes infrequently<\/li>\n<li>Need low latency<\/li>\n<li>Have repeated queries that benefit from caching<\/li>\n<li>Tolerate eventual or <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/integrated-cache#session-consistency\">session consistency<\/a><\/li>\n<li>Can operate without the need for an explicit cache refresh<\/li>\n<\/ul>\n<p>Let&#8217;s take a global e-commerce platform as an example. Such applications are predominantly read-heavy. Users frequently view product listings, but the underlying product information doesn&#8217;t change very often. Because these types of queries are recurrent, the low latency offered by Azure Cosmos DB with Integrated Cache can considerably enhance the user experience. The eventual or session consistency models work well in this scenario because users don&#8217;t need immediate updates on slight changes such as new product reviews. Moreover, as these applications don&#8217;t need explicit control over cache refreshing, the automatic background refreshes provided by Azure Cosmos DB with Integrated Cache can maintain a high-quality user experience even in the face of occasional minor data discrepancies.<\/p>\n<h2>Architecture of Azure Cosmos DB with Integrated Cache<\/h2>\n<p>When an application sends a request to Azure Cosmos DB using the dedicated gateway, this request first reaches a dedicated gateway node. This node checks its cache for the requested data. If the data isn&#8217;t present in the cache (a &#8220;cache miss&#8221;), the gateway node communicates with the Azure Cosmos DB database to fetch the necessary data. After fetching the data from the database, the gateway node stores a copy of it in its own cache for future requests and then finally sends the data back to the original caller. Each cache miss requires data retrieval from the database node, which results in request unit charges.<\/p>\n<p>Each dedicated gateway node <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/dedicated-gateway\">maintains its own cache<\/a>. This means you can potentially get back different data depending on which node the request is routed through (see the <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/integrated-cache\">docs<\/a> and the animation below). The backing data in the database is replicated as usual across all the backend nodes, but caches are not replicated across dedicated gateway nodes. Calls are routed to dedicated gateway nodes randomly, so it is possible to have a cache miss even if the data is cached on another dedicated gateway node.<\/p>\n<p>Azure Cosmos DB integrated cache has an item cache and a query cache:<\/p>\n<ul>\n<li>The <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/integrated-cache#item-cache\">item cache<\/a> services point reads where the <code>key<\/code> is a composite of the item id and partition key and the <code>value<\/code> is the data in the associated document. The item cache works like write-through cache, which means that the data in the cache is updated at the time it is written.<\/li>\n<li>The <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/integrated-cache#query-cache\">query cache<\/a> services queries. For example, a query with a <code>WHERE clause<\/code> will be cached in the query cache even if it returns a single value. The <code>key<\/code> for an item in the query cache is based on the query text and the <code>value<\/code> is the result of that query. Even if a particular document has been cached previously as part of the result for another query, it will be cached again if a given query varies from the previous one. For example, a request that returns documents [1..40] will be cached as one query result and a request that returns documents [2..41] will be cached as a distinct query result. Changes to the backing data do not affect cached values in the query cache.<\/li>\n<\/ul>\n<h2>Cache Refresh<\/h2>\n<p>Azure Cosmos DB with integrated cache handles cache refresh differently from other caching technologies. At this time there is no explicit cache invalidation, but that may be included in a future release. A workaround for if you need to invalidate all cached data on all dedicated gateway nodes is to deprovision and re-provision the dedicated gateway.<\/p>\n<p>As a cache on a given dedicated gateway node fills up with data, it evicts the least-recently accessed values. The item cache is automatically &#8220;refreshed&#8221; when an item is newly created, updated, or deleted. That is not true for values in the query cache.<\/p>\n<p>The primary mechanism to trigger a query cache refresh is via the <code>MaxIntegratedCacheStaleness<\/code> value that is passed in when an application requests data from Cosmos DB. The calling application essentially tells the dedicated gateway what its tolerance is for stale data. That tolerance may be mere minutes to several days depending on the application\u2019s use cases for the data. When the <code>MaxIntegratedCacheStaleness<\/code> value exceeds the amount of time that has passed since a query result was cached, the query is run against the database again and the updated result is stored in the cache on that dedicated gateway node. If an application queries Cosmos with a <code>MaxIntegratedCacheStaleness<\/code> of 0, it will always get back data from the database and the cache on that dedicated gateway node will be refreshed to store the new query results. The caches on any other dedicated gateway nodes will not be refreshed until a request that \u201cexpires\u201d the cached values is routed to that node or it is evicted according to the Least Recently Used (LRU) eviction policy.<\/p>\n<p>Be aware that queries that return no results are cached. This is an edge case in which an application queries data that does not yet exist, the data is then inserted into the database, and the same query is run again. We encountered this case during testing for our project. As a result we decided to skip the cache for certain types of queries in which the backing data changes more frequently. To always hit the database, you can connect to Cosmos DB via <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/nosql\/sdk-connection-modes\">direct mode<\/a> even with an account that has a dedicated gateway.<\/p>\n<h2>Azure Cosmos DB with Integrated Cache Request Routing and Cache Refresh<\/h2>\n<p>Below is an animation of what happens when a request is sent to the dedicated gateway endpoint.<\/p>\n<ul>\n<li>If the data requested is not in the cache, the request is routed to a backend database node. The data is retrieved and then cached on the dedicated gateway node prior to being returned to the caller.<\/li>\n<li>If the data is in the cache and the age of the cached data is within the <code>MaxIntegratedCacheStaleness<\/code> value sent with the request, no call is sent to a backend database node. The cached data is returned.<\/li>\n<li>If the data is in the cache but the age of the cached data <em>exceeds<\/em> the <code>MaxIntegratedCacheStaleness<\/code> value sent with the request, the data is retrieved from a backend database node, cached in the database, and then returned.<\/li>\n<\/ul>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2023\/08\/CosmosRequestRouting.gif\" alt=\"Animation Showing Azure Cosmos DB Request Routing\" \/><\/p>\n<h2>Code Examples<\/h2>\n<p>You can download and walk through the code for my working sandbox project that uses Azure Cosmos DB with integrated cache. Start with the <a href=\"https:\/\/github.com\/veritaserum27\/CosmosIntegratedCacheTest\/blob\/main\/README.md\">README<\/a> to understand how to set up for local development.<\/p>\n<h3>Configure CosmosClient to Use Dedicated Gateway<\/h3>\n<p>To configure a <code>CosmosClient<\/code> to use your dedicated gateway, you will need to supply your dedicated gateway connection string as well as <code>CosmosClientOptions<\/code> that specify the <code>ConnectionMode<\/code> and <code>ConsistencyLevel<\/code> for the client to use.<\/p>\n<pre><code class=\"language-C#\">    var dedicatedConnectionString = \"AccountEndpoint=https:\/\/&lt;cosmos-db-account-name&gt;.sqlx.cosmos.azure.com\/;AccountKey=&lt;cosmos-db-account-key&gt;;\";\r\n    var dedicatedGatewayCosmosClient = new CosmosClient(dedicatedConnectionString,\r\n    new CosmosClientOptions()\r\n    {\r\n        ConnectionMode = ConnectionMode.Gateway,\r\n        ConsistencyLevel = ConsistencyLevel.Eventual\r\n    });<\/code><\/pre>\n<h3>Hit the Integrated Query Cache<\/h3>\n<p>To build a query that hits the query cache, use the <code>GetItemLinqQueryable<\/code> method on your <code>CosmosContainer<\/code> object. In your <code>QueryRequestOptions<\/code> add the value you wish to use for <code>MaxIntegratedCacheStaleness<\/code> and set your <code>ConsistencyLevel<\/code>.<\/p>\n<pre><code class=\"language-C#\">    var sortedRefIds = refIds.OrderBy(id =&gt; id).ToList(); \/\/ Always sending a request in a specific order will maximize your chances of a cache hit\r\n\r\n    IQueryable&lt;TestItem&gt; queryable = containerWithDedicatedConnection.GetItemLinqQueryable&lt;TestItem&gt;(\r\n            requestOptions: new QueryRequestOptions\r\n            {\r\n                DedicatedGatewayRequestOptions = new DedicatedGatewayRequestOptions()\r\n                {\r\n                    MaxIntegratedCacheStaleness = TimeSpan.FromHours(cacheStaleness)\r\n                },\r\n                ConsistencyLevel = ConsistencyLevel.Eventual\r\n            }\r\n        );\r\n\r\n    queryable = queryable.Where(e =&gt; sortedRefIds.Contains(e.RefId));\r\n\r\n    using (var linqFeed = queryable.ToFeedIterator())\r\n    {\r\n        while (linqFeed.HasMoreResults)\r\n        {\r\n            var feedResponse = await linqFeed.ReadNextAsync();\r\n\r\n            result.RequestUnits = feedResponse.RequestCharge; \/\/ When this has a value of 0, that means you've successfully hit the cache\r\n\r\n            foreach (var item in feedResponse)\r\n            {\r\n                testItems.Add(item);\r\n            }\r\n        }\r\n    }<\/code><\/pre>\n<h3>Hit the Item Cache<\/h3>\n<p>To read from the item cache, send a <code>ReadItemAsync<\/code> request along with <code>ItemRequestOptions<\/code> that point the request to use the dedicated gateway.<\/p>\n<pre><code class=\"language-C#\">    ItemResponse&lt;TestItem&gt; response = await containerWithDedicatedConnection.ReadItemAsync&lt;TestItem&gt;(id, new PartitionKey(partitionKey), \r\n        new ItemRequestOptions() \r\n        {\r\n            DedicatedGatewayRequestOptions = new DedicatedGatewayRequestOptions()\r\n            {\r\n                MaxIntegratedCacheStaleness = TimeSpan.FromHours(cacheStaleness)\r\n            },\r\n            ConsistencyLevel = ConsistencyLevel.Eventual\r\n        });\r\n\r\n    var testItemQueryResult = new TestItemQueryResult();\r\n\r\n    testItemQueryResult.TestItems.Add(response.Resource); \/\/ the document\r\n    testItemQueryResult.RequestUnits = response.RequestCharge; \/\/ the request unit charge\r\n\r\n    return testItemQueryResult;<\/code><\/pre>\n<p>The document will be returned in the <code>Resource<\/code> property on the <code>ItemResponse<\/code> object and the number of Request Units consumed will be returned on the <code>RequestCharge<\/code> property.<\/p>\n<h2>Conclusion<\/h2>\n<p>The simplification of application architecture is what we liked the most about Azure Cosmos DB with integrated cache. It&#8217;s convenient to be able to simply write Cosmos queries and know that cache hits and misses are handled automatically. For usage patterns involving a high volume of <em>repeated<\/em> requests, Azure Cosmos DB with integrated cache can serve thousands of requests per second without a corresponding high Request Unit charge. The dedicated gateway costs can be predicted in advance, which can result in overall lower Cosmos DB charges as compared to a Azure Cosmos DB <em>without<\/em> an integrated cache. We look forward to additional features the Azure Cosmos DB product team may develop to make this an even more robust product.<\/p>\n<h2>Acknowledgements<\/h2>\n<p>I want to give thanks to crew members <a href=\"https:\/\/www.linkedin.com\/in\/bindu-msft-cse\/\">Bindu Chinnasamy<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/johnhauppa\/\">John Hauppa<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/cameron-taylor-a27078127\/\">Cameron Taylor<\/a>, and <a href=\"https:\/\/www.linkedin.com\/in\/kanishk-t-1723b2107\/\">Kanishk Tantia<\/a>. I also want to thank <a href=\"https:\/\/www.linkedin.com\/in\/justine-cocchi-51a977108\/\">Justine Cocchi<\/a> and <a href=\"https:\/\/www.linkedin.com\/in\/madhavannamraju\/\">Madhav Annamraju<\/a> who provided information and resources to help us understand this feature better.<\/p>\n<p>This article is also shared on <a href=\"https:\/\/medium.com\/@laura.l.lund\/exploring-cosmos-with-integrated-cache-3df25f72919\">Medium<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Industry use case for Cosmos DB with Integrated Cache<\/p>\n","protected":false},"author":122005,"featured_media":14910,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[3436,3434,3433,3435],"class_list":["post-14909","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","tag-azure-app-services","tag-cache","tag-cosmosdb","tag-csemake"],"acf":[],"blog_post_summary":"<p>Industry use case for Cosmos DB with Integrated Cache<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14909","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/122005"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=14909"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14909\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/14910"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=14909"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=14909"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=14909"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}