I assume the key-value storage is too large to just iterate over all k-v-pairs to find out which can be expired. I also assume that each read access refreshes the expiry timestamp, so only items which haven't been accessed for some time are expired.
The challenge is to efficiently find all records which can be expired (whenever cleanup is due), but also efficiently refresh the expiry timestamp on every read access (so we must find the key in the structure used for expiration).
My proposal: group expiry_timestamps into buckets; for example, if items
live for 8 hours, make one bucket per hour. Those buckets are kept in a linked list; when expiration happens, the first bucket is emptied and the list reduced. The number of buckets is lifespan/cleanup interval.
Each bucket contains a hashSet of all keys that should be expired. Iteration over all keys in a hashset is efficient enough.
During read access, the program checks which bucket the key currently is in and which bucket it now belongs to. In most cases, it's the same bucket, so no further action is necessary. Otherwise, remove the key from the old bucket (removing from a hash set is efficient) and insert it into the new bucket.
+--------------+ +--------------+ +--------------+
-->+ Expiry 08:00 +-->+ Expiry 09:00 +-->+ Expiry 10:00 +
| KeySet | | KeySet | | KeySet |
+--------------+ +--------------+ +--------------+