第 14 章
Day 14:依既有 Key 條件執行 SET
Redis 的 SET 支援依既有資料決定是否更新的選項:
NX:只有 key 不存在時才設定。XX:只有 key 存在時才設定。
# because key "foo" doesn't exist, SET successful.
(redis-cli) > SET foo bar NX
OK
(redis-cli) > GET foo
"bar"
# because key "foo" exists, SET failed.
(redis-cli) > SET foo baz NX
(nil)
(redis-cli) > GET foo
"bar"
IFEQ {val}:只有目前值等於{val}才設定。IFNE {val}:只有目前值不等於{val}才設定。
(redis-cli) > SET foo bar
OK
# SET successful
(redis-cli) > SET foo baz IFEQ bar
OK
# SET failed
(redis-cli) > SET foo bax IFEQ bar
Code Review 與重構
先前只完成部分雛形:
const (
nx = iota + 1 // set value only not exists
xx // set value only exists
ifeq // set only value == cond.Val
ifne // set only value != cond.Val
ifdeq // set only XXH3(value) == cond.Val
ifdne // set only XXH3(value) != cond.Val
)
type parseSetCond struct {
Typ int
Val string
}
type parsedSet struct {
K string
V string
Cond parseSetCond
Get bool
Exp *time.Time
}
var _ repo.SetParam = parsedSet{}
func (parsed parsedSet) Obj() object.Object {
return object.NewString(parsed.K, parsed.V, parsed.Exp)
}
它解析 NX、XX、IFEQ、IFNE 等選項,卻還沒有套用條件;條件定義在 cmd,實際上應交由 repo 使用。
此外,repo.SetParam 原本打算處理 object.String、object.Hash,未來可能還有 object.List、object.OrderedSet,但目前直接以 s.storage[param.Obj().Key()] = param.Obj() 寫入,無法處理各型別的特殊行為。
因此需要重構。
SetStringParam interface
定義組合 SetParam 的 SetStringParam:
const (
CondNX Cond = iota + 1 // set value only not exists
CondXX // set value only exists
CondIFEQ // set only value == cond.Val
CondIFNE // set only value != cond.Val
CondIFDEQ // set only XXH3(value) == cond.Val
CondIFDNE // set only XXH3(value) != cond.Val
)
type SetStringParam interface {
SetParam
CondType() Cond
CondValue() string
ExpiresAt() *time.Time
KeepTTL() bool
GetCurrent() bool
SetCurrent(*object.String)
}
將 cmd.parsedSet 改名為 cmd.setparams,並讓它實作 SetStringParam:
type setparams struct {
key string
val string
// NX|XX|IFEQ|IFNE|IFDEQ|IFDNE
condType repo.Cond
condValue string
// GET
get bool
cur *object.String
// EX|PX|EXAT|PXAT
exp *time.Time
keepTTL bool
}
var _ repo.SetStringParam = &setparams{}
func (p *setparams) Obj() object.Object {
return object.NewString(p.key, p.val, p.exp)
}
func (p *setparams) CondType() repo.Cond {
return p.condType
}
func (p *setparams) CondValue() string {
return p.condValue
}
func (p *setparams) ExpiresAt() *time.Time {
return p.exp
}
func (p *setparams) KeepTTL() bool {
return p.keepTTL
}
func (p *setparams) GetCurrent() bool {
return p.get
}
func (p *setparams) SetCurrent(cur *object.String) {
p.cur = cur
}
也要更新 parse() 回傳的型別。
讓 mapStorage.Set() 處理 SetStringParam
func (s *mapStorage) Set(_ context.Context, param SetParam) error {
if strparam, ok := param.(SetStringParam); ok {
return s.setString(strparam)
}
return errors.New("unimplemented")
}
func (s *mapStorage) setString(param SetStringParam) error {
s.mu.Lock()
defer s.mu.Unlock()
s.storage[param.Obj().Key()] = param.Obj()
return nil
}
setString() 實作
setString() 需要:
- 讀取目前值時刪除已到期的 key。
- 指定
GET且目前值為*object.String時,帶回舊值。 - 寫入前檢查
NX、XX、IFEQ、IFNE等條件。 - 指定
KEEPTTL時,以目前值的到期時間呼叫object.SetExpiresAt()。
func (s *mapStorage) setString(param SetStringParam) error {
s.mu.Lock()
defer s.mu.Unlock()
obj := param.Obj()
cur, exists := s.storage[obj.Key()]
if cur != nil && cur.Expired() {
delete(s.storage, obj.Key())
cur = nil
exists = false
}
if param.GetCurrent() {
if !exists {
param.SetCurrent(nil)
} else {
if curstr, ok := cur.(*object.String); !ok {
return ErrTypeMismatch
} else {
param.SetCurrent(curstr)
}
}
}
if err := s.checkStringCond(param, cur, exists); err != nil {
return fmt.Errorf("%w: %w", ErrCondMismatch, err)
}
if param.KeepTTL() && cur != nil {
obj.SetExpiresAt(cur.ExpiresAt())
}
s.storage[obj.Key()] = obj
return nil
}
func (s *mapStorage) checkStringCond(param SetStringParam, current object.Object, exists bool) error {
switch param.CondType() {
case 0:
return nil
case CondNX:
if exists {
return errors.New("data found")
}
case CondXX:
if !exists {
return errors.New("data not found")
}
case CondIFEQ:
if !exists {
return errors.New("data not found")
}
if str, ok := current.(*object.String); !ok {
return ErrTypeMismatch
} else if !str.Equals(param.CondValue()) {
return errors.New("data mismatch")
}
case CondIFNE:
if !exists {
return errors.New("data not found")
}
if str, ok := current.(*object.String); !ok {
return ErrTypeMismatch
} else if str.Equals(param.CondValue()) {
return errors.New("data match")
}
case CondIFDEQ:
if !exists {
return errors.New("data not found")
}
if str, ok := current.(*object.String); !ok {
return ErrTypeMismatch
} else if !str.EqualsDigest(param.CondValue()) {
return errors.New("data match")
}
case CondIFDNE:
if !exists {
return errors.New("data not found")
}
if str, ok := current.(*object.String); !ok {
return ErrTypeMismatch
} else if str.EqualsDigest(param.CondValue()) {
return errors.New("data match")
}
default:
panic(fmt.Sprintf("unknow condition type: %d", param.CondType()))
}
return nil
}
注意:目前的
setString()是過渡方案。未來可以用 factory method pattern 處理SET、HSET、ZSET等操作。
AOF 與 KEEPTTL
先前已將 EX、PX 改寫成適合 AOF 的指令,但 KEEPTTL 更難處理:解析指令時無法確定 storage 中現有的到期時間。
因此,目前同時使用 KEEPTTL 與 AOF 仍有非預期行為,留待未來解決。
IFDEQ 與 IFDNE
儲存的字串可能很大,IFEQ、IFNE 的比較成本為 ,其中 為字串長度。
Redis 的 IFDEQ、IFDNE 透過 digest 比較,使用 XXH3。
在 *object.String 加入 digest uint64:
type String struct {
*base
val string
digest uint64
}
func (str *String) EqualsDigest(v string) bool {
n, err := strconv.ParseUint(v, 10, 0)
if err != nil {
return false
}
return n == str.digest
}
寫入時產生 digest:
func NewString(k, v string, expiresAt *time.Time) *String {
return &String{
val: v,
digest: xxh3.HashString(v),
base: &base{key: k, expiresAt: expiresAt},
}
}
注意:這裡使用
github.com/zeebo/xxh3計算字串的 XXH3 hash,透過 SIMD 提升速度。