Fetching data from a webservice or a database can take some time. When it concerns static data that is used often in your application, it is wise to store the data in cache. That way you only have the performance hit of fetching the data once. ASP.NET has a HttpContext.Cache object that can be used to store your data. But what is the best way to read and write data in cache? And how do you handle the situation where multiple users of your web application access the same data in cache? I will try to answer these questions with an example.
Say you have a web application that uses a list of countries. The countries are stored in a database and do not change very often so you want to fetch and store the list in cache. You create a property 'Countries' that handles the fetching and caching of the countries. An obvious solution would be:
1: public List<string> Countries
2: {
3: get
4: {
5: // Try getting the countries from cache
6:
7: if (Cache["countries"] == null)
8: {
9: // Not in cache, so get the countries
10: // from the database and store them in cache
11: Cache["countries"] = GetCountriesFromDatabase();
12: }
13:
14: return (List<string>)Cache["countries"];
15: }
16: }
However, this is not a very good solution, because you should never assume objects in cache to be there. The cache can be expired at any time (for example because of a lack of resources). Even right after an object has been stored in cache, it can be removed. If, in the above example, the cache is cleared just before the return statement, the Countries property will incorrectly return null. To solve this, we should use a temporary variable. Like this:
1: public List<string> Countries
2: {
3: get
4: {
5: // Try getting the countries from cache
6: List<string> countries = (List<string>)Cache["countries"];
7: if (countries == null)
8: {
9: // Not in cache, so get the countries
10: // from the database and store them in cache
11: countries = GetCountriesFromDatabase();
12: Cache["countries"] = countries;
13: }
14:
15: return countries;
16: }
17: }
This is better, because now it is no longer possible to return null when the cache is cleared. But this solution is not optimal either. What happens when multiple users are using the application at the same time and the Countries property is called from multiple threads simultaneously. When this happens before the cache has been filled it will result in multiple expensive calls to the database, just to get the same list of countries and store it in cache. This will degrade the overall performance. It would be enough to let just one thread fetch the countries and store them in cache. This can be accomplished by using locks:
1: private readonly object CountriesCacheLock = new object();
2:
3: public List<string> Countries
4: {
5: get
6: {
7: lock (CountriesCacheLock)
8: {
9: // Try getting the countries from cache
10: List<string> countries = (List<string>)Cache["countries"];
11: if (countries == null)
12: {
13: // If not available from cache, apply a lock to fill the
14: // cache in a thread-safe way. This avoids that multiple
15: // threads will simultaneously fetch the same list of
16: // countries from the database
17: countries = GetCountriesFromDatabase();
18: Cache["countries"] = countries;
19: }
20:
21: return countries;
22: }
23: }
24: }
With the lock in place only one thread at a time can access the implementation of the Countries property, so there will be no more simultaneous calls to get the countries from the database. And still this is not an optimal solution, because now multiple threads must wait for each other before they can read the cache which is a loss of performance. Reading a value from cache is a thread-safe operation and could therefore be done by multiple threads at the same time. So the reading of the cache should be moved outside the lock:
1: private readonly object CountriesCacheLock = new object();
2:
3: public List<string> Countries
4: {
5: get
6: {
7: // Try getting the countries from cache
8: List<string> countries = (List<string>)Cache["countries"];
9: if (countries == null)
10: {
11: lock (CountriesCacheLock)
12: {
13: // If not available from cache, apply a lock to fill the
14: // cache in a thread-safe way. This avoids that multiple
15: // threads will simultaneously fetch the same list of
16: // countries from the database
17: countries = GetCountriesFromDatabase();
18: Cache["countries"] = countries;
19: }
20: }
21:
22: return countries;
23: }
24: }
But now we are back to the performance issue of multiple threads accessing the Countries property when the cache has not been filled. In such situation, the first thread will apply the lock en start the time consuming operation of fetching the list of countries. Threads that in the meantime access the Countries property will have to wait for the lock to be released. But when that happens each thread that has been waiting for the lock will still go and fetch the list of countries again. For this to work more efficiently we will have to check again if the cache has been filled within the lock. Only if the cache is still empty we have to do the expensive database call. The final solution now looks like this:
1: private readonly object CountriesCacheLock = new object();
2:
3: public List<string> Countries
4: {
5: get
6: {
7: // Try getting the countries from cache
8: List<string> countries = (List<string>)Cache["countries"];
9: if (countries == null)
10: {
11: // If not available from cache, apply a lock to fill the
12: // cache in a thread-safe way. This avoids that multiple
13: // threads will simultaneously fetch the same list of
14: // countries from the database.
15: lock (CountriesCacheLock)
16: {
17: // Try getting the countries from cache again, because the
18: // cache might have been filled while waiting for the lock
19: countries = (List<string>)Cache["countries"];
20: if (countries == null)
21: {
22: // If still not in cache, get the countries
23: // from the database and store them in cache
24: countries = GetCountriesFromDatabase();
25: Cache["countries"] = countries;
26: }
27: }
28: }
29:
30: return countries;
31: }
32: }
See also:
http://stackoverflow.com/questions/39112/what-is-the-best-way-to-lock-cache-in-aspnet