<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
通常意義上我們說讀後寫是指標對同一個資料的先讀後寫,且寫入的值依賴於讀取的值。
關於這個定義要拆成兩部分來看,一:同一個資料;二:寫依賴於讀。(記住這個拆分,後續會用到,記為定義一、定義二)只有當這兩部分都成立時,讀後寫的問題才會出現。
在專案中,當面對較多的並行時,使用redis進行讀後寫操作,是非常容易出問題的,常常使得程式不具備魯棒性,bug很難穩定復現(得到的值往往跟並行數有關)。
舉個栗子:
存在A、B兩個程序,同時操作下面這段程式碼:
$objRedis = new Redis(); //獲取key $intNum = $objRedis->get('key'); if ($intNum == 1) { //如果key的值為1,則給key加1 $bolRet = $objRedis->incr('key'); //do something... }
實際上,程式碼的本意是希望key為1時執行一些操作,但當出現並行的時候,這段程式碼很難滿足期望!
如果這樣的程式碼出現在抽獎、秒殺等活動中,那就只能期望公司不會讓個人承擔損失了(汗)。
以上就是一個比較簡單的讀後寫的問題。
對於這段程式碼其實很好解決,尤其是如果key的值本身沒有意義的時候:
$objRedis = new Redis(); //獲取key $intNum = $objRedis->incr('key'); if ($intNum == 1) { //do something... }
以上程式碼使用了incr原子型操作,限制了並行(相當於加鎖),就不會出現上述問題了。
但是,如果這個key如果是有意義的呢,那就不能隨意改變,這種情況我們該怎麼辦?
下面我舉一個更具體的例子,然後從這個例子出發來拋幾塊磚(個人想的解決辦法),希望引出更多的玉。
例子如下:
有一個活動,需要根據使用者連續參與天數進行發獎,規則如下:
簡單思路(使用讀後寫):
對每個使用者使用一個hash儲存,其中一個欄位表示連續天數(‘sequence’),另一個欄位儲存最近參與日期(‘lastdate’)。
精簡版程式碼如下:
$objRedis = new Redis(); //根據使用者ID,生成redis的key $strRedisKey = 'activity_' . $intUid; //從Hash中獲取最近參與時間 $mixDate = $objRedis->HGET($strRedisKey, 'lastdate'); $intLastDate = intval($mixDate); $intYesterDay = intval(date("Ymd", strtotime("-1 day"))); $intCurrDate = intval(date('Ymd')); $intNum = 0;//連續天數 if ($intCurrDate == $intLastDate) { //今天已經參與過,直接跳過 return; } elseif ($intLastDate == $intYesterDay) { //日期連續,增加連續天數 $intNum = $objRedis->HINCRBY($strRedisKey, 'sequence', 1); if ($intNum > 0) { //將最近參與時間設定為當天 $objRedis->HSET($strRedisKey, 'lastdate', $intCurrDate); } } else { //日期不連續,設定連續天數為1,最近參與時間為當天 $intNum = 1; $objRedis->HMSET($strRedisKey, 'sequence', $intNum, 'lastdate', $intCurrDate); } //do something(根據$intNum發放金幣等操作)...
很明顯,這也是一個讀後寫的方法——先獲取最近參與日期,再根據條件修改最近參與日期(定義一二都被滿足了),這個方法在高並行的時候很有可能會導致連續天數的錯誤累加。
那麼,這個例子如何避免讀後寫呢?
方法其實有很多,這裡先舉兩個:
方法1:
通過使定義一或二不成立,從而使得讀後寫的問題不存在。
按日期進行儲存——將redis的key按日期進行劃分,比如使用者ID為123的key從redis_123變為redis_123_20171225。這樣的話,其實相當於避免了讀寫同一份資料。
程式碼如下:
$objRedis = new Redis(); //根據使用者ID,生成redis的key $strCurrRedisKey = 'activity_' . $intUid . '_' . date('Ymd'); //從Hash中獲取最近參與時間 $mixNum = $objRedis->GET($strCurrRedisKey); $intNum = 0;//連續天數 if (is_null($mixNum)) { //當天還沒被處理過,查詢前一天的記錄 $strLastRedisKey = 'activity_' . $intUid . '_' . intval(date("Ymd", strtotime("-1 day"))); $mixLastNum = $objRedis->GET($strLastRedisKey); //計算連續天數 $intNum = intval($mixLastNum) + 1; //設定當天的連續天數,並給這個key一週的過期時間 $objRedis->SETEX($strCurrRedisKey, 604800, $intNum); } else { //今天已經操作了,直接返回 return; } //do something(根據$intNum發放金幣等操作)...
這個思路是通過讀昨天的資料後修改今天的資料,來達到避免對同一份資料讀後寫的目的的(使得定義一不成立,從而消除讀後寫的問題)。
這裡雖然在最開始的時候也讀取了今天的資料,但由於最後對今天的資料的修改只依賴於昨天的資料(今天的資料=昨天資料+1),而不依賴於讀到的今天的資料,所以也就沒有讀後寫的問題了(所以也可以看作是使定義二不成立)。
方法2:
限制並行。
方法一是使定義一或二不成立,從而解決讀後寫的問題。這裡就不再在定義一或二上做文章了,下面換一個思路。
讀後寫歸根結底其實還是並行下才會出現問題。因此這裡介紹一個釜底抽薪的方法,限制並行!
一說到限制並行,可能第一反應就是加鎖,自己在程式碼中加鎖當然是一種辦法,但是相對來說成本還是高一些(如何加鎖可以參考我之前的一篇博文《用redis實現悲觀鎖》),這裡就不再贅述。
其實讀後寫,最基本也是最簡單的拆分方式是——讀和寫,那麼釜底抽薪的辦法就是能不能不讀,只寫!
實現思路就是隻用一個key來儲存連續天數+當前日期,然後使用原子型操作來寫。一說到原子型操作,在redis中第一反應就是incr。那麼順著這個思路,我們怎麼利用incr來操作呢?
其實關鍵是設計一個儲存方式,滿足既能存放連續天數,又能存放當前日期,還能使得這個值多次incr而不影響本身資料。這裡說下我的設計方法:將一個12位元的整數值看作是一個分段有意義的值,連續天數用最高的2位表示(因業務自定義),中間8位元代表日期(如20171225),最後2位用於計數(無實際意義),比如:
將012017122523拆分成:
01|20171225|23
分別代表:連續天數|最近參與日期|計數
其中計數,這個欄位是為了在利用incr時限制並行的。
示意程式碼如下:
$objRedis = new Redis(); //根據使用者ID,生成redis的key $strRedisKey = 'activity_' . $intUid; //從Hash中獲取最近參與時間 $intVal = intval($objRedis->INCR($strRedisKey)); $intCnt = $intVal % 100;//獲取計數 $intLastDate = ($intVal - $intCnt) % 100000000;//獲取最近參與日期 $intNum = intval($intVal / 10000000000);//連續天數 $intYesterDay = intval(date("Ymd", strtotime("-1 day")));//昨天的日期 $intCurrDate = intval(date('Ymd'));//今天的日期 if ($intCurrDate == $intLastDate) { //今天已經操作了 if ($intCnt > 90) { //重置計數,防止超過給定範圍(最大99) $objRedis->SET($strRedisKey, $intNum * 10000000000 + $intCurrDate * 100 + 1); } return; } elseif ($intYesterDay == $intLastDate) { //日期連續,計算連續天數 $intNum += 1; } else { //日期不連續,重置連續天數 $intNum = 1; } //更新連續天數及當前日期 $objRedis->SET($strRedisKey, $intNum * 10000000000 + $intCurrDate * 100 + 1); //do something(根據$intNum發放金幣等操作)...
只要涉及到資料讀、寫,就會有資料一致性問題,mysql中可以通過事務、鎖(FOR UPDATE)等來保證一致性,而redis也可以根據業務需求設計不同的讀寫方式來實現(redis的事務真心不太好用)。這裡丟擲兩種redis克服讀後寫問題的思路,希望能起到引玉的作用!
到此這篇關於高並行下Redis如何保持資料一致性(避免讀後寫)的文章就介紹到這了,更多相關Redis 資料一致性內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45