Time-based TTLs with lazy delete
Go's time.Time encodes the unit. time.Now().Add(5 * time.Second) is unambiguous in a way Unix seconds are not. The sidecar approach from Python translates directly.
type Store struct {
mu sync.RWMutex
m map[string]string
expires map[string]time.Time
}
func (s *Store) checkExpiredLocked(key string) bool {
ts, ok := s.expires[key]
if !ok { return false }
if time.Now().After(ts) {
delete(s.m, key); delete(s.expires, key)
return true
}
return false
}Store now has expires map[string]time.Time. checkExpiredLocked runs inside every read.
Because lazy delete is a write. checkExpiredLocked can mutate the maps if the key is expired. RLock would race against this. The cost: GETs no longer fan out concurrently for keys with TTLs. Production Redis solves this with sharded stores or atomic timestamp reads. The exercises walk through both.
func parseSetOptions(args []string) (key, value string, ttl time.Duration, hasTTL bool, errMsg string) {
if len(args) < 2 { return "", "", 0, false, "ERR wrong arguments" }
key, value = args[0], args[1]
i := 2
for i < len(args) {
opt := strings.ToUpper(args[i])
if opt == "EX" && i+1 < len(args) {
n, _ := strconv.Atoi(args[i+1])
ttl = time.Duration(n) * time.Second; hasTTL = true; i += 2
} else if opt == "PX" && i+1 < len(args) {
n, _ := strconv.Atoi(args[i+1])
ttl = time.Duration(n) * time.Millisecond; hasTTL = true; i += 2
} else { return "", "", 0, false, "ERR option" }
}
return
}Parsing SET ... EX N. Returns a Duration in seconds, applies via SetWithTTL.
func (s *Store) Del(keys ...string) int {
s.mu.Lock()
defer s.mu.Unlock()
n := 0
for _, k := range keys {
s.checkExpiredLocked(k)
if _, ok := s.m[k]; ok {
delete(s.m, k); delete(s.expires, k); n++
}
}
return n
}
// ... in the dispatch table:
"DEL": func(args []string) []byte {
if len(args) == 0 { return encodeError("ERR wrong number of arguments for 'del'") }
return encodeInteger(store.Del(args...))
},
"EXPIRE": func(args []string) []byte {
if len(args) != 2 { return encodeError("ERR wrong number of arguments for 'expire'") }
secs, err := strconv.Atoi(args[1])
if err != nil { return encodeError("ERR value is not an integer") }
if store.Expire(args[0], time.Duration(secs)*time.Second) { return encodeInteger(1) }
return encodeInteger(0)
},DEL and EXPIRE complete the promised command set. Del is variadic and reports how many keys actually existed. EXPIRE attaches a TTL to an existing key, answering 1 on success and 0 if the key is missing.
func (s *Store) TTL(key string) int {
s.mu.Lock()
defer s.mu.Unlock()
if s.checkExpiredLocked(key) { return -2 }
if _, ok := s.m[key]; !ok { return -2 }
ts, ok := s.expires[key]
if !ok { return -1 }
d := time.Until(ts)
if d < 0 { return 0 }
return int(d.Seconds())
}TTL follows the real Redis return convention: remaining whole seconds if a timer is set, -1 if the key exists with no TTL, -2 if the key is missing or just lazily expired.
Quiz: Quiz
Loading practiceโฆ