聊聊db和快取一致性的5種實現方式

  • 2019 年 10 月 22 日
  • 筆記

數據存儲在資料庫中,為了加快業務訪問的速度,我們將資料庫中的一些數據放在快取中,那麼問題來了,如何確保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中當前數據的快取

存在的問題

  1. 上面u3成功,u4失敗,會導致刪除快取失敗,導致快取中數據和db數據會不一致。
  2. 如果同時有很多執行緒到達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事務

存在的問題

  1. 更新數據的執行緒執行u1成功之後,u2還未執行時,此時獲取快取的執行緒剛好執行了c1到c3的邏輯,此時會將舊的數據放入redis,導致redis和db數據不一致
  2. 同樣存在方案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中對應的快取清除。

存在的問題

  1. 更新db和清理redis中的快取之間存在一定的時間延遲,這段時間內,redis快取的數據是舊的,也就是說這段時間內db和快取數據是不一致的,但是最終會一致,這個不一致的時間可能比較小(這個需要看消息消費的效率了)
  2. 同樣存在方案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. 執行update  t set v = #v# where t.key = #key# for update; 將v的值賦值給value              update  t set v = #v# 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