Redis和數據庫數據一致性問題
Redis作為緩存分兩種情形
- 只讀緩存, 只讀緩存無需考慮數據更新問題, Redis中有則返回Redis中的數據, Redis無則查詢數據庫
- 讀寫緩存
- 同步直寫策略
- 異步緩寫策略
數據讀取流程:
正常回寫Redis代碼流程:
public Object getDataById(String id) {// 1. 先去Redis中查詢Object obj;obj = redisTemplate.opsForValue().get(DATA_PR + id);if (obj == null) {// 2. redis中查詢出來為null則查詢數據庫obj = testMapper.getDataById(id);if (obj == null) {// 3.1redis, 數據庫都沒有數據// 可以給這個id給個空值存在Redis中redisTemplate.opsForValue().set(DATA_PR + id, "null");} else {// 3.2 數據庫中有數據將數據寫回RedisredisTemplate.opsForValue().set(DATA_PR + id, obj);}}return obj;}
保證并發時, 避免同一數據頻繁寫入Redis對回寫緩存代碼進行加鎖
public Object getDataById1(String id) {// 1. 先去Redis中查詢Object obj;obj = redisTemplate.opsForValue().get(DATA_PR + id);if (obj == null) {// 緩存不存在則加鎖// 假設請求量很大synchronized (TestServiceImpl.class) {obj = redisTemplate.opsForValue().get(DATA_PR + id);if (obj != null) {// 查詢有數據則直接返回return obj;} else {// 沒有數據再查詢數據庫obj = testMapper.getDataById(id);// 回寫緩存if (obj != null) {redisTemplate.opsForValue().setIfAbsent(DATA_PR + id, obj, 20, TimeUnit.SECONDS);return obj;} else {return null;}}}}return obj;}
給緩存設置過期時間, 定期清理緩存并回寫, 是保證最終一致性的解決方案
我們可以對存入緩存的數據設置過期時間, 所有的寫操作以數據庫為準, 對緩存的操作只是盡最大努力即可, 也就是說數據庫寫成功緩存更新失敗, 那么只要達到過期時間, 則后面的讀請求自然會從數據庫讀取新值然后回填緩存,達到一致性, 要以數據庫寫入庫為準
更新數據庫并更新Redis有以下幾種情況
-
先更新數據庫再更新Redis
這種情況會出現的問題:
請求A先將字段x更新為10
請求B后將字段x更新為20
正常流程:
請求A更新數據庫將x更新為10
請求A更新Redis 將x更新為10
請求B更新數據庫將x更新為20
請求B更新Redis 將x更新為20
異常情況:
請求A更新數據庫將x更新為10
請求B更新數據庫將x更新為20
請求B更新Redis 將x更新為20
請求A更新Redis 將x更新為10
此時數據庫x為10, Redis中x為20出現緩存和數據庫數據不一致情況
-
先更新Redis再更新數據庫
這種情況會出現的問題:
請求A先將字段x更新為10
請求B后將字段x更新為20
正常流程:
請求A更新Redis 將x更新為10
請求A更新數據庫將x更新為10
請求B更新Redis將x更新為20
請求B更新數據庫將x更新為20
異常情況:
請求A更新Redis 將x更新為10
請求B更新Redis將x更新為20
請求B更新數據庫將x更新為20
請求A更新數據庫將x更新為10
此時數據中為20, Redis中為10 出現緩存和數據庫數據不一致情況
-
先刪除Redis再更新數據庫
這種情況會出現的問題:
請求A先將字段x更新為10
請求B查詢x的值
正常流程
請求A刪除Redis中x的值
請求A更新數據中的值為10
請求B查詢Redis中沒有值, 查詢數據庫
請求B回寫緩存x的值
異常流程
請求A刪除Redis中x的值
請求B查詢Redis中沒有值, 查詢數據庫但是此時請求A還未更新數據庫或者是還沒有commit
請求B查詢數據庫, 回寫緩存
請求A更新數據庫
此時緩存中仍然是緩存的舊值, 數據庫和緩存值不一致
-
先更新數據庫再刪除Redis
這種情況會出現的問題:
請求A先將字段x更新為10
請求B查詢x的值
正常流程
請求A更新數據庫
請求A刪除Redis中的緩存值
請求B查詢數據庫并回寫緩存
異常流程
請求A更新數據庫但未提交
請求B讀取的是舊值
請求A刪除緩存
為了解決先刪除后修改數據庫的異常情況
延時雙刪
public void updateData(Object obj, String id) {// 先刪除緩存中的數據redisTemplate.opsForValue().getAndDelete(DATA_PR + id);// 再更新數據庫testMapper.updateData(obj);// 再次刪除緩存中的數據避免其他線程讀取舊值并回寫緩存// 需要在這里等待,等待的原因是如果另外的線程讀取的線程還在回寫的流程中舊值還未寫到緩存中, 那么刪除是沒有意義的// 這里等待的時間就是大于等待其他線程將舊值寫入緩存的時間try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}redisTemplate.opsForValue().getAndDelete(DATA_PR + id);}
通常我們會采用先更新數據庫再刪除Redis緩存, 但是依然會存在在更新期間讀取到舊值的情況, 還會存在刪除緩存失敗問題, 此時可引入消息中間件將需要更改的數據推送到MQ, 再通過MQ去對Redis進行刪除, 這樣也不能保證強一致性, 只是一個比較折中的方案
引入中間件自動同步數據到Redis canal
如果我們的數據庫事MySQL可以通過引入開源中間件canal對MySQL的binlog進行監聽, 當數據庫表發生變化時自動去將MySQL的變更寫到我們的緩存中
未完待續…