Chapter 03 · Strings, Counters, Bitmaps, Bitfields, and Binary Values
SET/GET Options, NX/XX, GET, EX/PX, Atomic Initialization, and Conditional Writes
Use Redis strings deliberately: prove SET condition and return-value semantics, preserve or replace TTLs intentionally, and build one-command conditional initialization without hidden races.
Learning outcomes
AtlasMart stores checkout idempotency markers, short-lived
sessions, feature flags, and small serialized state in Redis
strings. A developer currently writes a value with
SET, then issues EXPIRE in a second
command. Another overwrites a session token and assumes its old
time to live survives. A third uses a read-then-write sequence
to initialize a key only if it is absent. Each approach can look
correct in a single manual test while hiding race conditions or
lifecycle changes. This lesson replaces those assumptions with
observable String and
SET semantics.
Explain Redis Strings as binary-safe byte sequences rather than a text-only type, and relate GET/SET replies to Redis Serialization Protocol (RESP) values.
Use SET with NX and XX conditions, GET old-value return semantics, and EX/PX expiration options while distinguishing successful writes from rejected conditions.
Prove when SET removes an existing TTL, when KEEPTTL preserves it, and why a failed condition must not be treated as a successful initialization.
Build atomic one-command initialization with a bounded lifetime instead of a vulnerable check-then-set or SET-then-EXPIRE sequence.
Connect conditional writes to client retries, persistence, replication/failover limits, Cluster slot locality, ACL permissions, and observability.
All Chapter 03 labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from
Docker Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default user disabled,
named ACL users atlasmart-app and
academy-admin, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data volume, and no explicit Redis
maxmemory limit/eviction policy. The application
ACL is restricted to ~atlasmart:* and normal
read/write/connection categories. Search, JSON, vector,
time-series, and probabilistic features are not required. This
chapter deliberately uses one standalone node so command
semantics are isolated from replication/failover. Later
chapters re-test the same patterns under replication,
Sentinel, and Cluster.
1. A Redis String is a binary-safe value, not a text promise
Redis calls its simplest value type a String,
but the server stores a length-delimited sequence of bytes.
Those bytes may encode UTF-8 text, JSON, a compressed object, an
integer-looking token, a bitmap, or arbitrary binary data.
GET returns the stored bytes as a bulk-string
reply. Your client decides whether to expose those bytes as
text, a byte array, or another host-language representation.
That distinction matters immediately. The bytes
31 30 happen to be the ASCII characters
10; numeric commands can parse them as an integer.
The bytes 00 ff 41 are equally valid as a Redis
String, but decoding them as UTF-8 would be an application
error. Lesson 3 performs an exact byte-for-byte round trip.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:flag:checkout enableddocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:flag:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app STRLEN atlasmart:ch03:flag:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:flag:checkout 0 2# Expected shape:# OK# "enabled"# (integer) 7# "ena"
STRLEN counts bytes, not Unicode characters.
GETRANGE addresses byte offsets and includes both
endpoints. Those details are easy to miss when all examples
contain plain ASCII.
2. SET replaces the value; conditions decide whether replacement happens
SET key value creates the key when absent and
replaces the current value when present, regardless of the
previous Redis data type. NX changes the condition
to “only if the key does not exist.” XX changes it
to “only if the key already exists.” These condition options are
mutually exclusive. A successful ordinary SET replies
OK; a condition that rejects the write produces a
null reply rather than modifying the key.
| Form | Condition | Main use | Important boundary |
|---|---|---|---|
SET k v |
Always | Create or replace | Existing TTL is discarded unless an expiration option/KEEPTTL says otherwise. |
SET k v NX |
Key must be absent | Atomic initialization / claim-if-absent | A null reply means no write occurred; caller must branch explicitly. |
SET k v XX |
Key must exist | Update-if-present | Does not prove the caller read the same version earlier. |
SET k v GET |
Normal SET condition | Return previous String while writing new value | If the existing value is not a String, Redis errors and aborts the SET. |
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:set:demodocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:set:demo v1 NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:set:demo v2 NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:set:demodocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:set:demo v2 XX GETdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:set:demo# Expected shape:# first NX -> OK# second NX -> (nil), value remains v1# XX GET -> old value "v1", while the stored value becomes v2
The GET option makes the read and replacement part
of one server command; it does not turn XX into
compare-and-swap. Redis 8.10.1 also documents additional
value/digest conditional SET modes. They are version-sensitive
extensions, so this curriculum keeps its main mental model on
NX/XX and asks you to re-check current
command documentation before relying on newer conditions.
3. EX, PX, and KEEPTTL are lifecycle semantics, not decoration
EX attaches a relative expiration in seconds;
PX uses milliseconds. A
time to live (TTL) is the remaining interval
before Redis considers the key expired. Chapter 02 showed that
expiration is not an exact task scheduler. Here the crucial rule
is replacement: plain SET discards an existing TTL.
KEEPTTL explicitly retains it. An EX/PX option
replaces the lifetime with a new deadline.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v1 EX 90docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:session:ttl# WRONG ASSUMPTION: plain SET preserves the old lifetime.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:session:ttl# -> -1: key exists and is now persistent.# Attach a new lifetime, then preserve it on replacement.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v3 PX 90000docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app PTTL atlasmart:ch03:session:ttldocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v4 XX GET KEEPTTLdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app PTTL atlasmart:ch03:session:ttl# -> positive and decreasing; exact milliseconds depend on observation time.
Do not assert that the two PTTL readings differ by exactly the
time spent typing. Client/network scheduling and expiration
processing make precise wall-clock output variable. The evidence
you need is state class: the plain replacement changed TTL to
-1; the KEEPTTL replacement left a positive
expiration.
4. Atomic initialization means one command contains the condition and lifetime
Redis executes one command without interleaving another command
in the middle of that command. That is the useful scope of
command atomicity. If AtlasMart wants to create
a short-lived “checkout request seen” marker only when absent,
SET ... NX EX ... keeps the absence check, write,
and lifetime attachment in one command.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:idempotency:req-9001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:idempotency:req-9001 processing NX EX 120docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:idempotency:req-9001 processing-again NX EX 120docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:idempotency:req-9001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:idempotency:req-9001# Exactly one initialization succeeds on this single primary.# A retry that sees (nil) must NOT be reported as a fresh successful claim.
This is stronger than EXISTS followed by
SET, because another client cannot win between the
test and write. It is also stronger than
SET NX followed by a separate EXPIRE,
because a client crash between those commands cannot leave the
newly created marker persistent. It is not, by itself, a
complete distributed lock or exactly-once workflow; later
chapters address ownership tokens, leases, idempotency,
failover, and reconciliation.
5. Deliberately wrong workflow: a session refresh silently becomes immortal
Assume AtlasMart sessions are required to expire. A handler
receives a refreshed token and runs plain SET. The
value is correct, so a functional test passes, but the old
expiration disappears. This is a data-retention and capacity
bug, not merely a syntax issue.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s1 EX 45docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:wrong:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:wrong:session# -> -1 proves the lifecycle changed.# Repair option A: preserve the existing deadline.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s3 XX KEEPTTL# The TTL was already lost, so KEEPTTL now preserves "persistent".# Repair option B: explicitly restore policy with a new bounded TTL.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s4 XX EX 45docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:wrong:session# -> positive <= 45
The first attempted repair is intentionally instructive:
KEEPTTL cannot resurrect an expiration that a
previous command already removed. Correctness depends on when
the policy is applied, not merely on whether the final command
contains the right option.
6. What Redis is doing—and what this does not guarantee
The server parses the RESP command, checks ACL permission and
key pattern, evaluates the SET condition against current key
state, validates the current type if GET needs the
old value, then applies the replacement and expiration policy as
one command. The command is fast for ordinary small values, but
payload size still affects memory allocation, network transfer,
persistence/AOF traffic, replication traffic, and tail latency.
On this standalone lab, a successful reply proves the primary
executed the command. With AOF everysec, it does
not prove that bytes are already fsynced to durable storage.
Under asynchronous replication later, it also would not prove
every replica has received the write. Client timeouts create
ambiguity: the server may have executed a command even if the
reply was lost. Conditional writes therefore need retry
semantics designed at the application level.
7. Hands-on lab: define a safe AtlasMart initialization contract
Create three keys: a persistent feature flag, an expiring
checkout marker, and a session whose TTL must survive a value
refresh. Keep all state under atlasmart:ch03:l1:.
Record the before/after values and TTL classes so the test can
detect lifecycle regressions.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:l1:flag atlasmart:ch03:l1:checkout atlasmart:ch03:l1:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:flag off NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:checkout accepted NX EX 180docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:session token-a EX 180docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:session token-b XX GET KEEPTTLdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l1:flagdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l1:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:l1:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l1:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:l1:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin MEMORY USAGE atlasmart:ch03:l1:session# Cleanup only this lesson:docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:l1:flag atlasmart:ch03:l1:checkout atlasmart:ch03:l1:session
Verification checklist:
- The checkout key is created only on the first NX attempt and has a positive bounded TTL.
- The session replacement returns the previous String when GET is used and the key still has a positive TTL after KEEPTTL.
- You can explain why plain SET would remove the session TTL and why KEEPTTL cannot restore a deadline already lost.
- You can distinguish command atomicity on the current primary from durability, replication acknowledgement, and exactly-once application semantics.
- You recorded the Redis 8.10.1 / standalone / DB 0 / ACL / AOF-everysec assumptions beside the result.
8. Production judgment
Use a String when the value is naturally one binary blob or
scalar and the operations you need match String commands. Use
NX/XX when the existence predicate is
actually the invariant; do not substitute them for version
checks or authorization. Pair creation with EX/PX in the same
SET when an absent TTL would be harmful. Use KEEPTTL only when
retaining the existing deadline is the explicit contract.
In production, observe request/value sizes, memory growth, command latency distributions, key cardinality, expiration state, errors/null conditional replies, retry counts, AOF/replication pressure, and hot-key concentration. Cluster deployments add same-slot constraints for multi-key logic, while a single SET addresses one key and routes naturally through a cluster-aware client. ACLs control which users may issue the command and which key patterns they can touch; key prefixes alone are not security. For rollback, preserve compatibility with old readers/writers and define what happens if a new deployment starts using KEEPTTL or different TTL values.
9. Summary and next step
Redis Strings are binary-safe values. SET creates or replaces, NX/XX gate that replacement, GET can return the old String as part of the same command, and EX/PX/KEEPTTL define the lifecycle of a successful write. Plain SET removes an existing TTL. One-command conditional initialization removes check/write and write/expire races, but it does not create exactly-once distributed processing. Next, you will use the same String representation as an atomic integer or floating-point counter and confront overflow, precision, retry, and sequencing boundaries.
Check your understanding
- Why can a plain SET turn a correctly expiring session into a retention bug?
- What does NX make atomic?
- Why is XX not compare-and-swap?
- What does SET ... GET add?
- Why can a timeout after SET be ambiguous?
Review the answers
1. Because replacing a key with SET discards its existing TTL unless a new expiration or KEEPTTL is specified.
2. The existence test and successful value/expiration write are evaluated inside one SET command on the server.
3. XX only requires that the key exists; it does not compare the current value with a version previously read by the client.
4. It returns the previous String value while applying the new value in the same command; a non-String old value causes an error.
5. The server may have executed the command even if the client did not receive the reply, so retries must be designed with application semantics.
Authoritative references
- SET command — current SET conditions, GET, EX/PX/EXAT/PXAT, and KEEPTTL semantics
- TTL command — TTL state and return values
- Redis keyspace / expiration — key expiration model
- Redis Strings — String data type and current commands
- Redis 8.10 release notes — 8.10.1 security baseline and 8.10 changes