Strings, Bitmaps & Bitfields
|
This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every example on this page targets Redis Open Source 8.10 (8.10.2), using Jedis 8 for Java examples and Lettuce 7.7 where an async/reactive client is shown. The string is Redis’s most basic data type but also its most feature-dense: a single binary-safe byte sequence that doubles as a JSON blob, a cache entry, a counter, or a raw bit array, depending only on which commands are used against it.
Strings
A Redis string is a binary-safe sequence of bytes, up to 512 MB, so it can hold anything from a short
counter to a serialized JSON document or an image. It has no internal structure of its own; structure is
imposed by the commands used to read and write it (INCR for a counter, SETBIT/BITFIELD for a bit
array, plain GET/SET for a blob).
SET and its flags
SET accepts a mutually exclusive condition group and a mutually exclusive expiration group. The
classic condition flags are:
-
NX— only set the key if it does not already exist. -
XX— only set the key if it already exists.
Redis 8.4 added four further condition flags, IFEQ/IFNE/IFDEQ/IFDNE, which compare against the key’s
current value (or a hash digest of it, computed with XXH3) rather than only its existence:
-
IFEQ value— set only if the current value equalsvalue; if the key doesn’t exist, it is not created. -
IFNE value— set only if the current value is not equal tovalue; if the key doesn’t exist, it is created. -
IFDEQ digest/IFDNE digest— the same comparisons, but against a hash digest (as returned byDIGEST) instead of the full value, useful when the value is large and only equality matters.
The expiration group is EX seconds, PX milliseconds, EXAT unix-time-seconds, PXAT
unix-time-milliseconds, or KEEPTTL (retain whatever TTL the key already had). GET can be combined with
any condition/expiration choice to return the previous value atomically. Prefer this single SET call with
NX/XX/IF* over the older, now-deprecated single-purpose commands (SETNX, SETEX, PSETEX,
GETSET).
package com.example.bookshop;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.params.SetParams;
class CatalogueCache {
private final RedisClient jedis;
CatalogueCache(RedisClient jedis) {
this.jedis = jedis;
}
void cacheBook(String isbn, String bookJson) {
// bookshop:cache:book:<isbn> -- catalogue cache entry, expires after 600 seconds
jedis.set(
"bookshop:cache:book:" + isbn,
bookJson,
new SetParams().ex(600));
}
}
See SET for the full flag reference.
GETEX and GETDEL
GETEX reads a key’s value while also updating (or clearing, with PERSIST) its TTL in the same round
trip — useful for a read that should also refresh a cache entry’s expiration. GETDEL reads a key’s value
and deletes it atomically, replacing the older GET + DEL pair.
> GETEX bookshop:cache:book:9780000000001 EX 600
> GETDEL bookshop:cache:book:9780000000001
MSET, MSETEX and DELEX
MSET key value [key value …] writes several keys unconditionally and atomically, with no shared
expiration and no per-key options. Redis 8.4 added MSETEX, which extends the same bulk write with the
condition and expiration groups SET supports (NX/XX, and EX/PX/EXAT/PXAT/KEEPTTL), applied
uniformly to every key in the call:
> MSETEX 2 bookshop:cache:book:9780000000001 <json1> bookshop:cache:book:9780000000002 <json2> NX EX 600
Also new in 8.4 is DELEX, a conditional delete that mirrors SET’s comparison flags: `DELEX key [IFEQ
value | IFNE value | IFDEQ digest | IFDNE digest]. Called with no flags it behaves like a plain DEL for
a single key; with a flag, the key is only removed if its current value (or digest) matches the condition — useful for releasing a lock or invalidating a cache entry only if it still holds the value the caller last
wrote.
Counters: INCR, INCRBY, INCRBYFLOAT and INCREX
INCR/INCRBY increment an integer-typed string by 1 or by a given 64-bit signed amount; INCRBYFLOAT
increments a float-typed (or integer-typed) string by a floating-point amount. All three create the key
with an initial value of 0 if it doesn’t exist, and are atomic even under concurrent callers.
Redis 8.8 added INCREX, which folds an atomic increment, an optional lower/upper bound (with WRAP-less
SATURATE capping instead of failing), and an expiration update into a single command — replacing the
INCR + EXPIRE pair that previously needed a Lua script to be atomic. ENX sets the TTL only if the key
currently has none, so a fixed window isn’t silently extended by every increment inside it. This is exactly
the primitive the bookshop’s rate limiter needs; see
Use Cases & Patterns for the full bookshop:ratelimit:<userId> pattern built
on INCREX BYINT 1 UBOUND 100 EX 60 ENX.
References: INCR, INCRBY, INCRBYFLOAT, INCREX.
String encodings
Redis picks a string’s internal encoding automatically, visible via OBJECT ENCODING key:
| Encoding | When used |
|---|---|
|
The value is an integer that fits a 64-bit signed range — stored without a separate string allocation. |
|
A short string, up to the hardcoded |
|
Any longer string — a normal, separately allocated |
This is purely an internal space optimization; every string command behaves identically regardless of encoding. Reference: OBJECT ENCODING.
Bitmaps
A bitmap is not a separate Redis type — it is a regular string addressed bit by bit. SETBIT key offset
value sets a single bit (0 or 1) at a zero-based offset, growing the string as needed; GETBIT key offset
reads one back. BITCOUNT counts set bits (optionally over a byte or bit range), and BITPOS finds the
first bit set to a given value.
package com.example.bookshop;
import redis.clients.jedis.RedisClient;
class DailyActiveUsers {
private final RedisClient jedis;
DailyActiveUsers(RedisClient jedis) {
this.jedis = jedis;
}
void recordActive(String day, long userId) {
// bookshop:dau:<day> -- one bit per userId, set when that user is seen on that day
jedis.setbit("bookshop:dau:" + day, userId, true);
}
long countActive(String day) {
return jedis.bitcount("bookshop:dau:" + day);
}
}
BITOP
BITOP combines multiple bitmaps into a destination key. The original operators are AND, OR, XOR
(all N-ary) and NOT (unary). Redis 8.2 added four more, which take a distinguished first key X against
one or more others:
-
DIFF destkey X Y1 [Y2 …]— a bit is set if it is set inXbut in none ofY1, Y2, …. -
DIFF1 destkey X Y1 [Y2 …]— the inverse: set if it is set in one or more ofY1, Y2, …but not inX. -
ANDOR destkey X Y1 [Y2 …]— set if it is set inXand in at least one ofY1, Y2, …. -
ONE destkey X1 [X2 …]— set if it is set in exactly one ofX1, X2, ….
ANDOR is a direct fit for a "readers of A who are also interested in at least one of B, C, D" segment
against the daily-active-users bitmaps above — e.g. combining a "loves fiction" bitmap with per-genre
interest bitmaps to target a promotion. Strings shorter than the longest input (including a missing key) are
treated as zero-padded.
Reference: BITOP.
BITFIELD
BITFIELD treats a string as a packed array of arbitrary-width signed (i<bits>) or unsigned (u<bits>)
integers, addressed either by a raw bit offset or, prefixed with #, by a field-width-relative index. A
single call can chain GET/SET/INCRBY operations, each returning its own reply, with OVERFLOW
WRAP|SAT|FAIL controlling how the following SET/INCRBY operations behave on overflow:
-
WRAP(default) — wraps around (modulo arithmetic for unsigned, sign-flip for signed). -
SAT— saturates at the encoding’s minimum/maximum value. -
FAIL— performs no write and returnsnilfor that operation.
package com.example.bookshop;
import java.util.List;
import redis.clients.jedis.RedisClient;
class ShelfSlotCounters {
private final RedisClient jedis;
ShelfSlotCounters(RedisClient jedis) {
this.jedis = jedis;
}
long incrementSlotCount(String shelfKey, int slot) {
// An 8-bit unsigned counter per shelf slot, saturating at 255 instead of wrapping to 0.
List<Long> result = jedis.bitfield(
shelfKey,
"OVERFLOW", "SAT",
"INCRBY", "u8", "#" + slot, "1");
return result.get(result.size() - 1);
}
}
Reference: BITFIELD.
BITFIELD lays consecutive fixed-width fields (here, one u8 counter per shelf slot) across the same
underlying byte string that SETBIT/GETBIT and BITCOUNT/BITPOS also address bit by bit — #0,
#1, #2, … select the 0th, 1st, 2nd 8-bit field (bit offsets 0, 8, 16, …) rather than raw bit
offsets passed directly.
Related pages
-
Getting Started — installing Redis and connecting with Jedis/Lettuce, before running any of the commands above.
-
Use Cases & Patterns — the catalogue cache, cart, and rate-limiter patterns built on the
SET, hash, andINCREXcommands introduced here.