對於併發請求如何在不強制加鎖的情況下快速更新呢?這裡有熱騰騰的線上實踐……
背景
當前架構的邏輯是將併發請求資料寫入佇列中,然後起一個單獨的非同步執行緒對資料進行序列處理。這種方式的好處就是不用考慮併發的問題,當然其弊端也是顯而易見的~
樂觀鎖實現資料的併發更新
根據當前業務的資料更新在秒級,key
的碰撞率較低的情況。筆者打算採用使用 CAS
樂觀鎖方案:使用 Lua
指令碼實現 Redis
對資料的原子更新,即便是在併發的情況下其效能也會上一個級別。下面是 CAS 樂觀鎖實現資料併發更新的流程圖:
根據上面的流程圖設計出了 Lua
指令碼:
local keys,values=KEYS,ARGV
local version = redis.call('get',keys[1])
if values[1] == '' and version == false
then
redis.call('SET',keys[1],'1')
redis.call('SET',keys[2],values[2])
return 1
end
if version == values[1]
then
redis.call('SET',keys[2],values[2])
redis.call('INCR',keys[1])
return 1
else
return 0
end
複製程式碼
可能存在問題及其解決方案
1,在併發衝突概率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值後失敗退出。如:
func main() {
for i := 0; i < 10; i++ {
isRetry := execLuaScript()
if !isRetry {
break
}
}
}
func execLuaScript() bool {
ctx := context.Background()
r := client.GetRedisKVClient(ctx)
defer r.Close()
luaScript := `
local keys,values=KEYS,ARGV
local version = redis.call('get',keys[1])
if values[1] == '' and version == false
then
redis.call('SET',keys[1],'1')
redis.call('SET',keys[2],values[2])
return 1
end
if version == values[1]
then
redis.call('SET',keys[2],values[2])
redis.call('INCR',keys[1])
return 1
else
return 0
end`
casVersion, err := r.Get("test_version")
kvs := make([]redis.KeyAndValue, 0)
kvs = append(kvs, redis.KeyAndValue{"test_version", casVersion.String()})
kvs = append(kvs, redis.KeyAndValue{"test", "123123123"})
mv, err := r.Eval(luaScript, kvs...)
if err != nil {
log.Errorf("%v", err)
}
val, _ := mv.Int64()
log.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)。
小結
對於上面的優化過程,目前程式碼重構開發工作已經完成,但是還未正式上線,等上線之後再來補一下優化之後效能的提升情況~