Memcached’s CAS (Check-And-Set) command lets you implement optimistic locking, preventing lost updates when multiple clients try to modify the same item concurrently.
Let’s see it in action. Imagine two clients, A and B, both trying to increment a counter stored in Memcached.
Client A:
# Get the current value and its CAS token
echo "get counter" | nc localhost 11211
# Output: VALUE counter 0 1\r\n1\r\nEND\r\n
# Client A decides to increment it
echo "set counter 0 10 2\r\n2\r\n" | nc localhost 11211
# Output: STORED\r\n
Client B, meanwhile, also fetched the initial value:
# Client B gets the same initial value
echo "get counter" | nc localhost 11211
# Output: VALUE counter 0 1\r\n1\r\nEND\r\
# Client B also decides to increment it
echo "set counter 0 10 2\r\n2\r\n" | nc localhost 11211
# Output: STORED\r\n
If both clients simply set the value, the last one to write wins, and the other client’s update is lost. This is where CAS comes in.
The gets command is the key. Instead of get, we use gets. It returns the value and a unique 64-bit CAS identifier.
Client A (using gets):
# Get the current value and its CAS token
echo "gets counter" | nc localhost 11211
# Output: VALUE counter 0 1 1\r\n1\r\nEND\r\
# The '1' after the flags is the CAS token
Client B (using gets):
# Get the current value and its CAS token
echo "gets counter" | nc localhost 11211
# Output: VALUE counter 0 1 1\r\n1\r\nEND\r\
# Client B also gets CAS token '1'
Now, when a client wants to update an item, it uses the cas command, providing the CAS token it originally received.
Client A modifies its value and attempts to cas:
# Client A increments its value to 2
# It uses the CAS token '1' it received
echo "cas counter 0 10 2 1\r\n2\r\n" | nc localhost 11211
# Output: STORED\r\
# Success!
Now, Client B, which still has the old CAS token (1), tries to cas its modified value (also 2 in this example):
# Client B tries to set its value to 2, but uses the OLD CAS token '1'
echo "cas counter 0 10 2 1\r\n2\r\n" | nc localhost 11211
# Output: NOT_STORED\r\
# Failure! The CAS token did not match.
NOT_STORED means the item was modified since the client fetched it with gets. The server rejected the cas operation because the CAS token provided by Client B (1) no longer matched the current CAS token on the server (which would have been updated by Client A’s successful cas).
This mechanism allows your application to:
- Fetch an item’s value and its CAS token using
gets. - Perform some computation or modification based on that value.
- Attempt to store the new value using
caswith the original CAS token. - If
casreturnsNOT_STORED, it means another client updated the item in the meantime. Your application should then re-fetch the item (usinggetsagain), re-apply its logic to the new value, and try thecasoperation again with the new CAS token.
This retry loop is the core of optimistic locking. It assumes conflicts are rare, and only requires a retry when a conflict actually occurs, which is more performant than always acquiring a lock.
The CAS token is not a version number that increments sequentially. It’s a 64-bit identifier that is guaranteed to be unique for each successful write to a given key. If a key is deleted and then re-added, its CAS token will change. This uniqueness is what makes cas reliable for detecting intervening writes.
The next challenge is managing the retry logic efficiently in your application to avoid livelock or excessive retries.