データの一貫性を維持するためにRedisを発行します(読み書きを避ける)


「読んでから書く」


通常、読み取り後書き込みは、同じデータに対するポインタの読み取り後書き込みであり、書き込みの値は読み取りの値に依存すると言います。

この定義について2つの部分に分解すると、1つは同じデータです.二:書くことは読むことに依存する.(この分割を覚えておいて、後で使用します.定義1、定義2と書きます)この2つの部分が成立したときだけ、読んでから書く問題が発生します.
プロジェクトでは,より多くの同時性に直面した場合,redisを用いて読み書き操作を行うことは非常に問題になりやすく,プログラムにロバスト性を持たず,bugが安定して再現しにくいことが多い(得られた値は同時数に関係することが多い).栗を挙げます:A、Bの2つのプロセスが存在して、同時に次のコードを操作します:
$objRedis = new Redis();
//  key
$intNum   = $objRedis->get('key');
if ($intNum == 1) {
    //  key   1,  key 1
    $bolRet   = $objRedis->incr('key');

    //do something...
}
  • Aプロセスがkeyに先にgetされた場合、keyの値は1である.
  • 同時に、Bプロセスもkeyにgetされ、同じkey値は1である.
  • Bプロセスの実行が速く、if判断を先に行い、条件を満たすことを発見し、keyに対して累積操作を行い、この時keyは2になった.
  • AプロセスはBプロセスに対してkeyを修正したという操作は漠然としていたので、if判定条件まで運転を続けるとgetのkeyが1であるため、条件も満たされ、Aプロセスもkeyを累加操作するが、keyはBによって累加された(keyの値はすでに2)ため、Aが再累加されるとkeyは最終的に3になる.

  • 実際、コードの本意はkeyが1の場合にいくつかの操作を実行することを望んでいるが、同時発生すると、このコードは期待を満たすことが難しい.このようなコードが抽選や秒殺などの活動に登場すれば、会社が個人に損失を負わせないことを期待するしかない(汗).以上は比較的簡単な読み書きの問題です.
    このコードは、特にkeyの値自体が意味がない場合、よく解決されます.
    $objRedis = new Redis();
    //  key
    $intNum   = $objRedis->incr('key');
    if ($intNum == 1) {
        //do something...
    }

    以上のコードはincr原子型操作を用い,同時(ロックに相当)を制限しているので,上記の問題は発生しない.
    しかし、もしこのkeyが意味があるなら、勝手に変えることはできません.このような状況はどうすればいいですか.

    詳細な説明


    次に私はもっと具体的な例を挙げて、それからこの例からいくつかのレンガ(個人が考えている解決方法)を投げて、もっと多くの玉を引き出したいと思っています.
    例は次のとおりです.ユーザーの連続参加日数に基づいて賞を授与するアクティビティがあります.ルールは次のとおりです.
  • は1-3日連続で参加し、毎日10金貨を追加奨励した.
  • は4-7日連続で参加し、毎日50金貨を追加奨励した.
  • は8-15日連続で参加し、毎日100金貨を追加奨励した.
  • は15日以上連続で参加し、毎日200金貨を追加奨励した.

  • 簡単な考え方(読んでから書く):
    各ユーザにはhashストレージが使用され、1つのフィールドは連続日数(「sequence」)を表し、もう1つのフィールドは最近の参加日(「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       )...

    明らかに、これも読み取り後に書く方法です.最近の参加日を取得し、条件に基づいて最近の参加日を変更します(定義は2つとも満たされています).この方法は、高い同時性の場合、連続日数のエラーが累積する可能性があります.
    では、この例はどのようにして読んで書くのを避けるのでしょうか.方法はいろいろありますが、ここでは2つ挙げます.
    方法1:
    定義1または2を成立させないことにより、読み書きの問題は存在しない。

    日付ごとに格納--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を成立させないとも考えられる).
    方法2:
    同時実行を制限します。

    方法1は定義1または2を成立させないことで,読後書きの問題を解決する.ここでは定義の1つまたは2つの上で文章を作るのではなく、次の考えを変えます.読んでから書くのは結局同時で問題が発生します.そこでここでは、釜底の昇給方法を紹介し、同時昇給を制限します!制限同時といえば、最初の反応はロックかもしれませんが、自分でコードにロックをかけるのはもちろん方法ですが、相対的にコストが高いです(どのようにロックをかけるかは私の前のブログ「redisで悲観的なロックを実現する」を参考にすることができます).ここではこれ以上説明しません.実は読んでから書くのは、最も基本的で最も簡単な分割方法は--読むことと書くことで、それでは釜底の給料を引き上げる方法は読まないことができて、書くだけです!実装の構想は,連続日数+現在の日付を1つの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のトランザクションは本当に使いにくい).ここでは2つのredisが読後の問題を克服する考え方を投げ出して、玉を引く役割を果たすことを望んでいます!
    レベルが限られているので、指摘を歓迎します~転送する必要がある場合は、出典を明記してください、thx~