tanujtyagi.com

Understanding Multi-Key Commands in Redis: Pros, Cons, and Best Practices

MGET is fine until Cluster Mode returns CROSSSLOT. Hash tags, pipelining and data modelling are the real fixes, and Lua is not one of them.

MGET is the sort of command you use for a year without thinking about it. One call, five keys, five values back. Then you turn on Cluster Mode and get this:

CROSSSLOT Keys in request don't hash to the same slot

That error is not a bug and it is not a limitation you can configure away. It is Redis telling you something true about your data model that you had not noticed yet.

The commands in question

Multi-key commands touch more than one key in a single call. The common ones:

TypeCommandWhat it does
StringsMGET key1 key2 key3Fetches several values at once
StringsMSET key1 value1 key2 value2Sets several values at once
SetsSINTER key1 key2Intersection of sets
SetsSUNION key1 key2Union of sets
Sorted setsZINTER 2 key1 key2Intersection of sorted sets
Sorted setsZUNIONSTORE dest 2 key1 key2Union, written to a destination key
KeysDEL key1 key2Deletes several keys
KeysEXISTS key1 key2Checks several keys

On a single instance these are close to free, and there is no reason not to use them. One round trip instead of five, atomic execution because Redis is single-threaded, and less code in your client. That is the entire upside and it is a real one.

Why Cluster Mode changes the answer

A Redis Cluster splits the keyspace into 16,384 hash slots and assigns ranges of slots to different shards. Which slot a key lands in is CRC16(key) mod 16384, so user:1:profile and user:1:settings almost certainly live on different shards.

A command has to execute on one node. Redis will not fan a single MGET out across shards, gather the pieces and reassemble them for you, because doing so would mean giving up the atomicity that made the command worth using. So it refuses, and you get CROSSSLOT.

The important thing to notice is that this is not about command syntax. It is about where your keys physically are. Any fix that does not move keys is not a fix.

Hash tags, which move the keys

If part of a key name is wrapped in braces, Redis hashes only that part:

{user:123}:profile
{user:123}:settings

Both keys now hash on user:123, so both land in the same slot, on the same shard, and MGET works again. This is the primary tool and most people should reach for it first.

The cost is that you have made a permanent decision about co-location. Everything tagged {user:123} lives together, which is exactly what you want until one user turns out to be a thousand times larger than the others and you have a hot shard that you cannot split. Tag on something with reasonable cardinality and reasonably even distribution. Tagging on tenant id works well when you have thousands of similar tenants and badly when one tenant is half your traffic.

Lua does not get you out of this

There is a piece of advice that circulates, which is that if MGET fails on CROSSSLOT you should wrap the logic in a Lua script instead. It sounds plausible. It is wrong, and worth spelling out because acting on it wastes an afternoon.

A script runs on one node, and Redis routes it using the keys you declare in KEYS. If those declared keys do not hash to the same slot, EVAL fails with the identical error:

CROSSSLOT Keys in request don't hash to the same slot

What Lua actually buys you is atomicity for multi-step logic on keys that already share a slot. Read a value, branch on it, write a different key, with nothing interleaving:

-- Atomic read-modify-write across keys in the same slot
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
    redis.call('SET', KEYS[2], ARGV[2])
    return 1
end
return 0
EVAL "<script>" 2 {user:123}:token {user:123}:session old new

Note the hash tags in that call. Lua sits on top of co-location; it does not replace it. And declare every key the script touches in KEYS, because accessing undeclared keys is unsupported in cluster mode and will eventually route somewhere you did not intend.

Redesigning so the question stops coming up

Often the multi-key command is a symptom of having split one thing into several keys for no strong reason. If you always read name and age together, they are one object:

user:123:name -> "John"
user:123:age  -> 30

becomes

user:123 -> {"name": "John", "age": 30}

as a Hash or a JSON document. Now it is a single-key read, HGETALL or JSON.GET, and slots never enter the conversation. This is usually the best available answer, and it is the one people skip because it means touching the application rather than the query.

Pipelining, for when you just want the round trips back

If what you actually wanted from MGET was fewer network hops rather than atomicity, pipelining gives you that and works across shards, because it is many independent single-key commands sent together:

Pipeline pipeline = jedis.pipelined();
pipeline.get("key1");
pipeline.get("key2");
pipeline.sync();

Most cluster-aware clients will split a pipeline by shard and run the parts in parallel. You lose atomicity, which for a read-heavy path you probably never needed.

Doing the work in the application

When keys genuinely have to live on different shards and you still need a set operation across them, fetch them individually and compute in your own code. Instead of:

SINTER key1 key2

read both sets and intersect them in the application. It is more code and more data over the wire, and sometimes it is simply the correct answer. Be honest about the volume though: intersecting two million-element sets in your service layer is a different proposition from intersecting two sets of fifty.

A note on cost

SUNION, SDIFF and ZUNIONSTORE over large sets are O(N) work on a single-threaded server, which means everything else waits. This is true on a standalone instance as well, where no error message warns you. Watch the latency on these before they become a production incident, and prefer the ...STORE variants with a short TTL if you find yourself recomputing the same union repeatedly.

Where this leaves you

Multi-key commands are good and you should use them. Just treat CROSSSLOT as information rather than an obstacle: it is telling you which keys your application considers related, and asking you to make that relationship explicit in the key names or in the data model.

Hash tags and better data structures are the real answers. Pipelining handles the cases where you wanted throughput rather than atomicity, and application-side computation handles what is left. Lua is a fine tool once your keys are already co-located, and no help at all before that.

If you have hit an interesting version of this, I would like to hear about it.