欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

高并發(fā)下Redis如何保持?jǐn)?shù)據(jù)一致性(避免讀后寫(xiě))

 更新時(shí)間:2022年03月18日 10:03:39   作者:grey256  
本文主要介紹了高并發(fā)下Redis如何保持?jǐn)?shù)據(jù)一致性(避免讀后寫(xiě)),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下

“讀后寫(xiě)”

通常意義上我們說(shuō)讀后寫(xiě)是指針對(duì)同一個(gè)數(shù)據(jù)的先讀后寫(xiě),且寫(xiě)入的值依賴于讀取的值。

關(guān)于這個(gè)定義要拆成兩部分來(lái)看,一:同一個(gè)數(shù)據(jù);二:寫(xiě)依賴于讀。(記住這個(gè)拆分,后續(xù)會(huì)用到,記為定義一、定義二)只有當(dāng)這兩部分都成立時(shí),讀后寫(xiě)的問(wèn)題才會(huì)出現(xiàn)。

在項(xiàng)目中,當(dāng)面對(duì)較多的并發(fā)時(shí),使用redis進(jìn)行讀后寫(xiě)操作,是非常容易出問(wèn)題的,常常使得程序不具備魯棒性,bug很難穩(wěn)定復(fù)現(xiàn)(得到的值往往跟并發(fā)數(shù)有關(guān))。

舉個(gè)栗子:

存在A、B兩個(gè)進(jìn)程,同時(shí)操作下面這段代碼:

$objRedis = new Redis();
//獲取key
$intNum ? = $objRedis->get('key');
if ($intNum == 1) {
? ? //如果key的值為1,則給key加1
? ? $bolRet ? = $objRedis->incr('key');

? ? //do something...
}
  • 如果A進(jìn)程先get到了key,而此時(shí)key的值為1;
  • 同時(shí),B進(jìn)程此時(shí)也get到了key,同樣key值為1;
  • B進(jìn)程運(yùn)行的快,先進(jìn)行了if判斷,發(fā)現(xiàn)滿足條件,于是對(duì)key進(jìn)行了累加操作,此時(shí)key變成了2;
  • A進(jìn)程對(duì)B進(jìn)程修改了key這個(gè)操作茫然無(wú)知,所以當(dāng)它繼續(xù)運(yùn)行走到if判斷條件時(shí),由于它get的key是1,因此也滿足條件,于是A進(jìn)程也會(huì)對(duì)key進(jìn)行累加操作,但是由于key已經(jīng)被B進(jìn)行累加過(guò)一次(key的值已經(jīng)是2),因此當(dāng)A再累加,key最終就變成了3。

實(shí)際上,代碼的本意是希望key為1時(shí)執(zhí)行一些操作,但當(dāng)出現(xiàn)并發(fā)的時(shí)候,這段代碼很難滿足期望!
如果這樣的代碼出現(xiàn)在抽獎(jiǎng)、秒殺等活動(dòng)中,那就只能期望公司不會(huì)讓個(gè)人承擔(dān)損失了(汗)。
以上就是一個(gè)比較簡(jiǎn)單的讀后寫(xiě)的問(wèn)題。

對(duì)于這段代碼其實(shí)很好解決,尤其是如果key的值本身沒(méi)有意義的時(shí)候:

$objRedis = new Redis();
//獲取key
$intNum   = $objRedis->incr('key');
if ($intNum == 1) {
    //do something...
}

以上代碼使用了incr原子型操作,限制了并發(fā)(相當(dāng)于加鎖),就不會(huì)出現(xiàn)上述問(wèn)題了。

但是,如果這個(gè)key如果是有意義的呢,那就不能隨意改變,這種情況我們?cè)撛趺崔k?

詳細(xì)說(shuō)明

下面我舉一個(gè)更具體的例子,然后從這個(gè)例子出發(fā)來(lái)拋幾塊磚(個(gè)人想的解決辦法),希望引出更多的玉。

例子如下:
有一個(gè)活動(dòng),需要根據(jù)用戶連續(xù)參與天數(shù)進(jìn)行發(fā)獎(jiǎng),規(guī)則如下:

  • 連續(xù)參與1-3天,每天額外獎(jiǎng)勵(lì)10金幣;
  • 連續(xù)參加4-7天,每天額外獎(jiǎng)勵(lì)50金幣;
  • 連續(xù)參加8-15天,每天額外獎(jiǎng)勵(lì)100金幣;
  • 連續(xù)參加15天以上,每天額外獎(jiǎng)勵(lì)200金幣;

簡(jiǎn)單思路(使用讀后寫(xiě)):

對(duì)每個(gè)用戶使用一個(gè)hash存儲(chǔ),其中一個(gè)字段表示連續(xù)天數(shù)(‘sequence’),另一個(gè)字段存儲(chǔ)最近參與日期(‘lastdate’)。
精簡(jiǎn)版代碼如下:

$objRedis = new Redis();
//根據(jù)用戶ID,生成redis的key
$strRedisKey = 'activity_' . $intUid;
//從Hash中獲取最近參與時(shí)間
$mixDate ? ? = $objRedis->HGET($strRedisKey, 'lastdate');

$intLastDate ?= intval($mixDate);
$intYesterDay = intval(date("Ymd", strtotime("-1 day")));
$intCurrDate ?= intval(date('Ymd'));
$intNum ? ? ? = 0;//連續(xù)天數(shù)
if ($intCurrDate == $intLastDate) {
? ? //今天已經(jīng)參與過(guò),直接跳過(guò)
? ? return;
} elseif ($intLastDate == $intYesterDay) {
? ? //日期連續(xù),增加連續(xù)天數(shù)
? ? $intNum = $objRedis->HINCRBY($strRedisKey, 'sequence', 1);
? ? if ($intNum > 0) {
? ? ? ? //將最近參與時(shí)間設(shè)置為當(dāng)天
? ? ? ? $objRedis->HSET($strRedisKey, 'lastdate', $intCurrDate);
? ? }
} else {
? ? //日期不連續(xù),設(shè)置連續(xù)天數(shù)為1,最近參與時(shí)間為當(dāng)天
? ? $intNum = 1;
? ? $objRedis->HMSET($strRedisKey, 'sequence', $intNum, 'lastdate', $intCurrDate);
}

//do something(根據(jù)$intNum發(fā)放金幣等操作)...

 很明顯,這也是一個(gè)讀后寫(xiě)的方法——先獲取最近參與日期,再根據(jù)條件修改最近參與日期(定義一二都被滿足了),這個(gè)方法在高并發(fā)的時(shí)候很有可能會(huì)導(dǎo)致連續(xù)天數(shù)的錯(cuò)誤累加。

那么,這個(gè)例子如何避免讀后寫(xiě)呢?
方法其實(shí)有很多,這里先舉兩個(gè):

方法1:

通過(guò)使定義一或二不成立,從而使得讀后寫(xiě)的問(wèn)題不存在。

按日期進(jìn)行存儲(chǔ)——將redis的key按日期進(jìn)行劃分,比如用戶ID為123的key從redis_123變?yōu)閞edis_123_20171225。這樣的話,其實(shí)相當(dāng)于避免了讀寫(xiě)同一份數(shù)據(jù)。
代碼如下:

$objRedis = new Redis();
//根據(jù)用戶ID,生成redis的key
$strCurrRedisKey = 'activity_' . $intUid . '_' . date('Ymd');
//從Hash中獲取最近參與時(shí)間
$mixNum ? ? ? ? ?= $objRedis->GET($strCurrRedisKey);

$intNum = 0;//連續(xù)天數(shù)
if (is_null($mixNum)) {
? ? //當(dāng)天還沒(méi)被處理過(guò),查找前一天的記錄
? ? $strLastRedisKey = 'activity_' . $intUid . '_' . intval(date("Ymd", strtotime("-1 day")));
? ? $mixLastNum ? ? ?= $objRedis->GET($strLastRedisKey);
? ? //計(jì)算連續(xù)天數(shù)
? ? $intNum = intval($mixLastNum) + 1;
? ? //設(shè)置當(dāng)天的連續(xù)天數(shù),并給這個(gè)key一周的過(guò)期時(shí)間
? ? $objRedis->SETEX($strCurrRedisKey, 604800, $intNum);
} else {
? ? //今天已經(jīng)操作了,直接返回
? ? return;
}

//do something(根據(jù)$intNum發(fā)放金幣等操作)...

這個(gè)思路是通過(guò)讀昨天的數(shù)據(jù)后修改今天的數(shù)據(jù),來(lái)達(dá)到避免對(duì)同一份數(shù)據(jù)讀后寫(xiě)的目的的(使得定義一不成立,從而消除讀后寫(xiě)的問(wèn)題)。
這里雖然在最開(kāi)始的時(shí)候也讀取了今天的數(shù)據(jù),但由于最后對(duì)今天的數(shù)據(jù)的修改只依賴于昨天的數(shù)據(jù)(今天的數(shù)據(jù)=昨天數(shù)據(jù)+1),而不依賴于讀到的今天的數(shù)據(jù),所以也就沒(méi)有讀后寫(xiě)的問(wèn)題了(所以也可以看作是使定義二不成立)。

方法2:

限制并發(fā)。

方法一是使定義一或二不成立,從而解決讀后寫(xiě)的問(wèn)題。這里就不再在定義一或二上做文章了,下面換一個(gè)思路。
讀后寫(xiě)歸根結(jié)底其實(shí)還是并發(fā)下才會(huì)出現(xiàn)問(wèn)題。因此這里介紹一個(gè)釜底抽薪的方法,限制并發(fā)!
一說(shuō)到限制并發(fā),可能第一反應(yīng)就是加鎖,自己在代碼中加鎖當(dāng)然是一種辦法,但是相對(duì)來(lái)說(shuō)成本還是高一些(如何加鎖可以參考我之前的一篇博文《用redis實(shí)現(xiàn)悲觀鎖》),這里就不再贅述。
其實(shí)讀后寫(xiě),最基本也是最簡(jiǎn)單的拆分方式是——讀和寫(xiě),那么釜底抽薪的辦法就是能不能不讀,只寫(xiě)!
實(shí)現(xiàn)思路就是只用一個(gè)key來(lái)存儲(chǔ)連續(xù)天數(shù)+當(dāng)前日期,然后使用原子型操作來(lái)寫(xiě)。一說(shuō)到原子型操作,在redis中第一反應(yīng)就是incr。那么順著這個(gè)思路,我們?cè)趺蠢胕ncr來(lái)操作呢?
其實(shí)關(guān)鍵是設(shè)計(jì)一個(gè)存儲(chǔ)方式,滿足既能存放連續(xù)天數(shù),又能存放當(dāng)前日期,還能使得這個(gè)值多次incr而不影響本身數(shù)據(jù)。這里說(shuō)下我的設(shè)計(jì)方法:將一個(gè)12位的整數(shù)值看作是一個(gè)分段有意義的值,連續(xù)天數(shù)用最高的2位表示(因業(yè)務(wù)自定義),中間8位代表日期(如20171225),最后2位用于計(jì)數(shù)(無(wú)實(shí)際意義),比如:

將012017122523拆分成:
01|20171225|23
分別代表:連續(xù)天數(shù)|最近參與日期|計(jì)數(shù)

其中計(jì)數(shù),這個(gè)字段是為了在利用incr時(shí)限制并發(fā)的。
示意代碼如下:

$objRedis ? ?= new Redis();
//根據(jù)用戶ID,生成redis的key
$strRedisKey = 'activity_' . $intUid;
//從Hash中獲取最近參與時(shí)間
$intVal ? ? ? = intval($objRedis->INCR($strRedisKey));
$intCnt ? ? ? = $intVal % 100;//獲取計(jì)數(shù)
$intLastDate ?= ($intVal - $intCnt) % 100000000;//獲取最近參與日期
$intNum ? ? ? = intval($intVal / 10000000000);//連續(xù)天數(shù)
$intYesterDay = intval(date("Ymd", strtotime("-1 day")));//昨天的日期
$intCurrDate ?= intval(date('Ymd'));//今天的日期

if ($intCurrDate == $intLastDate) {
? ? //今天已經(jīng)操作了
? ? if ($intCnt > 90) {
? ? ? ? //重置計(jì)數(shù),防止超過(guò)給定范圍(最大99)
? ? ? ? $objRedis->SET($strRedisKey, $intNum * 10000000000 + $intCurrDate * 100 + 1);
? ? }
? ? return;
} elseif ($intYesterDay == $intLastDate) {
? ? //日期連續(xù),計(jì)算連續(xù)天數(shù)
? ? $intNum += 1;
} else {
? ? //日期不連續(xù),重置連續(xù)天數(shù)
? ? $intNum = 1;
}
//更新連續(xù)天數(shù)及當(dāng)前日期
$objRedis->SET($strRedisKey, $intNum * 10000000000 + $intCurrDate * 100 + 1);

//do something(根據(jù)$intNum發(fā)放金幣等操作)...

只要涉及到數(shù)據(jù)讀、寫(xiě),就會(huì)有數(shù)據(jù)一致性問(wèn)題,mysql中可以通過(guò)事務(wù)、鎖(FOR UPDATE)等來(lái)保證一致性,而redis也可以根據(jù)業(yè)務(wù)需求設(shè)計(jì)不同的讀寫(xiě)方式來(lái)實(shí)現(xiàn)(redis的事務(wù)真心不太好用)。這里拋出兩種redis克服讀后寫(xiě)問(wèn)題的思路,希望能起到引玉的作用!

到此這篇關(guān)于高并發(fā)下Redis如何保持?jǐn)?shù)據(jù)一致性(避免讀后寫(xiě))的文章就介紹到這了,更多相關(guān)Redis 數(shù)據(jù)一致性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis過(guò)期鍵與內(nèi)存淘汰策略深入分析講解

    Redis過(guò)期鍵與內(nèi)存淘汰策略深入分析講解

    因?yàn)閞edis數(shù)據(jù)是基于內(nèi)存的,然而內(nèi)存是非常寶貴的資源,然后我們就會(huì)對(duì)一些不常用或者只用一次的數(shù)據(jù)進(jìn)行存活時(shí)間設(shè)置,這樣才能提高內(nèi)存的使用效率,下面這篇文章主要給大家介紹了關(guān)于Redis中過(guò)期鍵與內(nèi)存淘汰策略,需要的朋友可以參考下
    2022-11-11
  • Centos 7 如何安裝Redis(推薦)

    Centos 7 如何安裝Redis(推薦)

    這篇文章主要介紹了Centos 7 如何安裝Redis,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-12-12
  • CentOS 6.6下Redis安裝配置記錄

    CentOS 6.6下Redis安裝配置記錄

    這篇文章主要介紹了CentOS 6.6下Redis安裝配置記錄,本文給出了安裝需要的支持環(huán)境、安裝redis、測(cè)試Redis、配置redis等步驟,需要的朋友可以參考下
    2015-03-03
  • Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫(kù)過(guò)程

    Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫(kù)過(guò)程

    大家好,本篇文章主要講的是Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫(kù)過(guò)程,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽
    2021-12-12
  • Redis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)

    Redis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)

    下面小編就為大家?guī)?lái)一篇Redis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-03-03
  • 完美解決linux上啟動(dòng)redis后配置文件未生效的問(wèn)題

    完美解決linux上啟動(dòng)redis后配置文件未生效的問(wèn)題

    今天小編就為大家分享一篇完美解決linux上啟動(dòng)redis后配置文件未生效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-05-05
  • Redis 5.05 單獨(dú)模式安裝及配置方法

    Redis 5.05 單獨(dú)模式安裝及配置方法

    這篇文章主要介紹了Redis 5.05 單獨(dú)模式安裝,文中通過(guò)代碼給大家介紹了Redis 5.0.5 單節(jié)點(diǎn) 安裝配置方法,需要的朋友可以參考下
    2019-10-10
  • Redis核心原理詳細(xì)解說(shuō)

    Redis核心原理詳細(xì)解說(shuō)

    這篇文章主要介紹了Redis核心原理詳細(xì)解說(shuō),redis利用epoll實(shí)現(xiàn)IO多路復(fù)用,將連接信息和事件放到隊(duì)列中,依次放到文件事件分派器,事件分派器將事件分發(fā)給事件處理器
    2022-07-07
  • Redis教程(十一):虛擬內(nèi)存介紹

    Redis教程(十一):虛擬內(nèi)存介紹

    這篇文章主要介紹了Redis教程(十一):虛擬內(nèi)存介紹,本文講解了虛擬內(nèi)存簡(jiǎn)介、應(yīng)用場(chǎng)景和配置方法等內(nèi)容,需要的朋友可以參考下
    2015-04-04
  • Redis過(guò)期數(shù)據(jù)的刪除策略詳解

    Redis過(guò)期數(shù)據(jù)的刪除策略詳解

    Redis 是一個(gè)kv型數(shù)據(jù)庫(kù),我們所有的數(shù)據(jù)都是存放在內(nèi)存中的,但是內(nèi)存是有大小限制的,不可能無(wú)限制的增量,這篇文章主要介紹了Redis過(guò)期數(shù)據(jù)的刪除策略,需要的朋友可以參考下
    2023-08-08

最新評(píng)論