實時資料併發寫入 Redis 優化

haifeiWu發表於2019-11-12

實時資料併發寫入 Redis 優化-原文連結

對於併發請求如何在不強制加鎖的情況下快速更新呢?這裡有熱騰騰的線上實踐……

背景

當前架構的邏輯是將併發請求資料寫入佇列中,然後起一個單獨的非同步執行緒對資料進行序列處理。這種方式的好處就是不用考慮併發的問題,當然其弊端也是顯而易見的~

樂觀鎖實現資料的併發更新

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

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 的過程:對 keymd5sha1hash 演算法,根據 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)。

小結

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

關注我們

關注我們

相關文章