數據存儲在數據庫中,為了加快業務訪問的速度,我們將數據庫中的一些數據放在緩存中,那么問題來了,如何確保db和緩存中數據的一致性呢?我們列出了5種方法,大家都了解一下,然后根據業務自己選擇。
方案1
獲取緩存邏輯
使用過定時器,定時刷新redis中的緩存。
db更新數據邏輯
更新數據不用考慮緩存中的數據,直接更新數據就可以了
存在的問題
緩存中數據和db中數據一致性可能沒有那么及時,不過最終在某個時間點,數據是一致的。
方案2
獲取緩存邏輯
c1:根據key在redis中獲取對應的value
c2:如果value存在,直接返回value;若value不存在,繼續下面步驟
c3:從數據庫獲取值,賦值給value,然后將key->value放入redis,返回value
更新db邏輯
u1:開始db事務
u2:更新數據
u3:提交db事務
u4:刪除redis中當前數據的緩存
存在的問題
- 上面u3成功,u4失敗,會導致db數據更新成功,緩存刪除失敗,結果:db和緩存數據不一致
- 如果同時有很多線程到達c2發現緩存不存在,同時請求c3訪問db,會對db造成很大的壓力
方案3
獲取緩存邏輯
c1:根據key在redis中獲取對應的value
c2:如果value存在,直接返回value;若value不存在,繼續下面步驟
c3:從數據庫獲取值,賦值給value,然后將key->value放入redis,返回value
更新db邏輯
u1:刪除redis中當前數據的緩存
u2:開始db事務
u3:更新數據
u4:提交db事務
存在的問題
- 更新數據的線程執行u1成功之后,u2還未執行時,此時獲取緩存的線程剛好執行了c1到c3的邏輯,此時會將舊的數據放入redis,結果:db和緩存數據不一致
- 同樣存在方案2中說到的問題:如果同時有很多線程到達c2發現緩存不存在,同時請求c3訪問db,會對db造成很大的壓力
方案4
對方案2做改進,確保db更新成功之后,刪除緩存操作一定會執行,我們可以通過可靠消息來實現,可靠消息可以確保更新db操作和刪除redis中緩存最終要么都成功要么都失敗,依靠的是最終一致性來實現的。
改進之后過程如下。
獲取緩存邏輯
c1:根據key在redis中獲取對應的value
c2:如果value存在,直接返回value;若value不存在,繼續下面步驟
c3:從數據庫獲取值,賦值給value,然后將key->value放入redis,返回value
更新db邏輯
u1:開始db事務
u2:更新數據
u3:投遞刪除redis緩存的消息
u4:提交db事務
消息消費者-清理redis緩存的消費者
接受到清理redis緩存的消息之后,將redis中對應的緩存清除。
存在的問題
- 更新db和清理redis中的緩存之間存在一定的時間延遲,這段時間內,redis緩存的數據是舊的,也就是說這段時間內db和緩存數據是不一致的,但是最終會一致,這個不一致的時間可能比較小(這個需要看消息消費的效率了)
- 同樣存在方案2中說到的問題:如果同時有很多線程到達c2發現緩存不存在,同時請求c3訪問db,會對db造成很大的壓力
關於可靠消息的,可以看
方式5
我們先了解一些知識。
redis中幾個方法
get(key)
獲取key的值,如果存在,則返回;如果不存在,則返回nil
setnx(key,value)
setnx的含義就是SET if Not Exists,該方法是原子的,如果key不存在,則設置當前key成功,返回1;如果當前key已經存在,則設置當前key失敗,返回0
del(key)
將key對應的值從redis中刪除
數據庫相關知識
select v from t where t.key = #key# for update;
update t set v = #v# where t.key = #key#;
上面兩個sql會相互阻塞,直到其中一個提交之后,另外一個才可以繼續執行。
下面我們就通過上面的知識來實現db和緩存強一致性。
更新數據邏輯
1.打開db事務
2.update t set v = #v# where t.key = #key#;
3.根據key刪除redis中的緩存:RedisUti.del(key);
4.提交db事務
獲取緩存邏輯
/*公眾號:路人甲Java
* 工作10年的前阿里P7分享Java、算法、數據庫方面的技術干貨!
* 堅信用技術改變命運,讓家人過上更體面的生活。*/
public class CacheUtil {
//根據key獲取緩存中對應的value
public static String getCache(String key) throws InterruptedException {
String value = RedisUtils.get(key);
if (value != null) {
return value;
}
//過期時間為當前時間+5秒
String expireTimeKey = key + "ExpireTime";
long expireTimeValue = System.currentTimeMillis() + 5000;
//setnx是原子操作,所以只有一個會成功
int setnx = RedisUtils.setnx(expireTimeKey, expireTimeValue + "");
if (setnx == 0) {
expireTimeValue = Long.valueOf(RedisUtils.get(expireTimeKey));
//如果expireTimeValue小於當前時間,說明expireTimeKey過期了,將其刪除
if (System.currentTimeMillis() > expireTimeValue) {
//將expireTimeKey對應的刪除
RedisUtils.del(expireTimeKey);
} else {
//休眠1秒繼續獲取
TimeUnit.SECONDS.sleep(1);
}
//重試
return getCache(key);
} else {
//1. 開啟db事務
start transaction;
//2. 執行select v from t where t.key = #key# for update; 將v的值賦值給value
select v from t where t.key = #key# for update;
RedisUtils.set(key, value);
//3.提交db事務
commit transaction;
}
return value;
}
//redis工具類,內部方法為偽代碼
public static class RedisUtils {
//根據key獲取value
public static String get(String key) {
return null;
}
//設置key對應的value
public static void set(String key, String value) {
}
//刪除redis中一個key對應的值
public static void del(String key) {
}
//setnx的含義就是SET if Not Exists,該方法是原子的,如果key不存在,
//則設置當前key成功,返回1;如果當前key已經存在,則設置當前key失敗,返回0
public static int setnx(String key, String value) {
return 1;
}
}
}
這種方式可以確保db和redis中緩存同一時間強一致。
expireTimeKey為了防止某些線線程執行
RedisUtils.setnx(expireTimeKey, expireTimeValue + "");
返回1,表示setnx成功了,然后執行下一行代碼的時候系統后掛了,會導致將db數據加載到redis中失敗,代碼:if (System.currentTimeMillis() > expireTimeValue)
是給其他線程機會,可以獲取這個過期時間,發現過期之后直接刪掉,這樣其他線程才有機會將db數據load到redis中。
工作10年的前阿里P7分享Java、算法、數據庫方面的技術干貨!堅信用技術改變命運,讓家人過上更體面的生活!喜歡的請關注公眾號:路人甲Java