使用Redis類庫處理一般的搶購(秒殺)活動示例
阿新 • • 發佈:2018-11-13
1、建立搶購活動Redis類庫檔案
<?php /** * Created by PhpStorm. */ namespace app\base\service; use mikkle\tp_redis\RedisHashInfoBase; use think\Exception; class ScheduleDetail extends RedisHashInfoBase { protected $table="gopar_schedule_detail"; //資料表的 protected $pk = "id"; //資料表的主鍵 publicfunction _initialize() { //判斷資料存在 並設定檢查週期10分鐘 if (!$this->checkLock("dataExists") && !$this->checkTableDataExists()){ throw new Exception("相關產品資料不存在"); }else{ //設定檢查鎖10分鐘 $this->setLock("dataExists",600); } //如果資料不存在 初始化讀取資料if (!$this->checkExists()){ $this->initTableData(); } } public function getScheduleCenter() { return Schedule::instance( $this->getInfoFieldValue("schedule_id")); } public function __destruct() { //設定15天自動回收redis $this->setExpire((int)$this->getScheduleCenter()->getInfoFieldValue("end_time")+3600*24*15); } }
2、在服務層或者控制器處理搶購邏輯
public function index($data=["user_id"=>1,"ticket_detail_id"=>1,"buy_num"=>1]){ try { //檢測資料存在 if (!$this->checkArrayValueEmpty($data,["user_id","ticket_detail_id","buy_num"])){ throw new Exception($this->error); } $user_id= $data["user_id"] ; //使用者Id $ticket_detail_id = $data["ticket_detail_id"] ; //產品Id $buy_num = $data["buy_num"] ; //購買數量 $infoCenter= ScheduleDetail::instance( $ticket_detail_id ); $scheduleDetailInfo =$infoCenter->getInfoList(); //修改資料庫後 需要執行initTableData()方法重新初始化 推薦寫到Hook裡 // $infoCenter->initTableData(); if ( $infoCenter->getInfoFieldValue( "hot_schedule")){ //熱門搶購隨機過濾隨機過濾 if (!in_array(rand(100, 200) % 11, [1, 3, 5, 7, 9])) { throw new Exception("搶票人數眾多 ,你被擠出搶購隊伍,還有餘票,請重新再搶"); }; } // 這裡判斷 購買數量和銷售日期 不符合就 throw new Exception if (!true){ throw new Exception("這裡寫不符合原因"); } if (((int)$infoCenter->getInfoFieldValue("{$user_id}_num")+$buy_num)>$scheduleDetailInfo["limit_num"] ){ throw new Exception("你超過最大購買數量"); } if ($infoCenter->setInfoFieldIncre("pay_num",$buy_num) >$scheduleDetailInfo["limit_num"] ){ // $infoCenter->setInfoFieldIncre("pay_num", -$buy_num); throw new Exception("對不起,票已經賣光了!"); } //這裡寫主邏輯 啟用事務功能建立訂單 //事務參見下節原始碼 //升級已銷售數量 $infoCenter->updateTableData(["pay_num"]); //在這裡推薦埋鉤子 處理訂單完成的後續事情 //返回結果 } catch (Exception $e) { Log::error($e->getMessage()); return ShowCode::jsonCodeWithoutData(1008, $e->getMessage()); } } }
3.定時佇列判斷訂單是否處理完成 校準剩餘庫存
<?php /** * Created by PhpStorm. */ namespace mikkle\tp_worker; use mikkle\tp_master\Exception; use mikkle\tp_master\Log; /** * title 定時佇列類 * Class TimingWorkerBase * @package mikkle\tp_worker * 建立定時佇列類並繼承使用方法 * class Test extends TimingWorkerBase * { * protected function runHandle($data) * { * Log::notice( "測試".RandNumCenter::getTimeString() ); * } * } * * 新增方法定時佇列方法 * \app\worker\Test::add(["name"=>"mikkle",],30); */ abstract class TimingWorkerBase extends WorkerBase { protected $listName; protected $listData; protected $listNum; protected $lockName; public function _initialize($options = []) { $this->listData = "{$this->listName}_data"; $this->listNum = "{$this->listName}_num"; } /** * * 快速定時任務 * * 當命令列未執行 直接執行 * description add * @param $data * @param $runTime * @param array $options * @param string $handleName * @return bool */ static public function add($data, $runTime = 0, $handleName = "run", $options = []) { try { $data = json_encode($data); $instance = static::instance($options); switch (true) { case (self::checkCommandRun()): $time = $instance->getRunTime($runTime); $num = $instance->redis()->incre($instance->listNum); Log::notice("添加了 $num 號定時任務"); $instance->redis()->zAdd($instance->listName, [$time => $num]); $instance->redis()->hSet($instance->listData, $num, $data); Log::notice("Timing Command service start work!!"); $instance->runWorker($handleName); break; default: Log::notice("Timing Command service No away!!"); $instance->runHandle($data); } return true; } catch (Exception $e) { Log::error($e->getMessage()); return false; } } /** * 命令列執行的方法 */ static public function run() { try { $i = 0; $instance = static::instance(); //讀取並刪除定時任務 $workList = $instance->redis()->zRangByScore($instance->listName, 0, time()); $instance->redis()->zDelete($instance->listName, $workList); //剩餘任務數 $re = $instance->redis()->zCard($instance->listName); if ( $workList ){ foreach ($workList as $num) { try { $redisData = $instance->redis()->hGet($instance->listData, $num); if ($redisData) { $data = json_decode($redisData, true); $result = $instance->runHandle($data); Log::notice("執行{$num}編號任務"); if ($instance->saveLog) { $instance->saveRunLog($result, $data); } $instance->redis()->hDel($instance->listData, $num); } } catch (Exception $e) { Log::error($e->getMessage()); $instance->redis()->zAdd($instance->listData, [(time() + 300) => $num]); } $i++; sleep(1); } } if ( $re=== 0) { $instance->clearWorker(); } echo "執行了{$i}次任務,剩餘未執行任務[{$re}]項" . PHP_EOL; Log::notice("執行了{$i}次任務,剩餘未執行任務[{$re}]項"); } catch (Exception $e) { //Log::error($e); Log::error($e->getMessage()); echo($e->getMessage()); } } protected function getRunTime($time = 0) { $now = time(); switch (true) { case ($time == 0): return $now; break; case (is_int($time) && 30 * 3600 * 24 > $time): return $now + $time; break; case (is_int($time) && $now < $time): return $time; break; default: return $now + (int)$time; } } }