Redis緩存數(shù)據(jù)一致性解決方案分析
文章簡介
Redis作為一個非關(guān)系型數(shù)據(jù)庫,已經(jīng)被應(yīng)用在各種高性能的業(yè)務(wù)場景。Redis是一個基于內(nèi)存性質(zhì)的數(shù)據(jù)庫,因此在讀寫上面都是有著非常不錯的性能,在實際的使用過程中,大多數(shù)也是用在一些業(yè)務(wù)數(shù)據(jù)緩存的情況。
設(shè)計到緩存的情況,我們就不得不考慮一個情況,就是緩存數(shù)據(jù)的一致性。如何理解緩存的一致性呢?舉一個簡單的例子,在一個電商系統(tǒng)應(yīng)用中,我們將商品的庫存數(shù)量存在緩存中,此時我們在后臺更新了商品的庫存數(shù)量,如何保證緩存中的庫存信息同步更新并且不會出現(xiàn)庫存數(shù)量問題?文章后面在代碼演示,也以該案例作為演示。
緩存設(shè)計
了解緩存設(shè)計之前,我們先看看下面的一張圖。這張圖也是很多緩存系統(tǒng)的一個設(shè)計模式。
客戶端向服務(wù)端發(fā)送請求。直接去緩存中查詢數(shù)據(jù)。
如果緩存中存在數(shù)據(jù),則直接返回給客戶端緩存中的數(shù)據(jù)。
如果緩存中不存在數(shù)據(jù),則查詢數(shù)據(jù)庫。
根據(jù)MySQL中查詢的數(shù)據(jù),寫入緩存并返回給客戶端。
文章主旨
文章前面提到的數(shù)據(jù)一致性,指的是MySQL與緩存中數(shù)據(jù)如何保持同步。后面文章也是針對如何去實現(xiàn)數(shù)據(jù)同步進行分析。
更新策略
先緩存后數(shù)據(jù)庫

策略說明
后端發(fā)生更新請求,更新對應(yīng)的Redis緩存。在這個過程中可以直接刪除,再新寫入;也可以采用更新的方式。使用刪除相對更為便捷。
如果緩存更新失敗,直接返回客戶端錯誤信息。
如果緩存更新成功,則執(zhí)行更新MySQL操作。
如果MySQL更新失敗,則回滾整個更新,包括緩存中的更新操作。
問題分析
如果在第1中采用的刪除緩存,當(dāng)?shù)?中更新緩存失敗,此時需要手動的去追加緩存,否則會出現(xiàn)緩存擊穿情況,這種情況是非常嚴(yán)重的。
在第4中,更新MySQL失敗的情況下,會回滾緩存中的數(shù)據(jù)。如果在更新MySQL操作過程中,客戶端發(fā)生了新的請求,此時客戶端讀取到的是新數(shù)據(jù),然而實際MySQL更新是失敗的,不可能讓用戶讀取到新數(shù)據(jù),這樣數(shù)據(jù)也會發(fā)生不一致。
代碼演示
// Redis連接對象
$redis = null;
// MySQL連接對象
$mysql = null;
// 客戶端請求參數(shù)
$requestParams = [];
// 刪除緩存
$updateRedis = $redis->del('key');
if ($updateRedis) {
// 更新MySQL
$updateMysql = $mysql->update('update xxx set a=xx where id=xxx');
if ($updateMysql) {
return '數(shù)據(jù)更新失敗';
}
// 回滾緩存(由于緩存刪除失敗,此時就不需要手動回滾。如果是執(zhí)行的更新Redis,還需要手動回滾Redis)
$redis->set('key', $requestParams);
}
return '緩存更新失敗';
先數(shù)據(jù)庫后緩存

策略說明
客戶端發(fā)起更新請求,先更新MySQL。
MySQL更新成功之后,接著更新緩存。更新緩存可以直接使用刪除操作,也可以指定更新。
如果Redis更新失敗則返回客戶端信息。
問題分析
該策略能夠很明顯的看出,在更新MySQL階段是沒問題的。MySQL失敗直接返回客戶端更新失敗,也不需要去操作緩存。
但是當(dāng)更新緩存時,如果緩存更新失敗,但是MySQL中的數(shù)據(jù)是更新成功了。這樣就面臨這一個問題,到底是回滾還是不做任何操作呢?
如果第2中,操作緩存失敗,不做任何處理則緩存永遠(yuǎn)是舊數(shù)據(jù),除非緩存的有效期到了。
代碼演示
// Redis連接對象
$redis = null;
// MySQL連接對象
$mysql = null;
// 客戶端請求參數(shù)
$requestParams = [];
// 更新MySQL
$updateMysql = $mysql->update('update xxx set a=xx where id=xxx');
if ($updateMysql) {
// 更新緩存
$updateRedis = $redis->set($requestParams);
if ($updateRedis) {
return '數(shù)據(jù)更新成功';
}
return '緩存更新失敗';
}
return '數(shù)據(jù)更新失敗';
多線程同步

策略說明
客戶端發(fā)起請求,此時創(chuàng)建兩個線程。
一個線程執(zhí)行MySQL更新,一個線程執(zhí)行緩存更新。
如果兩個線程有一個不成功,則回滾整個更新操作。
問題分析
該策略通過多個線程更新數(shù)據(jù),減少阻塞問題,加快程序處理速度。
如果MySQL線程更新速度失敗并且處理的速度很慢,Redis更新成功處理速度快。此時做回滾,在更細(xì)過程中,新請求從緩存中得到的是新數(shù)據(jù),回滾之后緩存的數(shù)據(jù)又是舊數(shù)據(jù)。
代碼演示
// Redis連接對象
$redis = null;
// MySQL連接對象
$mysql = null;
// 客戶端請求參數(shù)
$requestParams = [];
// 線程一更新MySQL
$updateMysql = $mysql->update('update xxx set a=xx where id=xxx');
// 線程二更新緩存
$updateRedis = $redis->set('key', $requestParams);
if ($updateMysql && $updateRedis) {
return '數(shù)據(jù)更新成功';
}
// 執(zhí)行數(shù)據(jù)回滾
.....
return '數(shù)據(jù)更新失敗';
加鎖處理

策略說明
客戶端發(fā)起請求,創(chuàng)建一個鎖。
此時依次更新MySQL和緩存數(shù)據(jù)。
不管成功和失敗,執(zhí)行完之后就釋放鎖。
問題分析
客戶端發(fā)起請求,創(chuàng)建一個鎖。在創(chuàng)建鎖的時候,可以使用set-nx方式,避免服務(wù)掛掉緩存不會自動過期。
更新MySQL和緩存數(shù)據(jù)。
緩存成功則釋放鎖,緩存失敗則釋放鎖。
該方式適合數(shù)據(jù)高度一致性的情況,例如后端在發(fā)起請求時,客戶端就不能進行讀操作,直到寫操作成功或者失敗后釋放鎖。
使用該方式,需要客戶端讀代碼判斷鎖情況處理。存在鎖則處于等待情況。不適合高并發(fā)的業(yè)務(wù)場景。但是保證了數(shù)據(jù)的完全一致。
代碼演示
// Redis連接對象
$redis = null;
// MySQL連接對象
$mysql = null;
// 客戶端請求參數(shù)
$requestParams = [];
/ 客戶端發(fā)起請求加鎖
// 更新MySQL
$updateMysql = $mysql->update('update xxx set a=xx where id=xxx');
$updateRedis = $redis->set('key', $requestParams);
if ($updateMysql && $updateRedis) {
// 釋放鎖
// 返回信息
return '數(shù)據(jù)更新成功';
}
// 釋放鎖
// 返回信息
return '更新失敗';
文章總結(jié)
該文屬于針對不同情況的分析。很多情況也只是出于一種理論的狀態(tài)。比較推薦的方式,還是推薦使用先更新MySQL在更新緩存。
推薦閱讀
Redis數(shù)據(jù)類型應(yīng)用場景總結(jié)
Redis的過期策略和內(nèi)存淘汰策略最全總結(jié)與分析
