Hash Field Expiration (#13303)
## Background
This PR introduces support for field-level expiration in Redis hashes. Previously, Redis supported expiration only at the key level, but this enhancement allows setting expiration times for individual fields within a hash.
## New commands
* HEXPIRE
* HEXPIREAT
* HEXPIRETIME
* HPERSIST
* HPEXPIRE
* HPEXPIREAT
* HPEXPIRETIME
* HPTTL
* HTTL
## Short example
from @moticless
```sh
127.0.0.1:6379> hset myhash f1 v1 f2 v2 f3 v3
(integer) 3
127.0.0.1:6379> hpexpire myhash 10000 NX fields 2 f2 f3
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> hpttl myhash fields 3 f1 f2 f3
1) (integer) -1
2) (integer) 9997
3) (integer) 9997
127.0.0.1:6379> hgetall myhash
1) "f3"
2) "v3"
3) "f2"
4) "v2"
5) "f1"
6) "v1"
... after 10 seconds ...
127.0.0.1:6379> hgetall myhash
1) "f1"
2) "v1"
127.0.0.1:6379>
```
## Expiration strategy
1. Integrate active
Redis periodically performs active expiration and deletion of hash keys that contain expired fields, with a maximum attempt limit.
3. Lazy expiration
When a client touches fields within a hash, Redis checks if the fields are expired. If a field is expired, it will be deleted. However, we do not delete expired fields during a traversal, we implicitly skip over them.
## RDB changes
Add two new rdb type s`RDB_TYPE_HASH_METADATA` and `RDB_TYPE_HASH_LISTPACK_EX`.
## Notification
1. Add `hpersist` notification for `HPERSIST` command.
5. Add `hexpire` notification for `HEXPIRE`, `HEXPIREAT`, `HPEXPIRE` and `HPEXPIREAT` commands.
## Internal
1. Add new data structure `ebuckets`, which is used to store TTL and keys, enabling quick retrieval of keys based on TTL.
2. Add new data structure `mstr` like sds, which is used to store a string with TTL.
This work was done by @moticless, @tezc, @ronen-kalish, @sundb, I just release it.
This commit is contained in:
+12
-16
@@ -333,17 +333,7 @@ void freeZsetObject(robj *o) {
|
||||
}
|
||||
|
||||
void freeHashObject(robj *o) {
|
||||
switch (o->encoding) {
|
||||
case OBJ_ENCODING_HT:
|
||||
dictRelease((dict*) o->ptr);
|
||||
break;
|
||||
case OBJ_ENCODING_LISTPACK:
|
||||
lpFree(o->ptr);
|
||||
break;
|
||||
default:
|
||||
serverPanic("Unknown hash encoding type");
|
||||
break;
|
||||
}
|
||||
hashTypeFree(o);
|
||||
}
|
||||
|
||||
void freeModuleObject(robj *o) {
|
||||
@@ -502,6 +492,9 @@ void dismissHashObject(robj *o, size_t size_hint) {
|
||||
dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1])*sizeof(dictEntry*));
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
dismissMemory(o->ptr, lpBytes((unsigned char*)o->ptr));
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
listpackEx *lpt = o->ptr;
|
||||
dismissMemory(lpt->lp, lpBytes((unsigned char*)lpt->lp));
|
||||
} else {
|
||||
serverPanic("Unknown hash encoding type");
|
||||
}
|
||||
@@ -939,6 +932,7 @@ char *strEncoding(int encoding) {
|
||||
case OBJ_ENCODING_HT: return "hashtable";
|
||||
case OBJ_ENCODING_QUICKLIST: return "quicklist";
|
||||
case OBJ_ENCODING_LISTPACK: return "listpack";
|
||||
case OBJ_ENCODING_LISTPACK_EX: return "listpackex";
|
||||
case OBJ_ENCODING_INTSET: return "intset";
|
||||
case OBJ_ENCODING_SKIPLIST: return "skiplist";
|
||||
case OBJ_ENCODING_EMBSTR: return "embstr";
|
||||
@@ -979,7 +973,6 @@ size_t streamRadixTreeMemoryUsage(rax *rax) {
|
||||
* are checked and averaged to estimate the total size. */
|
||||
#define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. */
|
||||
size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
|
||||
sds ele, ele2;
|
||||
dict *d;
|
||||
dictIterator *di;
|
||||
struct dictEntry *de;
|
||||
@@ -1016,7 +1009,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
|
||||
di = dictGetIterator(d);
|
||||
asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d));
|
||||
while((de = dictNext(di)) != NULL && samples < sample_size) {
|
||||
ele = dictGetKey(de);
|
||||
sds ele = dictGetKey(de);
|
||||
elesize += dictEntryMemUsage() + sdsZmallocSize(ele);
|
||||
samples++;
|
||||
}
|
||||
@@ -1052,14 +1045,17 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
asize = sizeof(*o)+zmalloc_size(o->ptr);
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
listpackEx *lpt = o->ptr;
|
||||
asize = sizeof(*o) + zmalloc_size(lpt) + zmalloc_size(lpt->lp);
|
||||
} else if (o->encoding == OBJ_ENCODING_HT) {
|
||||
d = o->ptr;
|
||||
di = dictGetIterator(d);
|
||||
asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d));
|
||||
while((de = dictNext(di)) != NULL && samples < sample_size) {
|
||||
ele = dictGetKey(de);
|
||||
ele2 = dictGetVal(de);
|
||||
elesize += sdsZmallocSize(ele) + sdsZmallocSize(ele2);
|
||||
hfield ele = dictGetKey(de);
|
||||
sds ele2 = dictGetVal(de);
|
||||
elesize += hfieldZmallocSize(ele) + sdsZmallocSize(ele2);
|
||||
elesize += dictEntryMemUsage();
|
||||
samples++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user