The MemCachier Add-on

This article was contributed by The MemCachier Add-on

MemCachier manages and scales clusters of memcache servers so you can focus on your app. Tell us how much memory you need and get started for free instantly. Add capacity later as you need it.

follow @MemCachier on twitter

Getting the Most Out of Memcache

Last Updated: 07 October 2013

memcache memcached performance

Table of Contents

Despite being the go-to scaling solution for most production websites, Memcache often isn’t used to its full potential. Most developers only know about the get, set, and delete operations. However, Memcache has a broader set of operations that help developers build more advanced apps with less code and even further improved performance.

This article will outline the more useful advanced Memcache operations with a series of real-world use-cases and show their impact on app implementation and performance.

Prerequisites

This article assumes you have the following:

Test environment

Provision the following app on Heroku to create a sandbox environment complete with the MemCachier add-on and a simple command line, allowing you to run advanced Memcache operations yourself. Entering the commands directly instead of just reading over their description will reinforce their syntax and increase your familiarity with them.

Use this one-click Memcache environment setup to follow along and try out actual Memcache operations on Heroku.

You now have a copy of the test app deployed to your Heroku account. Take note of the URL of your new app. It resembles something like http://serene-mesa-2821.herokuapp.com/ where serene-mesa-2821 is the name of your application on Heroku. You will need this name to establish an interactive shell.

Interactive shell

Use heroku run console from your terminal with your app name to connect to an instance of your app running on Heroku. Load the Memcache client libraries and perform basic set and get commands to confirm setup.

$ heroku run console -a app-name
irb> cache = Dalli::Client.new
irb> cache.set("foo", "bar")
=> true
irb> cache.get("foo")
=> "bar"

This example uses an interactive Ruby shell with the Dalli client to load and interact with Memcache. This is only for demonstration purposes as any language's Memcache driver will support similar commands.

The rest of this guide assumes you have this shell running and client libraries loaded.

Cache expiration

The biggest challenge when using Memcache is avoiding cache staleness while still writing clean code. Most developers store data to Memcache and delete or update data when it changes. This strategy can get messy very quickly – Memcache code becomes riddled throughout an application. Rails’ Sweepers can help with this problem, but other languages and frameworks don’t have similar alternatives.

One simple strategy to avoid code complexity is to write data to Memcache with an expiration. Data with an expiration will automatically expire when the expiration is reached. Most applications can benefit from time-based cache expiration with infrequently changing content such as static assets, headers, footers, blog posts, etc.

In the sandbox shell, run the following commands to set a value that will expire after 10 seconds.

Setting the expiration to “0” means the value will never expire.

irb> cache.set("expires", "bar", ttl=10.seconds)
irb> cache.get("expires")
=> "bar"
.. wait 10 seconds ..
irb> cache.get("expires")
=> nil

You can see that no action is required to explicitly expire the given content. After the ttl value has passed any gets for the key simply return a nil result.

When the expiration time specified is 30 days or more in seconds, Memcache treats the expiration as an absolute date by converting the amount of seconds specified to a Unix epoch date. Be careful, because specifying 40 days in seconds will set the expiration to a time in 1970, which will yield unknown results.

Cache clearing

Developers often change their caching strategies several times as they write new cache code. Rapidly changing cache strategies create a dirty cache and make debugging difficult. Whenever a cache strategy is changed in development, a flush command should be issued to Memcache to clear the cache of all values.

In the sandbox console, run the following commands to experiment with flush:

irb> cache.set("foo", "bar")
irb> cache.get("foo")
=> "bar"
irb> cache.flush
irb> cache.get("foo")
=> nil

flush can also be used when deploying to production. But be careful – the application may not be able to withstand a cache flush. Apps with large caches and heavy traffic are advised to not issue a flush in production or to do so only when traffic is low enough to be handled without caching.

The MemCachier Heroku add-on has a web dashboard that can issue a flush command for you. Access the MemCachier dashboard by clicking on the add-on from your Heroku dashboard or by running `heroku addons:open memcachier` from the CLI.

Lightweight counters

A lightweight counter stored in Memcache can be useful for tracking how often a certain event happens in your app without degrading your app’s performance. Counters can be used for debugging, profiling, and usage tracking.

For example, an app that depends on a 3rd party API may want to know how often the 3rd party API is unavailable or returns bad data. A Memcache counter is an ideal solution because page load time will hardly be impacted and the database won’t see additional load.

In the sandbox console, run the following commands to experiment with incr (increment) and decr (decrement):

irb> cache.incr("my_counter", 1, nil, 0)
irb> cache.get("my_counter")
=> "0"
irb> cache.incr("my_counter")
irb> cache.get("my_counter")
=> "1"
irb> cache.incr("my_counter", amt=5)
irb> cache.get("my_counter")
=> "6"
irb> cache.decr("my_counter")
irb> cache.get("my_counter")
=> "5"

incr and decr should be used in favor of manual changes with get and set because incr and decr are thread safe and will require fewer TCP round trips. For an explaination of incr’s arguments, see the Dalli documentation.

List management

A simple list stored in Memcache can be useful for maintaining denormalized relationships. For example, an e-commerce website may want to store a small table of recent purchases. Rather than keeping a serialized list in Memcache and recalculating it when a new purchase is made, append and prepend can be used to store denormalized data, avoiding a database query.

Instead of using a traditional set operation to update a customer’s list of recent purchases:

cache.set("user_1_recent_purchases", Purchases.recent)

prepend can be used with only the new data to the same effect:

cache.prepend("user_1_recent_purchases", product.name + "||")

This approach creates a smaller Memcache footprint and avoids a database query to get all the user’s recent purchases.

In the sandbox console, run the following commands to experiment with append and prepend:

ttl=0 means the key won't expire and :raw => true specifies the value is stored as raw bytes, which is required to use append and prepend.

irb> cache.set("my_list", "foo", ttl=0, options={:raw => true})
irb> cache.get("my_list")
=> "foo"
irb> cache.prepend("my_list", "bar||")
irb> cache.get("my_list")
=> "bar||foo"
irb> cache.append("my_list", "||baz")
irb> cache.get("my_list")
=> "bar||foo||baz"

This example uses an arbitrary delimiter (||). A delimiter should be chosen based on the expected value of each item in the list such that the list items will never contain the delimiter string. Additionally, a fix-lengthed string implementation may be better for certain applications. Read more about Memcache lists here.

append and prepend should be used in favor of manual changes with get and set because append and prepend are thread safe.

Memcache only supports a max value size of 1 MB. Be careful creating lists that may grow larger in size than the maximum allowed value size. Some clients, including Dalli, support compression. In Dalli, set the :compress option to true when connecting to Dalli.

Thread safe set

A simple JSON hash stored in Memcache can be useful for maintaining configuration that is accessed frequently. For example, a website may want to track which features are currently turned on, or which AB tests are running. Often these configurations are conveniently stored together in a JSON hash.

append and prepend aren’t relevant for a JSON hash, because hashes are unordered. And set is dangerous because two concurrent changes to the JSON hash may cause one change to be lost.

Compare and swap, with the cas operator, compares the original value with the new value and swaps the values only if the old value hasn’t been changed by another writer. In other words, cas is a thread-safe set.

In the sandbox console, run the following commands to experiment with cas:

cache.cas in this example expects a block, which is required by Dalli, the chosen Ruby Memcache client. Other clients may not share this approach.

irb> cache.set("my_json", "{}")
irb> cache.get("my_json")
=> "{}"
irb> cache.cas("my_json") { {key: "val"}.to_json }
irb> cache.get("my_json")
=> "{\"key\":\"val\"}"

The Memcache protocol doesn't directly implement a cas operation. Instead, it supports set with a version number, where the set only takes place if the version number matches the version of the key stored Memcache. Using versions as opposed to implementing cas directly in the protocol addresses the classic ABA problem.

Some clients may require cas to be implemented manually by passing a version to set.

Reference

Each Memcache operation discussed here is listed for quick reference below. The API isn’t documented in detail because each client and language will have a different API.

Operation Description
Set with expiration Sets a key and value along with an expiration in seconds. Once the key has expired it will be removed from the cache. An expiration of up to 30 days is interpreted as time interval from the current time, whereas an expiration of 30 days or more is interpreted as an absolute Unix date.
Flush Removes all data from the cache. Be careful using this command in production -- production apps may experience downtime if an empty cache puts too much stress on the database.
Increment Increments an integer value by a specified amount. Thread safe.
Decrement Decrements an integer value by a specified amount. Thread safe.
Append Appends to the end of a value. The original value must be stored as raw bytes in certain clients. Thread safe.
Prepend Prepends to the start of a value. The original value must be stored as raw bytes in certain clients. Thread safe.
CAS (or set with a version) Sets a new value as long as the value hasn’t been changed by another process. Thread safe.