自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

技術(shù)派中的緩存一致性解決方案

數(shù)據(jù)庫(kù) MySQL
這篇文章很基礎(chǔ),也非常適用,大家可以直接下載技術(shù)派項(xiàng)目,里面都有代碼和測(cè)試用例,代碼倉(cāng)庫(kù)詳見(jiàn):https://github.com/itwanger/paicoding

大家好,我是樓仔呀。

之前寫(xiě)過(guò)一篇《高頻面試:如何保障 MySQL 和 Redis 的數(shù)據(jù)一致性?》,閱讀量直奔 7K,但是里面只有理論,沒(méi)有實(shí)戰(zhàn),今天就結(jié)合技術(shù)派項(xiàng)目,告訴大家如何去實(shí)現(xiàn) MySQL 和 Redis 的一致性。

在講解實(shí)戰(zhàn)部分之前,我們還是先回顧一下理論知識(shí),根據(jù)網(wǎng)上的眾多解決方案,我們總結(jié)出 6 種:

圖片

你可以先想想,技術(shù)派會(huì)采用哪種方案呢?

理論知識(shí)

溫馨提示:如果你對(duì)理論知識(shí)已經(jīng)非常清楚,可以直接跳到文章的實(shí)戰(zhàn)部分。

不好的方案

1. 先寫(xiě) MySQL,再寫(xiě) Redis

圖片

圖解說(shuō)明:

  • 這是一副時(shí)序圖,描述請(qǐng)求的先后調(diào)用順序;
  • 橘黃色的線(xiàn)是請(qǐng)求 A,黑色的線(xiàn)是請(qǐng)求 B;
  • 橘黃色的文字,是 MySQL 和 Redis 最終不一致的數(shù)據(jù);
  • 數(shù)據(jù)是從 10 更新為 11;
  • 后面所有的圖,都是這個(gè)含義,不再贅述。

請(qǐng)求 A、B 都是先寫(xiě) MySQL,然后再寫(xiě) Redis,在高并發(fā)情況下,如果請(qǐng)求 A 在寫(xiě) Redis 時(shí)卡了一會(huì),請(qǐng)求 B 已經(jīng)依次完成數(shù)據(jù)的更新,就會(huì)出現(xiàn)圖中的問(wèn)題。

這個(gè)圖已經(jīng)畫(huà)的很清晰了,我就不用再去啰嗦了吧,不過(guò)這里有個(gè)前提,就是對(duì)于讀請(qǐng)求,先去讀 Redis,如果沒(méi)有,再去讀 DB,但是讀請(qǐng)求不會(huì)再回寫(xiě) Redis。 大白話(huà)說(shuō)一下,就是讀請(qǐng)求不會(huì)更新 Redis。

2. 先寫(xiě) Redis,再寫(xiě) MySQL

圖片

同“先寫(xiě) MySQL,再寫(xiě) Redis”,看圖可秒懂。

3. 先刪除 Redis,再寫(xiě) MySQL

這幅圖和上面有些不一樣,前面的請(qǐng)求 A 和 B 都是更新請(qǐng)求,這里的請(qǐng)求 A 是更新請(qǐng)求,但是請(qǐng)求 B 是讀請(qǐng)求,且請(qǐng)求 B 的讀請(qǐng)求會(huì)回寫(xiě) Redis。

圖片

請(qǐng)求 A 先刪除緩存,可能因?yàn)榭D,數(shù)據(jù)一直沒(méi)有更新到 MySQL,導(dǎo)致兩者數(shù)據(jù)不一致。

這種情況出現(xiàn)的概率比較大,因?yàn)檎?qǐng)求 A 更新 MySQL 可能耗時(shí)會(huì)比較長(zhǎng),而請(qǐng)求 B 的前兩步都是查詢(xún),會(huì)非???。

好的方案

4. 先刪除 Redis,再寫(xiě) MySQL,再刪除 Redis

對(duì)于“先刪除 Redis,再寫(xiě) MySQL”,如果要解決最后的不一致問(wèn)題,其實(shí)再對(duì) Redis 重新刪除即可,這個(gè)也是大家常說(shuō)的“緩存雙刪”。

圖片

為了便于大家看圖,對(duì)于藍(lán)色的文字,“刪除緩存 10”必須在“回寫(xiě)緩存10”后面,那如何才能保證一定是在后面呢?網(wǎng)上給出的第一個(gè)方案是,讓請(qǐng)求 A 的最后一次刪除,等待 500ms。

對(duì)于這種方案,看看就行,反正我是不會(huì)用,太 Low 了,風(fēng)險(xiǎn)也不可控。

那有沒(méi)有更好的方案呢,我建議異步串行化刪除,即刪除請(qǐng)求入隊(duì)列

圖片

異步刪除對(duì)線(xiàn)上業(yè)務(wù)無(wú)影響,串行化處理保障并發(fā)情況下正確刪除。

如果雙刪失敗怎么辦,網(wǎng)上有給 Redis 加一個(gè)緩存過(guò)期時(shí)間的方案,這個(gè)不敢茍同。個(gè)人建議整個(gè)重試機(jī)制,可以借助消息隊(duì)列的重試機(jī)制,也可以自己整個(gè)表,記錄重試次數(shù),方法很多。

簡(jiǎn)單小結(jié)一下:

  • “緩存雙刪”不要用無(wú)腦的 sleep 500 ms;
  • 通過(guò)消息隊(duì)列的異步&串行,實(shí)現(xiàn)最后一次緩存刪除;
  • 緩存刪除失敗,增加重試機(jī)制。

5. 先寫(xiě) MySQL,再刪除 Redis

圖片

對(duì)于上面這種情況,對(duì)于第一次查詢(xún),請(qǐng)求 B 查詢(xún)的數(shù)據(jù)是 10,但是 MySQL 的數(shù)據(jù)是 11,只存在這一次不一致的情況,對(duì)于不是強(qiáng)一致性要求的業(yè)務(wù),可以容忍。(那什么情況下不能容忍呢,比如秒殺業(yè)務(wù)、庫(kù)存服務(wù)等。)

當(dāng)請(qǐng)求 B 進(jìn)行第二次查詢(xún)時(shí),因?yàn)闆](méi)有命中 Redis,會(huì)重新查一次 DB,然后再回寫(xiě)到 Reids。

圖片

這里需要滿(mǎn)足 2 個(gè)條件:

  • 緩存剛好自動(dòng)失效;
  • 請(qǐng)求 B 從數(shù)據(jù)庫(kù)查出 10,回寫(xiě)緩存的耗時(shí),比請(qǐng)求 A 寫(xiě)數(shù)據(jù)庫(kù),并且刪除緩存的還長(zhǎng)。

對(duì)于第二個(gè)條件,我們都知道更新 DB 肯定比查詢(xún)耗時(shí)要長(zhǎng),所以出現(xiàn)這個(gè)情況的概率很小,同時(shí)滿(mǎn)足上述條件的情況更小。

6. 先寫(xiě) MySQL,通過(guò) Binlog,異步更新 Redis

這種方案,主要是監(jiān)聽(tīng) MySQL 的 Binlog,然后通過(guò)異步的方式,將數(shù)據(jù)更新到 Redis,這種方案有個(gè)前提,查詢(xún)的請(qǐng)求,不會(huì)回寫(xiě) Redis。

圖片

這個(gè)方案,會(huì)保證 MySQL 和 Redis 的最終一致性,但是如果中途請(qǐng)求 B 需要查詢(xún)數(shù)據(jù),如果緩存無(wú)數(shù)據(jù),就直接查 DB;如果緩存有數(shù)據(jù),查詢(xún)的數(shù)據(jù)也會(huì)存在不一致的情況。

所以這個(gè)方案,是實(shí)現(xiàn)最終一致性的終極解決方案,但是不能保證實(shí)時(shí)性。

幾種方案比較

我們對(duì)比上面討論的 6 種方案:

  1. 先寫(xiě) Redis,再寫(xiě) MySQL
  • 這種方案,我肯定不會(huì)用,萬(wàn)一 DB 掛了,你把數(shù)據(jù)寫(xiě)到緩存,DB 無(wú)數(shù)據(jù),這個(gè)是災(zāi)難性的;
  • 我之前也見(jiàn)同學(xué)這么用過(guò),如果寫(xiě) DB 失敗,對(duì) Redis 進(jìn)行逆操作,那如果逆操作失敗呢,是不是還要搞個(gè)重試?
  1. 先寫(xiě) MySQL,再寫(xiě) Redis
  • 對(duì)于并發(fā)量、一致性要求不高的項(xiàng)目,很多就是這么用的,我之前也經(jīng)常這么搞,但是不建議這么做;
  • 當(dāng) Redis 瞬間不可用的情況,需要報(bào)警出來(lái),然后線(xiàn)下處理。
  1. 先刪除 Redis,再寫(xiě) MySQL
  • 這種方式,我還真沒(méi)用過(guò),直接忽略吧。
  1. 先刪除 Redis,再寫(xiě) MySQL,再刪除 Redis
  • 這種方式雖然可行,但是感覺(jué)好復(fù)雜,還要搞個(gè)消息隊(duì)列去異步刪除 Redis。
  1. 先寫(xiě) MySQL,再刪除 Redis
  • 比較推薦這種方式,刪除 Redis 如果失敗,可以再多重試幾次,否則報(bào)警出來(lái);
  • 這個(gè)方案,是實(shí)時(shí)性中最好的方案,在一些高并發(fā)場(chǎng)景中,推薦這種。
  1. 先寫(xiě) MySQL,通過(guò) Binlog,異步更新 Redis
  • 對(duì)于異地容災(zāi)、數(shù)據(jù)匯總等,建議會(huì)用這種方式,比如 binlog + kafka,數(shù)據(jù)的一致性也可以達(dá)到秒級(jí);
  • 純粹的高并發(fā)場(chǎng)景,不建議用這種方案,比如搶購(gòu)、秒殺等。

個(gè)人結(jié)論:

  • 實(shí)時(shí)一致性方案:采用“先寫(xiě) MySQL,再刪除 Redis”的策略,這種情況雖然也會(huì)存在兩者不一致,但是需要滿(mǎn)足的條件有點(diǎn)苛刻,所以是滿(mǎn)足實(shí)時(shí)性條件下,能盡量滿(mǎn)足一致性的最優(yōu)解。
  • 最終一致性方案:采用“先寫(xiě) MySQL,通過(guò) Binlog,異步更新 Redis”,可以通過(guò) Binlog,結(jié)合消息隊(duì)列異步更新 Redis,是最終一致性的最優(yōu)解。

項(xiàng)目實(shí)戰(zhàn)

數(shù)據(jù)更新

因?yàn)轫?xiàng)目對(duì)實(shí)時(shí)性要求高,所以采用方案 5,先寫(xiě) MySQL,再刪除 Redis 的方式。

下面只是一個(gè)示例,我們將文章的標(biāo)簽放入 MySQL 之后,再刪除 Redis,所有涉及到 DB 更新的操作都需要按照這種方式處理。

這里加了一個(gè)事務(wù),如果 Redis 刪除失敗,MySQL 的更新操作也需要回滾,避免查詢(xún)時(shí)讀取到臟數(shù)據(jù)。

@Override
@Transactional(rollbackFor = Exception.class)
public void saveTag(TagReq tagReq) {
    TagDO tagDO = ArticleConverter.toDO(tagReq);

    // 先寫(xiě) MySQL
    if (NumUtil.nullOrZero(tagReq.getTagId())) {
        tagDao.save(tagDO);
    } else {
        tagDO.setId(tagReq.getTagId());
        tagDao.updateById(tagDO);
    }

    // 再刪除 Redis
    String redisKey = CACHE_TAG_PRE + tagDO.getId();
    RedisClient.del(redisKey);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void deleteTag(Integer tagId) {
    TagDO tagDO = tagDao.getById(tagId);
    if (tagDO != null){
        // 先寫(xiě) MySQL
        tagDao.removeById(tagId);

        // 再刪除 Redis
        String redisKey = CACHE_TAG_PRE + tagDO.getId();
        RedisClient.del(redisKey);
    }
}

@Override
public void operateTag(Integer tagId, Integer pushStatus) {
    TagDO tagDO = tagDao.getById(tagId);
    if (tagDO != null){

        // 先寫(xiě) MySQL
        tagDO.setStatus(pushStatus);
        tagDao.updateById(tagDO);

        // 再刪除 Redis
        String redisKey = CACHE_TAG_PRE + tagDO.getId();
        RedisClient.del(redisKey);
    }
}

數(shù)據(jù)獲取

這個(gè)也很簡(jiǎn)單,先查詢(xún)緩存,如果有就直接返回;如果未查詢(xún)到,需要先查詢(xún) DB ,再寫(xiě)入緩存。

我們放入緩存時(shí),加了一個(gè)過(guò)期時(shí)間,用于兜底,萬(wàn)一兩者不一致,緩存過(guò)期后,數(shù)據(jù)會(huì)重新更新到緩存。

@Override
public TagDTO getTagById(Long tagId) {

    String redisKey = CACHE_TAG_PRE + tagId;

    // 先查詢(xún)緩存,如果有就直接返回
    String tagInfoStr = RedisClient.getStr(redisKey);
    if (tagInfoStr != null && !tagInfoStr.isEmpty()) {
        return JsonUtil.toObj(tagInfoStr, TagDTO.class);
    }

    // 如果未查詢(xún)到,需要先查詢(xún) DB ,再寫(xiě)入緩存
    TagDTO tagDTO = tagDao.selectById(tagId);
    tagInfoStr = JsonUtil.toStr(tagDTO);
    RedisClient.setStrWithExpire(redisKey, tagInfoStr, CACHE_TAG_EXPRIE_TIME);

    return tagDTO;
}

測(cè)試用例

/**
 * @author Louzai
 * @date 2023/5/5
 */
@Slf4j
public class MysqlRedisService extends BasicTest {

    @Autowired
    private TagSettingService tagSettingService;

    @Test
    public void save() {
        TagReq tagReq = new TagReq();
        tagReq.setTag("Java");
        tagReq.setTagId(1L);
        tagSettingService.saveTag(tagReq);
        log.info("save success:{}", tagReq);
    }

    @Test
    public void query() {
        TagDTO tagDTO = tagSettingService.getTagById(1L);
        log.info("query tagInfo:{}", tagDTO);
    }
}

我們看一下 Redis:

127.0.0.1:6379> get pai_cache_tag_pre_1
"{\"tagId\":1,\"tag\":\"Java\",\"status\":1,\"selected\":null}"

以及結(jié)果輸出:

圖片

后記

這篇文章很基礎(chǔ),也非常適用,大家可以直接下載技術(shù)派項(xiàng)目,里面都有代碼和測(cè)試用例,代碼倉(cāng)庫(kù)詳見(jiàn):https://github.com/itwanger/paicoding。

后面我會(huì)把 RabbitMQ、ES、Nacos、MongoDB 和 prometheus 都集成到技術(shù)派項(xiàng)目,不為其它的,存粹為了自?shī)首詷?lè)。

責(zé)任編輯:武曉燕 來(lái)源: 樓仔
相關(guān)推薦

2022-12-14 08:23:30

2021-06-06 12:45:41

分布式CAPBASE

2020-11-02 07:09:24

緩存服務(wù)器異構(gòu)

2010-05-24 11:35:11

WCDMA

2023-06-07 08:10:29

2020-05-12 10:43:22

Redis緩存數(shù)據(jù)庫(kù)

2023-06-29 08:00:59

redis數(shù)據(jù)MySQL

2020-06-01 22:09:48

緩存緩存同步緩存誤用

2022-09-06 15:30:20

緩存一致性

2025-02-04 15:48:21

悲觀鎖數(shù)據(jù)庫(kù)應(yīng)用

2017-07-25 14:38:56

數(shù)據(jù)庫(kù)一致性非鎖定讀一致性鎖定讀

2019-09-20 21:50:47

數(shù)據(jù)庫(kù)緩存

2024-12-26 15:01:29

2019-03-27 13:56:39

緩存雪崩穿透

2021-12-01 08:26:27

數(shù)據(jù)庫(kù)緩存技術(shù)

2023-11-01 10:11:00

Java分布式

2024-05-28 00:50:00

RedisMySQL緩存

2024-10-28 12:41:25

2022-03-29 10:39:10

緩存數(shù)據(jù)庫(kù)數(shù)據(jù)

2020-10-26 19:25:23

CPU緩存Cache
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)