緩存一致性

讀緩存
雙檢加鎖策略
采用雙檢加鎖策略
- 多個線程同時去查詢數(shù)據(jù)庫的這條數(shù)據(jù),那么我們可以在第一個查詢數(shù)據(jù)的請求上使用一個 互斥鎖來鎖住它。
- 其他的線程走到這一步拿不到鎖就等著,等第一個線程查詢到了數(shù)據(jù),然后做緩存。
- 后面的線程進(jìn)來發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
package com.atguigu.redis.service;
import com.atguigu.redis.entities.User;
import com.atguigu.redis.mapper.UserMapper;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2021-05-01 14:58
*/
@Service
@Slf4j
public class UserService {
public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* 業(yè)務(wù)邏輯沒有寫錯,對于小廠中廠(QPS《=1000)可以使用,但是大廠不行
* @param id
* @return
*/
public User findUserById(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先從redis里面查詢,如果有直接返回結(jié)果,如果沒有再去查詢mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null)
{
//2 redis里面無,繼續(xù)查詢mysql
user = userMapper.selectByPrimaryKey(id);
if(user == null)
{
//3.1 redis+mysql 都無數(shù)據(jù)
//你具體細(xì)化,防止多次穿透,我們業(yè)務(wù)規(guī)定,記錄下導(dǎo)致穿透的這個key回寫redis
return user;
}else{
//3.2 mysql有,需要將數(shù)據(jù)寫回redis,保證下一次的緩存命中率
redisTemplate.opsForValue().set(key,user);
}
}
return user;
}
/**
* 加強(qiáng)補(bǔ)充,避免突然key失效了,打爆mysql,做一下預(yù)防,盡量不出現(xiàn)擊穿的情況。
* @param id
* @return
*/
public User findUserById2(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先從redis里面查詢,如果有直接返回結(jié)果,如果沒有再去查詢mysql,
// 第1次查詢redis,加鎖前
user = (User) redisTemplate.opsForValue().get(key);
if(user == null) {
//2 大廠用,對于高QPS的優(yōu)化,進(jìn)來就先加鎖,保證一個請求操作,讓外面的redis等待一下,避免擊穿mysql
synchronized (UserService.class){
//第2次查詢redis,加鎖后
user = (User) redisTemplate.opsForValue().get(key);
//3 二次查redis還是null,可以去查mysql了(mysql默認(rèn)有數(shù)據(jù))
if (user == null) {
//4 查詢mysql拿數(shù)據(jù)(mysql默認(rèn)有數(shù)據(jù))
user = userMapper.selectByPrimaryKey(id);
if (user == null) {
return null;
}else{
//5 mysql里面有數(shù)據(jù)的,需要回寫redis,完成數(shù)據(jù)一致性的同步工作
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}
}
寫緩存
保障最終數(shù)據(jù)一致性解決方案
- 給緩存設(shè)置過期時間
- 定期清理緩存并回寫
先更新數(shù)據(jù)庫,再更新緩存
-
案例演示1->更新緩存異常
- 先更新mysql的某商品的庫存,當(dāng)前商品的庫存是100,更新為99個。
- 先更新mysql修改為99成功,然后更新redis。
- 此時假設(shè)異常出現(xiàn),更新redis失敗了,這導(dǎo)致mysql里面的庫存是99而redis里面的還是100 。
- 上述發(fā)生,會讓數(shù)據(jù)庫里面和緩存redis里面數(shù)據(jù)不一致,讀到redis臟數(shù)據(jù)
-
案例演示2->并發(fā)導(dǎo)致
-
A、B兩個線程發(fā)起調(diào)用;A寫,B寫
1 A update mysql 100 3 B update mysql 80 4 B update redis 80 2 A update redis 100
-
最終結(jié)果:mysql80,redis100->數(shù)據(jù)不一致
-
先更新緩存,再更新數(shù)據(jù)庫
不推薦,業(yè)務(wù)上一般把mysql作為底單數(shù)據(jù)庫,保證最后解釋
-
案例演示->并發(fā)導(dǎo)致
-
A、B兩個線程發(fā)起調(diào)用;A寫,B寫
A update redis 100 B update redis 80 B update mysql 80 A update mysql 100
-
最終結(jié)果:mysql100,redis80->數(shù)據(jù)不一致
-
先刪除緩存,再更新數(shù)據(jù)庫
-
案例演示->并發(fā)導(dǎo)致
-
A、B兩個線程發(fā)起調(diào)用;A寫,B讀
- 請求A進(jìn)行寫操作,刪除redis緩存后,工作正在進(jìn)行中,更新mysql…A還么有徹底更新完mysql,還沒commit
- 請求B開工查詢,查詢redis發(fā)現(xiàn)緩存不存在(被A從redis中刪除了)
- 請求B繼續(xù),去數(shù)據(jù)庫查詢得到了mysql中的舊值(A還沒有更新完)
- 請求B將舊值寫回redis緩存
- 請求A將新值寫入mysql數(shù)據(jù)庫
-
總結(jié)
時間 線程A 線程B 出現(xiàn)的問題 t1 請求A進(jìn)行寫操作,刪除緩存成功后,工作正在mysql進(jìn)行中… t2 1 緩存中讀取不到,立刻讀mysql,由于A還沒有對mysql更新完,讀到的是舊值 2 還把從mysql讀取的舊值,寫回了redis 1 A還沒有更新完mysql,導(dǎo)致B讀到了舊值 2 線程B遵守回寫機(jī)制,把舊值寫回redis,導(dǎo)致其它請求讀取的還是舊值,A白干了。 t3 A更新完mysql數(shù)據(jù)庫的值,over redis是被B寫回的舊值,mysql是被A更新的新值。出現(xiàn)了,數(shù)據(jù)不一致問題。
-
-
解決策略->延時雙刪
加上sleep的這段時間,就是為了讓線程B能夠先從數(shù)據(jù)庫讀取數(shù)據(jù),再把缺失的數(shù)據(jù)寫入緩存,然后,線程A再進(jìn)行刪除。所以,線程Asleep的時間,就需要大于線程B讀取數(shù)據(jù)再寫入緩存的時間。這樣一來,其它線程讀取數(shù)據(jù)時,會發(fā)現(xiàn)緩存缺失,所以會從數(shù)據(jù)庫中讀取最新值。因為這個方案會在第次刪除緩存值后,延遲一段時間再次進(jìn)行刪除,所以我們也把它叫做“延遲雙刪”
-
刪除該休眠多久合適?
-
方式一:
在業(yè)務(wù)程序運行的時候,統(tǒng)計下線程讀數(shù)據(jù)和寫緩存的操作時間,自行評估自己的項目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時,
以此為基礎(chǔ)來進(jìn)行估算。然后寫數(shù)據(jù)的休眠時間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時基礎(chǔ)上加百毫秒即可。
-
方式二
新啟動一個后臺監(jiān)控程序,比如后面要講解的WatchDog監(jiān)控程序,會加時
-
-
先更新數(shù)據(jù)庫,再刪除緩存(推薦~~)
-
案例演示1->更新緩存異常
-
A、B兩個線程發(fā)起調(diào)用;A寫,B讀
t3時間上線程A更新Redis緩存失敗,會導(dǎo)致Redis緩存與mysql數(shù)據(jù)不一致的情況發(fā)生
時間 線程A 線程B 出現(xiàn)的問題 t1 更新數(shù)據(jù)庫中的值… t2 緩存中立刻命中,此時B讀取的是緩存舊值。 A還沒有來得及刪除緩存的值,導(dǎo)致B緩存命中讀到舊值。 t3 更新緩存的數(shù)據(jù),over
-
-
解決策略->消息隊列重試寫Redis緩存
-
可以把要刪除的緩存值或者是要更新的數(shù)據(jù)庫值暫存到消息隊列中(例如使用Kafka/RabbitMQ等)。
-
當(dāng)程序沒有能夠成功地刪除緩存值或者是更新數(shù)據(jù)庫值時,可以從消息隊列中重新讀取這些值,然后再次進(jìn)行刪除或更新。
-
如果能夠成功地刪除或更新,我們就要把這些值從消息隊列中去除,以免重復(fù)操作,此時,我們也可以保證數(shù)據(jù)庫和緩存的數(shù)據(jù)一致了,否則還需要再次進(jìn)行重試
-
如果重試超過的一定次數(shù)后還是沒有成功,我們就需要向業(yè)務(wù)層發(fā)送報錯信息了,通知運維人員。
-
如何選方案
優(yōu)先使用先更新數(shù)據(jù)庫,再刪除緩存的方案(先更庫→后刪存)
- 先刪除緩存值再更新數(shù)據(jù)庫,有可能導(dǎo)致請求因緩存缺失而訪問數(shù)據(jù)庫,給數(shù)據(jù)庫帶來壓力導(dǎo)致打滿mysql。
- 如果業(yè)務(wù)應(yīng)用中讀取數(shù)據(jù)庫和寫緩存的時間不好估算,那么,延遲雙刪中的等待時間就不好設(shè)置。
策略 | 高并發(fā)多線程條件下 | 問題 | 現(xiàn)象 | 解決方案 |
---|---|---|---|---|
先刪除redis緩存,再更新mysql | 無 | 緩存刪除成功但數(shù)據(jù)庫更新失敗 | Java程序從數(shù)據(jù)庫中讀到舊值 | 再次更新數(shù)據(jù)庫,重試 |
有 | 緩存刪除成功但數(shù)據(jù)庫更新中…有并發(fā)讀請求 | 并發(fā)請求從數(shù)據(jù)庫讀到舊值并回寫到redis,導(dǎo)致后續(xù)都是從redis讀取到舊值 | 延遲雙刪 | |
先更新mysql,再刪除redis緩存 | 無 | 數(shù)據(jù)庫更新成功,但緩存刪除失敗 | Java程序從redis中讀到舊值 | 再次刪除緩存,重試 |
有 | 數(shù)據(jù)庫更新成功但緩存刪除中…有并發(fā)讀請求 | 并發(fā)請求從緩存讀到舊值 | 等待redis刪除完成,這段時間有數(shù)據(jù)不一致,短暫存在。 |
Redis與MySQL數(shù)據(jù)雙寫一致性工程落地

阿里巴巴開源的中間件-canal
定義
定義:歷史背景是早期阿里巴巴因為杭州和美國雙機(jī)房部署,存在跨機(jī)房數(shù)據(jù)同步的業(yè)務(wù)需求,實現(xiàn)方式主要是基于業(yè)務(wù) trigger(觸發(fā)器) 獲取增量變更。從2010年開始,阿里巴巴逐步嘗試采用解析數(shù)據(jù)庫日志獲取增量變更進(jìn)行同步,由此衍生出了canal項目

官網(wǎng):https://github.com/alibaba/canal/wiki
作用
- 數(shù)據(jù)庫鏡像
- 數(shù)據(jù)庫實時備份
- 索引構(gòu)建和實時維護(hù)(拆分異構(gòu)索引、倒排索引等)
- 業(yè)務(wù) cache 刷新
- 帶業(yè)務(wù)邏輯的增量數(shù)據(jù)處理
下載
地址:https://github.com/alibaba/canal/releases
工作原理
-
MySQL的主從復(fù)制
-
當(dāng) master 主服務(wù)器上的數(shù)據(jù)發(fā)生改變時,則將其改變寫入二進(jìn)制事件日志文件binlog中;
-
salve 從服務(wù)器會在一定時間間隔內(nèi)對 master 主服務(wù)器上的二進(jìn)制日志進(jìn)行探測,探測其是否發(fā)生過改變,如果探測到 master 主服務(wù)器的二進(jìn)制事件日志發(fā)生了改變,則開始一個 I/O Thread 請求 master 二進(jìn)制事件日志;
-
同時 master 主服務(wù)器為每個 I/O Thread 啟動一個dump Thread,用于向其發(fā)送二進(jìn)制事件日志
-
slave 從服務(wù)器將接收到的二進(jìn)制事件日志保存至自己本地的中繼日志文件中;
-
salve 從服務(wù)器將啟動 SQL Thread 從中繼日志中讀取二進(jìn)制日志,在本地重放,使得其數(shù)據(jù)和主服務(wù)器保持一致;
-
最后 I/O Thread 和 SQL Thread 將進(jìn)入睡眠狀態(tài),等待下一次被喚醒;
-
-
canal工作原理
-
canal 模擬 MySQL slave 的交互協(xié)議,偽裝自己為 MySQL slave,向 MySQL master 發(fā)送dump 協(xié)議
-
MySQL master 收到 dump 請求,開始推送 binary log 給 slave ( canal )
-
canal解析 binary log 對象(原始為 byte 流)
-
一致性工程案例
MySQL
-
查看當(dāng)前主機(jī)二進(jìn)制日志
show master status;
-
查看log_bin日志狀態(tài)
show variables like 'log_bin';
-
開啟MySQL的binlog寫入功能(Windows下的my.ini配置文件,Linux下的my.cnf配置文件)
log-bin=mysql-bin #開啟 binlog binlog-format=ROW #選擇 ROW 模式 server_id=1 #配置MySQL replaction需要定義,不要和canal的 slaveId重復(fù) ROW模式 除了記錄sql語句之外,還會記錄每個字段的變化情況,能夠清楚的記錄每行數(shù)據(jù)的變化歷史,但會占用較多的空間。 STATEMENT模式只記錄了sql語句,但是沒有記錄上下文信息,在進(jìn)行數(shù)據(jù)恢復(fù)的時候可能會導(dǎo)致數(shù)據(jù)的丟失情況; MIX模式比較靈活的記錄,理論上說當(dāng)遇到了表結(jié)構(gòu)變更的時候,就會記錄為statement模式。當(dāng)遇到了數(shù)據(jù)更新或者刪除情況下就會變?yōu)閞ow模式;
-
重啟MySQL
-
驗證開啟binlog是否成功
show variables like 'log_bin';
-
授權(quán)canal連接mysql的賬號
-
查看當(dāng)前目錄下的賬號
SELECT * FROM mysql.user;
-
創(chuàng)建賬號并授權(quán)(此次mysql版本為5.7)
[刪除canal用戶,可忽略] DROP USER IF EXISTS 'canal'@'%'; CREATE USER 'canal'@'%' IDENTIFIED BY 'canal'; GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal'; FLUSH PRIVILEGES; [查看是否添加成功] SELECT * FROM mysql.user;
-
canal服務(wù)端
-
下載
canal.deployer-1.1.6.tar.gz
:https://github.com/alibaba/canal/releases/tag/canal-1.1.6 -
解壓
-
配置
修改/mycanal/conf/example路徑下instance.properties文件
-
換成自己的mysql主機(jī)master的IP地址
-
換成自己的在mysql新建的canal賬戶
-
-
啟動
/opt/mycanal/bin路徑下執(zhí)行
./startup.sh
-
查看驗證
-
查看server日志
-
查看樣例example的日志文章來源:http://www.zghlxwxcb.cn/news/detail-423152.html
-
canal客戶端
https://github.com/bithaolee/canal-python文章來源地址http://www.zghlxwxcb.cn/news/detail-423152.html
到了這里,關(guān)于【Redis】緩存一致性的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!