實時數據併發寫入 Redis 優化

  1. <strong>
  2. <strong>
  3. <strong>

01 背景

當前架構的邏輯是將併發請求數據寫入隊列中,然後起一個單獨的異步線程對數據進行串行處理。這種方式的好處就是不用考慮併發的問題,當然其弊端也是顯而易見的~

實時數據併發寫入 Redis 優化

02 樂觀鎖實現數據的併發更新

根據當前業務的數據更新在秒級,key 的碰撞率較低的情況。筆者打算採用使用 CAS 樂觀鎖方案:使用 Lua 腳本實現 Redis 對數據的原子更新,即便是在併發的情況下其性能也會上一個級別。下面是 CAS 樂觀鎖實現數據併發更新的流程圖:

實時數據併發寫入 Redis 優化

根據上面的流程圖設計出了 Lua 腳本:

local keys,values=KEYS,ARGV
local version = redis.call('get',keys[1])
if values[1] == '' and version == false
then
\tredis.call('SET',keys[1],'1')
\tredis.call('SET',keys[2],values[2])
\treturn 1
end

if version == values[1]
then
\tredis.call('SET',keys[2],values[2])
\tredis.call('INCR',keys[1])
\treturn 1
else
\treturn 0
end

03 可能存在問題及其解決方案

1,在併發衝突概率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值後失敗退出。如:

func main() { for i := 0; i < 10; i++ {
isRetry := execLuaScript()
if !isRetry {
break
}
}
}

func execLuaScript() bool {
ctx := context.Background()
\tr := client.GetRedisKVClient(ctx)
\tdefer r.Close()


\tluaScript := `
local keys,values=KEYS,ARGV
local version = redis.call('get',keys[1])
if values[1] == '' and version == false
then
\tredis.call('SET',keys[1],'1')
\tredis.call('SET',keys[2],values[2])
\treturn 1
end

if version == values[1]
then
\tredis.call('SET',keys[2],values[2])
\tredis.call('INCR',keys[1])
\treturn 1
else
\treturn 0
end`

\tcasVersion, err := r.Get("test_version")

\tkvs := make([]redis.KeyAndValue, 0)
\tkvs = append(kvs, redis.KeyAndValue{"test_version", casVersion.String()})
\tkvs = append(kvs, redis.KeyAndValue{"test", "123123123"})
\tmv, err := r.Eval(luaScript, kvs...)

\tif err != nil {
\t\tlog.Errorf("%v", err)
\t}

\tval, _ := mv.Int64()
\tlog.Debugf(">>>>>> lua 腳本運行結果 :%d", val)
if val == 1 {
// lua 腳本執行成功,無需重試
return false
} else if val == 0 {
return true
}
}

2,Lua 腳本執行時只能在同一臺機器上生效,因此在 Redis 集群在就要求相關聯的 key 分配到相同機器。這裡很多同學可能會問為什麼,其實很簡單,Redis 是單線程的,倘若 Lua 腳本操作的 key 在不同機器上執行,也就無法保證其執行的原子性了。

解決方法還是從分片技術的原理上找: 數據分片,就是一個 hash 的過程:對 key 做 md5,sha1 等 hash 算法,根據 hash 值分配到不同的機器上。

為了實現將key分到相同機器,就需要相同的 hash 值,即相同的 key(改變 hash 算法也行,但比較複雜)。但 key 相同是不現實的,因為 key 都有不同的用途。但是我們讓 key 的一部分相同對我們業務實現來說是可以實現的。那麼能不能拿 key 一部分來計算 hash 呢?答案是肯定的,

這就是 Hash Tag 。允許用key的部分字符串來計算hash。當一個key包含 {} 的時候,就不對整個key做hash,而僅對 {} 包括的字符串做 hash。假設 hash 算法為sha1。對 user:{user1}:ids和user:{user1}:tweets ,其 hash 值都等同於 sha1(user1)。

04 小結

對於上面的優化過程,目前代碼重構開發工作已經完成,但是還未正式上線,等上線之後再來補一下優化之後性能的提升情況~


作者:haifeiWu
鏈接:https://juejin.im/post/5dca1ec46fb9a04ab25be4ad


分享到:


相關文章: