1. 程式人生 > 其它 >Redis練習-模擬一個搶紅包系統

Redis練習-模擬一個搶紅包系統

技術標籤:Redisredis資料庫nosqlmysql併發

文章目錄

Redis練習-模擬一個搶紅包系統

前面學習過 Redis 的一些基礎知識,為了達到學以致用的目的,加深和鞏固對 Redis 的理解,這篇文章模擬微信搶紅包,設計一個簡單的搶紅包系統。

1.需求描述

  • 應用場景描述
  • 假設某微信群有:10000
  • 某土豪發紅包: 1000
  • 紅包個數: 10
  • 分配規則:發 1000 元,隨機分成 10 個紅包
  • 紅包過期時間 24 小時
  • 新建紅包規則
    自定義紅包個數範圍 1~100 個 ,金額範圍 0.01~1000 元,紅包留言備註字元自定義長度 0~25

2.新建紅包

  • 確定新建紅包表字段
欄位名稱型別和長度含義
idint(11)自增主鍵
uuidvarchar(100)UUID
total_amountint(11)紅包總金額,單位分
total_packetsmallint(5)紅包總個數
residue_amountint(11)剩餘紅包總金額,單位分
residue_packetsmallint(5)剩餘紅包個數
user_idbigint(20)使用者ID
remarkvarchar(25)紅包留言
created_atint(11)建立時間(時間戳)
updated_atint(11)更新時間(時間戳)
  • 建表語句
CREATE TABLE `red_packet_record` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `uuid` varchar(100) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT 'UUID',
  `total_amount` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '紅包總金額,單位分',
  `total_packet` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '紅包總個數',
  `residue_amount` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '剩餘紅包金額,單位分',
  `residue_packet` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '剩餘紅包個數',
  `user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '新建紅包使用者ID',
  `remark` varchar(25) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '紅包留言',
  `created_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '紅包建立時間',
  `updated_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '紅包更新時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 介面路由定義
$api->group(['middleware' => 'auth:user'], function ($api) {
	$api->post('test', '[email protected]');
}
  • 控制器方法定義
    /**
     * 新建紅包
     * @param Request $request
     * @return mixed
     */
    public function addRedPacket(Request $request)
    {
        //資料校驗
        $this->sceneValidate((new RedPacketValidate())->scene('add-red-packet'));

        $redPacketService = new RedPacketService();
        $redPacketService->setUser($this->user())
                         ->addRedPacket($request)
                         ->allot()
                         ->inform();

        return $this->success("建立成功");
    }

Tips: 其中 addRedPacket() 表示新建紅包入庫邏輯,allot() 表示紅包分配並丟進 Redis list 中,inform() 表示通知客戶端可以開始搶紅包了。

  • 介面資料校驗
    /**
     * 定義資料驗證規則
     * @var array
     */
    protected $rule = [
        'total_amount'    => 'required|numeric|between:0.01,1000',
        'total_packet' => 'required|integer|between:1,100',
        'remark' => 'max:25',
    ];
    /**
     * 定義資料驗證錯誤提示
     * @var array
     */
    protected $message = [
        'total_amount.required'    => '紅包金額不能為空',
        'total_amount.numeric'     => '紅包金額必須是數字',
        'total_amount.between'     => '紅包總金額範圍必須是0.01~1000元之間',
        'total_packet.required'    => '紅包數量不能為空',
        'total_packet.integer'     => '紅包數量必須是整數',
        'total_packet.between'     => '紅包數量範圍必須是1~100之間',
        'remark.max'               => '紅包留言字元長度不能超過25',
    ];
    /**
     * 定義資料驗證場景
     * @var array
     */
    protected $scene = [
        'add-red-packet' => ['total_amount', 'total_packet', 'remark']
    ];
  • 新建紅包入庫邏輯
    public function addRedPacket(Request $request){
        $total_amount = (float) $request->input("total_amount");
        $total_packet = (int) $request->input("total_packet");
        try{
            $redPacket = new RedPacketRecord();
            $redPacket->uuid = uuidCode();
            $redPacket->total_amount = $total_amount * 100;
            $redPacket->total_packet = $total_packet;
            $redPacket->residue_amount = $total_amount * 100;
            $redPacket->residue_packet = $total_packet;
            $redPacket->user_id = 88;//這裡需要使用者登入才有資訊,為了驗證方便直接寫死
            $redPacket->remark = (string) $request->input("remark");
            $redPacket->created_at = YouDate::now()->getTimestamp();
            $redPacket->save();
        }catch(\Exception $exception){
            throw new ValidatorException("建立紅包失敗");
        }
    }
  • 紅包分配邏輯
    /**
     * 分配紅包數量和金額丟進 redis
     * @return $this
     */
    public function allot(){
        $data = [];
        $n = $this->redPacket->total_packet;
        for ($i = 0;$i < $n;$i++){
            //平均值
            $ave = $this->redPacket->total_amount/$this->redPacket->total_packet;

            $randAmount = mt_rand(1,$ave * 2);
            $data[] = $randAmount;

            $this->redPacket->total_amount = $this->redPacket->total_amount - $randAmount;
            $this->redPacket->total_packet--;
        }

        //把分配好的紅包金額丟進 Redis list
        $redList = Redis::lpush("ADD_RED_".$this->redPacket->user_id."_".$this->redPacket->uuid,$data);//將紅包丟進 list 裡面,準備後面的使用者搶
        if(!$redList){
            //若操作 redis 失敗,實施重試機制,處理相關退款邏輯
            //若重試多次 throw new ValidatorException("紅包發起失敗");
        }

        return $this;
    }
  • 通知客戶端
    /**
     * 通知客戶端開始搶紅包
     */
    public function inform(){
        //通知使用者可以開始搶紅包了,下面是虛擬碼
        //send::clien("ADD_RED_".$this->redPacket->user_id."_".$this->redPacket->uuid);
        return $this;
    }

3.搶紅包

需要判斷紅包是否已經搶完,然後限制同一個使用者最多隻能搶到一個紅包:

  • 確定使用者搶紅包表字段
欄位名稱型別和長度含義
idint(11)自增主鍵
uuidvarchar(100)紅包的UUID
amountint(11)紅包金額,單位分
user_idbigint(20)使用者ID
created_atint(11)建立時間(時間戳)
  • 建表語句
CREATE TABLE `red_packet_user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `uuid` varchar(100) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '紅包的UUID',
  `amount` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '紅包金額',
  `user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '搶到紅包的使用者ID',
  `remark` varchar(25) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '紅包留言',
  `created_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '搶到紅包時間',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
  • 介面路由定義
    $api->get('test', '[email protected]');
  • 控制器方法定義
    /**
     * 搶紅包介面
     * @param Request $request
     */
    public function getRedPacket(Request $request){
        $redPacketToken = $request->input("red_packet_token");//紅包標識,這個會在發起紅包時通知客使用者
        if(!$redPacketToken){
            throw new ValidatorException("驗證引數不能為空");
        }

        $redPacketService = new RedPacketService();

        $redPacketUser =  $redPacketService->setUser($this->user())
                         ->setRedPacketToken($redPacketToken)
                         ->isStart()
                         ->statNum()
                         ->getRedPacket();

        return $this->success($redPacketUser->amount);
    }

Tips: 其中 isStart() 表示判斷紅包是否被搶空或該使用者是否已經搶過,statNum() 表示統計參與搶紅包人數,getRedPacket() 表示搶紅包入庫邏輯。

  • 判斷紅包是否被搶空或該使用者是否已經搶過
    /**
     * 判斷紅包是否被搶空&該使用者是否已經搶過
     */
    public function isStart(){
        //判斷紅包是否還有
        if(!Redis::exists($this->redPacketToken) || !Redis::llen($this->redPacketToken)){
            throw new ValidationException("紅包已搶光");
        }

        //判斷該使用者是否已經搶過,需要登入
        if(Redis::getbit($this->redPacketToken."_BIT",$this->user->id) == 1){
            throw new ValidationException("該使用者已經搶過,不能再搶");
        }
        return $this;
    }

Tips: 其中已經搶過紅包的使用者 ID 會在 bitmap 中記錄。

  • 統計參與使用者數
    /**
     * 統計累計參與人數
     */
    public function statNum(){
        Redis::pfadd($this->redPacketToken."_COUNT",$this->user->id);//需要使用者登入
        return $this;
    }
  • 搶紅包邏輯
    /**
     * 使用者從 Redis list 取出紅包,並操作 db
     * @return $this
     */
    public function getRedPacket(){
        //從紅包列隊裡邊取出紅包金額,並備份至hash裡面
        $amount = Redis::rpop($this->redPacketToken);
        //備份至 hash 裡面
        if(!Redis::hset($this->redPacketToken."_BAK",$this->user->id,$amount)){
            //若操作 redis 失敗,實施重試邏輯
        }

        DB::beginTransaction();
        try{
            $redPacket = RedPacketRecord::where('uuid',$this->redPacketToken)->lockForUpdate()->first();//這裡必須要加一個鎖,若果不加鎖可能當前讀造成影響

            $redPacketUser = new RedPacketUser();
            $redPacketUser->uuid = $redPacket->uuid;
            $redPacketUser->amount = $redPacket->uuid;
            $redPacketUser->user_id = $this->user->id;
            $redPacketUser->created_at = YouDate::now()->getTimestamp();
            $redPacketUser->save();

            $redPacket->residue_packet--;
            $redPacket->residue_amount = $redPacket->residue_amount - $amount;
            $redPacket->updated_at = YouDate::now()->getTimestamp();
            $redPacket->save();
            DB::commit();
        }catch(\Exception $exception){
            DB::rollBack();
            Redis::lpush($this->redPacketToken,$amount);//丟回到list讓別人搶
            if(!Redis::hdel($this->redPacketToken."_BAK",$this->user->id)){
                //若操作 redis 失敗,實施重試邏輯
            }
            throw new ValidationException("搶紅包失敗");
        }
        //成功之後將使用者標記為已搶
        Redis::setbit($this->redPacketToken."_BIT", $this->user->id,1);
        return $redPacketUser;
    }

掃碼關注
在這裡插入圖片描述