天天看點

PHP使用Redis悲觀鎖簡單實作每日簽到功能,防止并發資料重複

網上的簽到大部分都很複雜表示有的看不懂,直接用Mysql也是可以做,但是每次查詢很消耗記憶體,還有很多的并發問題,是以想到利用Redis的緩存時間來做

提到悲觀鎖,先通過網上給出的一個比較形象的比喻

拿健身房比喻,門口挂着把鑰匙(隻有一把),想進去的人必須拿到這把鑰匙才行,拿到鑰匙的人可以進入,不管是熱身、喝水還是跑步都可以,直到他出來把鑰匙挂回牆上,下一個才能去争取,拿到的才可以再進去。

聽着好像有點不人性化,是以悲觀鎖比較适合強一緻性的場景,但效率比較低,特别是讀的并發低。樂觀鎖則适用于讀多寫少,并發沖突少的場景。

實作要點和思路

1、一個任務在同一時間段内隻能被一個使用者所持有;

2、避免出現死任務,即避免任務被使用者長時間占有,無法釋放。

設定一個鎖的key,setnx是原子操作,隻能一個程序寫入成功,寫入成功傳回true(表示擷取鎖權限),然後寫入内容立即釋放鎖即删除鎖key。如果隻用SETNX指令設定鎖的話,如果當持有鎖的程序崩潰或删除鎖失敗時,其他程序将無法擷取到鎖,問題就大了。擷取不到鎖的程序去判斷鎖的剩餘有效時間,如果為-1,那麼表示沒有設定過期時間,則設定鎖的有效時間為5秒(預留5秒給拿到鎖的程序處理時間,足夠多了),傳回true,等待鎖删除。

//每日簽到
public function sign_in(){
    $this->load->model('user_model');
    $this->load->model('account_log_model');
    $this->load->model('config_model');
    $user_id = $this->_getId();
    $change_desc="每日簽到送積分";
    //擷取今天結束的時間   23:59:59
    $endTime=mktime(23,59,59,date('m'),date('d'),date('y'));
    //擷取現在簽到的時間
    $starTima=time();
    //查詢是否簽到
    include APPPATH.'config/load_redis.php';
    //設定DB 2 為簽到資料庫
    $redis->select(2);
    $lock_key= "LOCK_PREFIX".$user_id;
    $redis_user_locak = $redis->setnx($lock_key,1);  //設定鎖,隻是鎖定的某個會員,其他人不受影響
    if($redis_user_locak){  //擷取鎖權限成功
        $redis_data = $redis->get($user_id);
        //如果redis沒資料,則進行簽到
        if(empty($redis_data)){
            $config_info = $this->config_model->getConfigInfo(array('code' => 'login_points'));
            if ($config_info['value'] > 0) {
                $logResult=$this->account_log_model->log_account_change($user_id, 0, 0, 0, $config_info['value'], $change_desc,Account_log_model::ACT_SIGN_INTEGRAL);
            }
            if($logResult){
                //簽到成功計入redis 并且計算過期時間   今天結束的時間減去現在時間
                $expiration_time = $endTime - $starTima;
                $redis->set($user_id,json_encode(array('user_id'=>$user_id,'add_time'=>$starTima)),$expiration_time);
                $redis->del($lock_key);   //釋放鎖
                //寫入資料庫
                $this->db->query("insert into ecs_user_sign_in (user_id,sign_time,pay_points) values('$user_id','".date("Y-m-d H:i:s",time())."','".$config_info['value']."')");
                $this->_tojson(200, '簽到成功');
            }
        }else{
            if($redis->ttl($lock_key) == -1){  //防止死鎖
                $redis->expire($lock_key,3);
            }
            $this->_tojson(400, '該使用者已經簽到');
        }
    }else{
        if($redis->ttl($lock_key) == -1){  //防止死鎖
            $redis->expire($lock_key,3);
        }
        $this->_tojson(400, '請勿重複簽到!');
    }

}
           

每天使用者簽到之後,Redis 存貯使用者ID 為下标的字元串類型,過期時間設定為 每天結束的時間戳和現在簽到的時間戳的差

Redis配置檔案

<?php
		$redis = new Redis();
		//連接配接
		$redis->connect('127.0.0.1', 6379);