Skip to content

Commit 0012ca7

Browse files
authored
Add DELIFEQ command (#1975)
This feature introduces a new command: `DELIFEQ key value`. The command deletes the given key _only if_ its current value is equal to the provided `value`. It returns: - 1 if the key was removed (i.e., it existed and matched the value), - 0 otherwise (key did not exist, or the value did not match). This command complements `SET key value IFEQ match-value`, added in #1324. **The problem/use-case that the feature addresses** A very common operation when synchronizing distributed locks is to remove a key, but only if the value matches a specific string. This pattern is frequently used in distributed locking algorithms like Redlock. The conditional delete prevents a client from inadvertently releasing a lock it doesn’t own—e.g., when a timeout or retry causes overlapping or stale attempts. Today, this logic is typically implemented using a small Lua script like: ```lua if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end ``` However, using scripts adds a layer of complexity and overhead. Having this logic as a built-in Redis command improves simplicity, reliability, and performance. It also complements `SET key IFEQ value` very nicely. Example: ```redis SET foo test DELIFEQ foo test # returns 1, key is deleted SET foo nope DELIFEQ foo test # returns 0, key remains ``` Signed-off-by: Linus Unnebäck <linus@folkdatorn.se>
1 parent c551108 commit 0012ca7

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

src/commands.def

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10459,6 +10459,31 @@ struct COMMAND_ARG DECRBY_Args[] = {
1045910459
{MAKE_ARG("decrement",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
1046010460
};
1046110461

10462+
/********** DELIFEQ ********************/
10463+
10464+
#ifndef SKIP_CMD_HISTORY_TABLE
10465+
/* DELIFEQ history */
10466+
#define DELIFEQ_History NULL
10467+
#endif
10468+
10469+
#ifndef SKIP_CMD_TIPS_TABLE
10470+
/* DELIFEQ tips */
10471+
#define DELIFEQ_Tips NULL
10472+
#endif
10473+
10474+
#ifndef SKIP_CMD_KEY_SPECS_TABLE
10475+
/* DELIFEQ key specs */
10476+
keySpec DELIFEQ_Keyspecs[1] = {
10477+
{NULL,CMD_KEY_RM|CMD_KEY_ACCESS|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
10478+
};
10479+
#endif
10480+
10481+
/* DELIFEQ argument table */
10482+
struct COMMAND_ARG DELIFEQ_Args[] = {
10483+
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
10484+
{MAKE_ARG("value",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
10485+
};
10486+
1046210487
/********** GET ********************/
1046310488

1046410489
#ifndef SKIP_CMD_HISTORY_TABLE
@@ -11316,6 +11341,7 @@ struct COMMAND_STRUCT serverCommandTable[] = {
1131611341
{MAKE_CMD("append","Appends a string to the value of a key. Creates the key if it doesn't exist.","O(1). The amortized time complexity is O(1) assuming the appended value is small and the already present value is of any size, since the dynamic string library used by the server will double the free space available on every reallocation.","2.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,APPEND_History,0,APPEND_Tips,0,appendCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,APPEND_Keyspecs,1,NULL,2),.args=APPEND_Args},
1131711342
{MAKE_CMD("decr","Decrements the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECR_History,0,DECR_Tips,0,decrCommand,2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECR_Keyspecs,1,NULL,1),.args=DECR_Args},
1131811343
{MAKE_CMD("decrby","Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECRBY_History,0,DECRBY_Tips,0,decrbyCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECRBY_Keyspecs,1,NULL,2),.args=DECRBY_Args},
11344+
{MAKE_CMD("delifeq","Delete key if value matches string.","O(1)","9.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DELIFEQ_History,0,DELIFEQ_Tips,0,delifeqCommand,3,CMD_WRITE,ACL_CATEGORY_STRING,DELIFEQ_Keyspecs,1,NULL,2),.args=DELIFEQ_Args},
1131911345
{MAKE_CMD("get","Returns the string value of a key.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GET_History,0,GET_Tips,0,getCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,GET_Keyspecs,1,NULL,1),.args=GET_Args},
1132011346
{MAKE_CMD("getdel","Returns the string value of a key after deleting the key.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETDEL_History,0,GETDEL_Tips,0,getdelCommand,2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETDEL_Keyspecs,1,NULL,1),.args=GETDEL_Args},
1132111347
{MAKE_CMD("getex","Returns the string value of a key after setting its expiration time.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETEX_History,0,GETEX_Tips,0,getexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETEX_Keyspecs,1,NULL,2),.args=GETEX_Args},

src/commands/delifeq.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"DELIFEQ": {
3+
"summary": "Delete key if value matches string.",
4+
"complexity": "O(1)",
5+
"group": "string",
6+
"since": "9.0.0",
7+
"arity": 3,
8+
"function": "delifeqCommand",
9+
"command_flags": [
10+
"WRITE"
11+
],
12+
"acl_categories": [
13+
"STRING"
14+
],
15+
"key_specs": [
16+
{
17+
"flags": [
18+
"RM",
19+
"ACCESS",
20+
"DELETE"
21+
],
22+
"begin_search": {
23+
"index": {
24+
"pos": 1
25+
}
26+
},
27+
"find_keys": {
28+
"range": {
29+
"lastkey": 0,
30+
"step": 1,
31+
"limit": 0
32+
}
33+
}
34+
}
35+
],
36+
"reply_schema": {
37+
"oneOf": [
38+
{
39+
"description": "The key was not deleted.",
40+
"const": 0
41+
},
42+
{
43+
"description": "The key was deleted.",
44+
"const": 1
45+
}
46+
]
47+
},
48+
"arguments": [
49+
{
50+
"name": "key",
51+
"type": "key",
52+
"key_spec_index": 0
53+
},
54+
{
55+
"name": "value",
56+
"type": "string"
57+
}
58+
]
59+
}
60+
}

src/server.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3628,6 +3628,7 @@ void setCommand(client *c);
36283628
void setnxCommand(client *c);
36293629
void setexCommand(client *c);
36303630
void psetexCommand(client *c);
3631+
void delifeqCommand(client *c);
36313632
void getCommand(client *c);
36323633
void getexCommand(client *c);
36333634
void getdelCommand(client *c);

src/t_string.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,32 @@ void psetexCommand(client *c) {
384384
setGenericCommand(c, OBJ_PX | OBJ_ARGV3, c->argv[1], c->argv[3], c->argv[2], UNIT_MILLISECONDS, NULL, NULL, NULL);
385385
}
386386

387+
void delifeqCommand(client *c) {
388+
robj *existing_value = lookupKeyWrite(c->db, c->argv[1]);
389+
390+
if (existing_value == NULL) {
391+
addReplyLongLong(c, 0);
392+
return;
393+
}
394+
395+
if (checkType(c, existing_value, OBJ_STRING)) {
396+
/* Error reply already sent */
397+
return;
398+
}
399+
400+
if (compareStringObjects(existing_value, c->argv[2]) != 0) {
401+
addReplyLongLong(c, 0);
402+
return;
403+
}
404+
405+
if (!dbSyncDelete(c->db, c->argv[1])) {
406+
addReplyLongLong(c, 0);
407+
return;
408+
}
409+
410+
addReplyLongLong(c, 1);
411+
}
412+
387413
int getGenericCommand(client *c) {
388414
robj *o;
389415

tests/unit/type/string.tcl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,27 @@ if {[string match {*jemalloc*} [s mem_allocator]]} {
756756
lappend res [r get bar]
757757
} {12 12}
758758

759+
test {DELIFEQ non-existing key} {
760+
r del foo
761+
assert_equal 0 [r delifeq foo "test"]
762+
}
763+
764+
test {DELIFEQ existing key, matching value} {
765+
r set foo "test"
766+
assert_equal 1 [r delifeq foo "test"]
767+
}
768+
769+
test {DELIFEQ existing key, non-matching value} {
770+
r set foo "nope"
771+
assert_equal 0 [r delifeq foo "test"]
772+
}
773+
774+
test {DELIFEQ existing key, non-string value} {
775+
r del foo
776+
r sadd foo "test"
777+
assert_error "WRONGTYPE*" {r delifeq foo "test"}
778+
}
779+
759780
if {[string match {*jemalloc*} [s mem_allocator]]} {
760781
test {Memory usage of embedded string value} {
761782
# Check that we can fit 9 bytes of key + value into a 32 byte

0 commit comments

Comments
 (0)