Using Memcache: An In-Depth Look at Example Code

Last week we answered the question, “Why should I use memcache?” Now we’ll get a little deeper into the usage of memcache by showing some example code, along with detailed explanations. This post will give you an idea about what it takes to write memcache code to help your app scale. We’ll use pseudocode to demonstrate.

Step 1: Connect to Memcache

Connecting to memcache is simple – all that’s involved is a set of servers, a username, and a password (although if you’re running your own memcache you won’t need a username and password). The details of connecting to memcache are language- and framework-specific. Each language has its own set of memcache clients, and each client has a different configuration interface. We won’t cover each language, framework, and client in this post. However, our Heroku docs contain detailed instructions for connecting to memcache in Ruby, Rails, Django, PHP, and Java.

The reason that you use a list of servers when connecting and not a single server when connecting is that standard memcache puts some of the intelligence of the design into the client. So the distributed cache component of memcache is actually handled by clients, not servers. (As a quick side note, MemCachier works differently than this, giving you a single big-ass cache in the sky so you only need one server to connect to and never need to change your configuration to add more cache space). We previously covered some of the computer science behind all of this in a blog post on consistent hashing.

Step 2: Test the Connection

Before you start writing application code, make sure your connection to memcache is working. You can do this in a console (for example, irb in Ruby) or in a test application. Start by establishing a connection and trying a get and set operation. Something like this (again, we’ll show you pseudocode here):

cache = Cache.connect(SERVERS, USERNAME, PASSWORD)
cache.set("foo", "bar")
print cache.get("foo")
 > "bar"

In the above example, we’re establishing a connection, setting the “foo” key to the value “bar”, and printing the results of a get with the “foo” key.

Step 3: Write Application Caching Code

Once your client is configured properly and you’ve verified that you can get and set, now you should start writing some caching code. Start with a small, isolated part of your code to familiarize yourself with memcache. For example, if you’re building a friend-based app, perhaps a good starting point is with a list of friends and a user profile. Below we’ll demonstrate each of these scenarios: one with a database query, the other with a rendered view.

Database Queries

The below code shows how you can use memcache to cache the results of a database query:

...
user = current_logged_in_user
friend_list = cache.get("friends_for_user_" + user.id)
if (friend_list == null) {
  friend_list = Friend.for_user(user.id) # 'SELECT * FROM friends WHERE user_id = <user.id>'
  cache.set("friends_for_user_" + user.id, friend_list)
}
...

The code starts by checking the cache to see if the user’s friends are already in the cache. We’ve chosen a descriptive key name "friends_for_user_<user_id>" to store the friend list. You can name your keys whatever you want, but descriptive names are ideal.

Next, if the friend list is cached, nothing else needs to happen. However, if the friend list is not cached, we’ll need to fetch the list from the database and store the results in the cache. The friend list is stored in the cache so the next time this code runs, the database query won’t need to be executed.

Rendered Views

In addition to caching database queries, you probably want to start caching rendered views, too. The below code shows how you can use memcache to cache the results of a rendered view:

...
user = current_logged_in_user
profile_view = cache.get("profile_for_user_" + user.id)
if (profile_view == null) {
  profile_view = render("profile", user)
  cache.set("profile_for_user_" + user.id, profile_view)
}
http_respond(profile_view)
...

The code starts by checking the cache to see if the user’s profile view is already in the cache. Again, we choose a descriptive key name.

Next, if the profile view is cached, nothing else needs to happen except that the view is sent with the HTTP response. However, if the profile view is not cached, we’ll need to render the view and store the rendered view in the cache. The rendered view is stored in the cache so the next time this code runs, the view won’t need to be rendered again.

Note that frameworks such as Django and Rails have their own helper functions for caching rendered views. You should read your framework’s caching documentation to learn how to utilize these helper methods.

Step 4: Expirations and Deletions

The code we’ve seen so far for caching database queries and rendered views is all well and good until data changes. In the above examples, our cache will be out of date if a user adds a new friend or changes their profile information. There are two strategies for avoiding an out-of-date cache: expirations and deletions. Each of these will be covered in detail below:

Expirations

Keys, at the time they’re set, are given an expiration time – or a time when the key will be automatically removed from the cache. Most clients allow for a global expiration to be set, along with a mechanism to set the expiration on a key-by-key basis. Again, when a key expires it is automatically removed from the cache.

Expiration times can be strategically set according to the type of data being stored. For example, if you cache algebra equations and their results, you can set an infinite expiration time (0) because the result will never change (2+2 always equals 4). As for a user’s profile or list of friends, you could set an expiration of a few minutes – the logic being that these keys won’t change more frequently than a few minutes. However, relying on an expiration time in this case will result in a bad user experience. You don’t want a user adding a friend, then having to wait a few minutes for that friend to show up in their friend list. This is where deletions come in.

Deletions

Deletions, at least in the web and mobile space, are the most common way to keep your cache up-to-date. A deletion strategy is one where a particular key is deleted whenever its value is known to be out of date. In our friend and profile example, we would delete the profile_for_user_X key when a user updated their profile, and the friends_for_user_X key when the user added or removed a friend.

Deletion is the best way to keep a cache up to date. However, the code can get ugly very quickly. Without careful consideration it’s easy to complicate most of your logic with deletion calls whenever you know a key needs to be deleted. For example, we could put the call to delete the friend list in the friendship controller:

...
user = current_logged_in_user
Friend.create(user.id, new_friend_id)
cache.delete("friends_for_user_" + user.id)
cache.delete("friends_for_user_" + new_friend_id)
...

The above example will properly delete out-of-date values from the cache. But now our controller code is polluted with cache calls. A better strategy for deleting keys is to put the deletion code into the model itself so that it is triggered when the model changes. This allows you to centralize the logic for cache invalidation, avoiding duplication of code and complication of controllers.

Rails, for example, has excellent support for this style with its ActiveRecord::Callbacks methods. Other frameworks and languages have great support for such model triggers, too. To go along with our pseudocode example, an improvement on the above deletion code would look something like the below – instead of logic going in the controller, it goes in the model.

class Friend {
  after_create: delete_cache()
  after_delete: delete_cache()

  ...

  function delete_cache() {
    cache.delete("friends_for_user_" + this.user_one_id)
    cache.delete("friends_for_user_" + this.user_two_id)
  }
}

Notice how the above cache deletion logic is conveniently tucked away in our model instead of our controller. This greatly simplifies our controller code and makes our app far easier to maintain.

There’s still one more improvement we can make to this code. Instead of just deleting each key when we know its value is out of date, we could set the value of each key to the new, up-to-date value. Something like this:

class Friend {
  after_create: refresh_cache()
  after_delete: refresh_cache()

  ...

  function refresh_cache() {
    cache.set("friends_for_user_" + this.user_one_id, Friend.for_user(this.user_one_id))
    cache.set("friends_for_user_" + this.user_two_id, Friend.for_user(this.user_two_id))
  }
}

With the above code, our cache will be up-to-date, our controller code won’t be ugly, and our cache will be warm immediately, at least for the two new friends.

Certain frameworks such as Rails have Sweepers, which are classes that observe changes to a model and expire the cache as needed. Sweepers are yet another improved way to write maintainable code, but not all frameworks have them.

Step 5: Be Strategic About What to Cache

As you can tell from the above examples, writing cache code can be messy. Your cache is also generally far smaller than your database. For this reason, you should be strategic about what you cache. Not all database queries and rendered views will be expensive or frequently accessed, so pick the right ones. You should profile your app, either by examining the logs or by using a profiling tool like New Relic, to understand where your application spends the majority of its time. Chances are good that only a few complex queries and views will be slowing down your app, and you should start caching these expensive operations first.

What else?

This post has only scratched the surface for getting started with memcache. In later posts we’ll cover more advanced topics such as multiget and advanced profiling. But for now, we hope you’re in a better position to get started writing some code. If you have any questions or issues, please let us know via a comment to this post.

Code Examples

We have a number of real code examples, showing both how to connect to memcache and get/set keys. Take a look: