Skip to content

Optimistic locking for string objects - compare-and-set and compare-and-delete#14435

Merged
minchopaskal merged 20 commits intoredis:unstablefrom
minchopaskal:cas-n-cad
Oct 21, 2025
Merged

Optimistic locking for string objects - compare-and-set and compare-and-delete#14435
minchopaskal merged 20 commits intoredis:unstablefrom
minchopaskal:cas-n-cad

Conversation

@minchopaskal
Copy link
Collaborator

@minchopaskal minchopaskal commented Oct 16, 2025

Description

Add optimistic locking for string objects via compare-and-set and compare-and-delete mechanism.

What's changed

Introduction of new DIGEST command for string objects calculated via XXH3 hash.

Extend SET command with new parameters supporting optimistic locking. The new value is set only if checks against a given (old) value or a given string digest pass.

Introduction of new DELEX command to support conditionally deleting a key. Conditions are also checks against string value or string digest.

Motivation

For developers who need to to implement a compare-and-set and compare-and-delete single-key optimistic concurrency control this PR provides single-command based implementation.

Compare-and-set and compare-and-delete are mostly used for Optimistic concurrency control: a client (1) fetches the value, keeps the old value (or its digest, for a large string) in memory, (2) manipulates a local copy of the value, (3) applies the local changes to the server, but only if the server’s value hasn’t been changed (still equal to the old value).

Note that compare-and-set can also be implemented with WATCH … MULTI … EXEC and Lua scripts. The new SET optional arguments and the DELEX command do not enable new functionality, however, they are much simpler and faster to use for the very common use case of single-key optimistic concurrency control.

Related issues and PRs

#12485
#8361
#4258

Description of the new commands

DIGEST

DIGEST key

Get the hash digest of the value stored in key, as an hex string.

Reply:

  • Null if key does not exist
  • error if key exists but holds a value which is not a string
  • (bulk string) the XXH3 digest of the value stored in key, as an hex string

SET

SET key value [NX | XX | IFEQ match-value | IFNE match-value | IFDEQ match-digest | IFDNE match-digest] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

IFEQ match-value - Set the key’s value and expiration only if its current value is equal to match-value. If key doesn’t exist - it won’t be created.
IFNE match-value - Set the key’s value and expiration only if its current value is not equal to match-value. If key doesn’t exist - it will be created.
IFDEQ match-digest - Set the key’s value and expiration only if the digest of its current value is equal to match-digest. If key doesn’t exist - it won’t be created.
IFDNE match-digest - Set the key’s value and expiration only if the digest of its current value is not equal to match-digest. If key doesn’t exist - it will be created.

Reply update:

  • If GET was not specified:
    • Nil reply if either
      • the key doesn’t exist and XX/IFEQ/IFDEQ was specified. The key was not created.
      • the key exists, and NX was specified or a specified IFEQ/IFNE/IFDEQ/IFDNE condition is false. The key was not set.
    • Simple string reply: OK: The key was set.
  • If GET was specified, any of the following:
    • Nil reply: The key didn't exist before this command (whether the key was created or not).
    • Bulk string reply: The previous value of the key (whether the key was set or not).

DELEX

DELEX key [IFEQ match-value | IFNE match-value | IFDEQ match-digest | IFDNE match-digest]

Conditionally removes the specified key. A key is ignored if it does not exist.

IFEQ match-value - Delete the key only if its value is equal to match-value
IFNE match-value - Delete the key only if its value is not equal to match-value
IFDEQ match-digest - Delete the key only if the digest of its value is equal to match-digest
IFDNE match-digest - Delete the key only if the digest of its value is not equal to match-digest

Reply:

  • error if key exists but holds a value that is not a string and IFEQ/IFNE/IFDEQ/IFDNE is specified.
  • (integer) 0 if not deleted (the key does not exist or a specified IFEQ/IFNE/IFDEQ/IFDNE condition is false), or 1 if deleted.

Notes

Added copy of xxhash repo to deps - version

@minchopaskal minchopaskal added state:needs-doc-pr requires a PR to redis-doc repository release-notes indication that this issue needs to be mentioned in the release notes labels Oct 16, 2025
@sggeorgiev
Copy link
Collaborator

Could you please clarify and document in the PR description how the new conditional operations handle key expiration scenarios? Specifically:

  1. Race condition with expiration: What is the expected behavior when a key expires between the initial read (getting the value/digest) and the conditional SET operation?
  2. IFNE with expired keys: How does SET key value IFNE match-value behave when the key has just expired? Since IFNE creates the key if it doesn't exist, does an expired key count as "doesn't exist" for this purpose?
  3. DELEX with expiration: Similarly, for DELEX key IFEQ match-value, what happens if the key expires between checking the condition and performing the deletion?
  4. Digest validation on expired keys: Does IFDEQ/IFDNE treat an expired key as having no digest (operation fails) or as non-existent (follows the create/no-create logic)?

@LiorKogan
Copy link
Member

LiorKogan commented Oct 18, 2025

Re #14435 (comment)

A key could be deleted, renamed, evicted, or expired between the read and the conditional SET. We should not implement any special logic for these use cases. To the best of my knowledge, we don't do that in other places.

For SET, a deleted key is equivalent to not-equal, because, basically, it was changed by another user. An evicted or expired key - well, it is the responsibility of the user to ensure it won't happen.

minchopaskal and others added 2 commits October 20, 2025 10:36
Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
minchopaskal and others added 3 commits October 20, 2025 13:43
Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
@ShooterIT
Copy link
Member

Introduction of new DIGEST command for string objects calculated via XXH3 hash.

why do we use XXH3 hash?

@LiorKogan
Copy link
Member

why do we use XXH3 hash?

It is 64-bit, which, I believe, is a good tradeoff for our use case.

It has good properties. Specifically, speed and quality. See table here.

It is widely adopted.

Copy link
Member

@ShooterIT ShooterIT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job, developers in the community have wanted this feature for too long, thanks

"key_specs": [
{
"flags": [
"RW",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"RW",
"RM",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that's right.
This mutually exclusive flag defines what Redis physically does to the key (the other one is a set of logical operations from the user's perspective).
Considering that, this command is similar to GETDEL. e.g. for ROF, it'll need to load the data to compare it (it doesn't rely on that, but it's an indication this could be wrong).
@guybe7 please correct me if I'm wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYM it's similar to GETDEL? It does not return the old value. Maybe you mean we need the ACCESS flag?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, i mean there's one set of (mutually exclusive) flags that define how redis uses the data in the key, in that set this command is similar to GETDEL since redis does need the data in the key, so it'w RM.

the other set of (non-mutually exclusive) flags, is the user's perspective, and the user doesn't ACCESS the data (note that we've documented that side-channel, like the user being able to observe the content of the key via DELEX doesn't count as ACCESS)

/* The following refer what the command actually does with the value or metadata
 * of the key, and not necessarily the user data or how it affects it.
 * Each key-spec may must have exactly one of these. Any operation that's not
 * distinctly deletion, overwrite or read-only would be marked as RW. */
#define CMD_KEY_RO (1ULL<<0)     /* Read-Only - Reads the value of the key, but
                                  * doesn't necessarily returns it. */
#define CMD_KEY_RW (1ULL<<1)     /* Read-Write - Modifies the data stored in the
                                  * value of the key or its metadata. */
#define CMD_KEY_OW (1ULL<<2)     /* Overwrite - Overwrites the data stored in
                                  * the value of the key. */
#define CMD_KEY_RM (1ULL<<3)     /* Deletes the key. */
/* The following refer to user data inside the value of the key, not the metadata
 * like LRU, type, cardinality. It refers to the logical operation on the user's
 * data (actual input strings / TTL), being used / returned / copied / changed,
 * It doesn't refer to modification or returning of metadata (like type, count,
 * presence of data). Any write that's not INSERT or DELETE, would be an UPDATE.
 * Each key-spec may have one of the writes with or without access, or none: */
#define CMD_KEY_ACCESS (1ULL<<4) /* Returns, copies or uses the user data from
                                  * the value of the key. */
#define CMD_KEY_UPDATE (1ULL<<5) /* Updates data to the value, new value may
                                  * depend on the old value. */
#define CMD_KEY_INSERT (1ULL<<6) /* Adds data to the value with no chance of
                                  * modification or deletion of existing data. */
#define CMD_KEY_DELETE (1ULL<<7) /* Explicitly deletes some content
                                  * from the value of the key. */

Copy link
Collaborator Author

@minchopaskal minchopaskal Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see GETDEL uses RW instead of RM but I'm not sure why exactly. For DELEX the key is deleted, conditionally but still deleted, so not sure why RM is not the correct one for DELEX (and GETDEL for that matter).

Also from the ACCESS docs it feels like it might be correct for DELEX as it uses the data. Or maybe I don't get the terminology correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/* The following refer what the command actually does with the value or metadata
 * of the key, and not necessarily the user data or how it affects it.

this refers to the redis code, does redis access the value?
try thinking of a key that's on flash in ROF, it does need to load the data to ram in order to read it.

Also from the ACCESS docs it feels like it might be correct for DELEX as it uses the data. Or maybe I don't get the terminology correctly.

i think you might have got it reverse, the short ones (RM and RW) are about what redis does with the data, and the long one (ACCESS and DELETE) are the logical operation from the user's perspective

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the clarification.
The redis code does access the data and does modify it (delete).
So RM is specific for delete only cases - which are DEL and UNLINK currently.
I'll open a PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oranagra I'm wondering if we can optimize the comments; otherwise, it's very likely to cause confusion.

#define CMD_KEY_RW (1ULL<<1)     /* Read-Write - Modifies or Deletes the data stored in the
                                  * value of the key or its metadata. */
#define CMD_KEY_RM (1ULL<<3)     /* Deletes the key without reading it. */

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok..
maybe also: Deletes the key without reading it's value

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about

 Read-Write - Reads and modifies/deletes the data stored in the value of the key or its metadata.

minchopaskal and others added 2 commits October 20, 2025 15:40
Co-authored-by: Yuan Wang <wangyuancode@163.com>
Co-authored-by: debing.sun <debing.sun@redis.com>
@sundb
Copy link
Collaborator

sundb commented Oct 20, 2025

@minchopaskal don't forget to generate commands.def again.

Copy link
Collaborator

@sundb sundb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Co-authored-by: debing.sun <debing.sun@redis.com>
@minchopaskal minchopaskal merged commit aed879a into redis:unstable Oct 21, 2025
21 checks passed
@sundb sundb added this to Redis 8.4 Oct 23, 2025
@sundb sundb moved this from Todo to Done in Redis 8.4 Oct 23, 2025
oranagra pushed a commit that referenced this pull request Oct 24, 2025
In #14435 the `RM` flag  was incorrect for the DELEX command as the Redis command code accesses and uses the value of the key, it needs to be RW (just like `GETDEL` command).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-notes indication that this issue needs to be mentioned in the release notes state:needs-doc-pr requires a PR to redis-doc repository

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants