📜 ⬆️ ⬇️

Using Caching Infrastructure in ASP.NET, continued

In the last post I told you how to use the cache infrastructure in ASP.NET to increase the performance of the site. By adding a few lines of code I was able to increase the performance of the site’s home page 5 times. This time, let's go ahead and squeeze even more performance without resorting to different hacks.

For example, I still use the Mvc Music Store project .
If you have not read the previous posts, then it's time to see how the home page was sped .

All optimization concerned the home page, now I will pass in the internal.

Load test


For verification, I made a load test in Visual Studio for 25 "virtual users" with the following script:
1) Request home page
2) Request a page of the genre (catalog)
3) Request an album page (product)
For fidelity, I’ve made visits not on the same page of the catalog \ product, but randomly assigned to different pages.
And also increased the percentage of new users to 80%, which is true.
')
The result is 42 scripts per second.

Adding Caching - A Simple Approach


In ASP.NET, you can set attributes, caching with dependencies on the database.
To do this, you need to follow a few simple steps:
1. Enter caching parameters in web.config

<system.web> <caching> <sqlCacheDependency enabled="true" pollTime="1000"> <databases> <add name="MusicStore" connectionStringName="MusicStoreEntities" /> </databases> </sqlCacheDependency> <outputCacheSettings> <outputCacheProfiles> <add name="Catalog" sqlDependency="MusicStore:Genres;MusicStore:Albums" duration="86400" location="ServerAndClient" varyByParam="Genre" enabled="true" /> </outputCacheProfiles> </outputCacheSettings> </caching> </system.web> 

The sqlCacheDependency element defines cache dependencies on the database. The database dependency will check for changes at the pollTime interval, in this case 1000 milliseconds (1 second).
The outputCacheProfiles element sets profiles so as not to repeat the same settings for different actions. In addition, it allows you to manage caching, without rebuilding the project.

2. Make changes to the database schema so that dependencies work

To do this, at the start of the application, call the following lines of code
 String connStr = System.Configuration.ConfigurationManager.ConnectionStrings["MusicStoreEntities"].ConnectionString; System.Web.Caching.SqlCacheDependencyAdmin.EnableNotifications(connStr); System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, "Genres"); System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, "Albums"); 


3. Add Attributes

 [OutputCache(CacheProfile = "Catalog")] public ActionResult Browse(string genre) { //... } [OutputCache(CacheProfile = "Catalog")] public ActionResult Details(int id) { //... } 


Run the test again - 60 scripts per second. That is, it was possible to increase the speed in this case by almost 50%.

Installing dependencies from code


If you use WebAPI, you will not be able to use caching attributes. But in this case the SqlCacheDependency class will help you. It is very simple to use it - in the constructor you specify the database name from the web.config and the table name. An instance of SqlCacheDependency can be used to specify dependencies of items in the local cache.

So you can make caching navigation, if you cache all the pages will be unprofitable.
 [ChildActionOnly] public ActionResult GenreMenu() { var cacheKey = "Nav"; var genres = this.HttpContext.Cache.Get(cacheKey); if (genres == null) { genres = storeDB.Genres.ToList(); this.HttpContext.Cache.Insert(cacheKey, genres, new SqlCacheDependency("MusicStore","Genres")); } return PartialView(genres); } 


There is another SqlCacheDependency constructor that takes a SqlCommand . This is a completely different mechanism for tracking changes in the database, built on alerts from SQL Server Service Broker. I tried to use these alerts, but they do not work for all requests. And if the request is “wrong”, then no errors occur and the alert arrives immediately after creation. In addition, alerts are very slow. According to my measurements in 8 times slow down the entry in the table.

but on the other hand


Dependencies on the database are not at all free. For their work, triggers are created that trigger the creation, modification and deletion of records. These triggers update information in the service table about which tables and when they were changed. And the flow on the side of the application periodically reads the table and notifies the dependencies.

If volume changes occur infrequently, then the overhead of triggers is small. And if changes occur often, the cache efficiency drops. In the example of the Mvc Music Store, any change to any album will reset the entire cache for the entire catalog.

What to do?


If you stay within one server, then the same approach as used for caching a basket in the Mvc Music Store is suitable - to save individual items or data samples in the cache, and when writing - to reset the cache (more in early post ). With proper selection of the granularity of caching and flushing, you can achieve high cache efficiency.

But when scaling to multiple servers, this approach almost always does not work. In the case of the basket, it will work only in the case of client affinity, when the same client comes to the same server. Modern NLBs provide this, but in the case of, for example, caching goods, the client affinity will not help.

The distributed cache will help us.

If you have already installed more than one web server to service requests, then it is worth thinking about distributed cache.
One of the best options for today is Redis. It is available both on premises and in the Microsoft Azure cloud.

To add Redis to an ASP.NET project, open the Package Manager Console and execute a couple of commands.
 Install-Package Redis-64 Install-Package StackExchange.Redis 


Redis supports an excellent feature - the so-called Keyspace Notifications (http://redis.io/topics/notifications). This allows you to track when an item has been changed, even if changes occur on another server.

To integrate this feature in ASP.NET, I wrote a small class:
 class RedisCacheDependency: CacheDependency { public RedisCacheDependency(string key):base() { Redis.Client.GetSubscriber().Subscribe("__keyspace@0__:" + key, (c, v) => { this.NotifyDependencyChanged(new object(), EventArgs.Empty ); }); } } 

This class implements CacheDependency in Redis.

And now the client himself:
 public static class Redis { public static readonly ConnectionMultiplexer Client = ConnectionMultiplexer.Connect("localhost"); public static CacheDependency CreateDependency(string key) { return new RedisCacheDependency(key); } public static T GetCached<T>(string key, Func<T> getter) where T:class { var localCache = HttpRuntime.Cache; var result = (T) localCache.Get(key); if (result != null) return result; var redisDb = Client.GetDatabase(); var value = redisDb.StringGet(key); if (!value.IsNullOrEmpty) { result = Json.Decode<T>(value); localCache.Insert(key, result, CreateDependency(key)); return result; } result = getter(); redisDb.StringSet(key, Json.Encode(result)); localCache.Insert(key, result, CreateDependency(key)); return result; } public static void DeleteKey(string key) { HttpRuntime.Cache.Remove(key); var redisDb = Client.GetDatabase(); redisDb.KeyDelete(key); } } 

The GetCached method saves the result in the ASP.NET local cache. The local cache is very fast, checking the item in the cache takes nanoseconds. This is much faster than a remote request to Redis + serialization-deserialization.

Now I can bind the item in the Redis cache to the page cache:
 public ActionResult Browse(string genre) { var cacheKey = "catalog-" + genre; var genreModel = Redis.GetCached(cacheKey, () => (from g in storeDB.Genres where g.Name == genre select new GenreBrowse { Name = g.Name, Albums = from a in g.Albums select new AlbumSummary { Title = a.Title, AlbumId = a.AlbumId, AlbumArtUrl = a.AlbumArtUrl } } ).Single() ); this.Response.AddCacheItemDependency(cacheKey); this.Response.Cache.SetLastModifiedFromFileDependencies(); this.Response.Cache.AppendCacheExtension("max-age=0"); this.Response.Cache.VaryByParams["genre"] = true; this.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate); return View(genreModel); } 

The standard OutputCache attribute must be removed, otherwise it will not respond to your dependencies. If you wish, you can write your ActionFilter for caching, so as not to copy-paste the code.

To reset the cache, call Redis.DeleteKey in the methods that modify the data.

Repeated load test issued 52 scripts per second. This is less than without Redis, but performance will not noticeably fall with an increase in the number of records in the table.

What else can be done with Redis?

In addition to manually placing data in the cache, you can use the NuGet Microsoft.Web.RedisSessionStateProvider and Microsoft.Web.RedisOutputCacheProvider packages to place the session state and page cache on Redis. Unfortunately, the custom OutputCacheProvider restricts the use of CacheDependency to reset the output cache.

Conclusion


In ASP.NET, there are a lot of caching options, besides the posts reviewed in this series there are also cache validation callbacks, linking to files and directories. But there are pitfalls that I haven’t mentioned yet. If you are interested in everything related to optimization of web applications on ASP.NET, then come to my seminar - gandjustas.timepad.ru/event/150915

All posts series


Source code along with tests is available on GitHub - github.com/gandjustas/McvMusicStoreCache

Source: https://habr.com/ru/post/240269/


All Articles