Redis練習-模擬一個搶紅包系統
阿新 • • 發佈:2021-02-04
技術標籤:Redisredis資料庫nosqlmysql併發
文章目錄
Redis練習-模擬一個搶紅包系統
前面學習過 Redis
的一些基礎知識,為了達到學以致用的目的,加深和鞏固對 Redis
的理解,這篇文章模擬微信搶紅包,設計一個簡單的搶紅包系統。
1.需求描述
- 應用場景描述:
- 假設某微信群有:
10000
人 - 某土豪發紅包:
1000
元 - 紅包個數:
10
個 - 分配規則:發
1000
元,隨機分成10
個紅包 - 紅包過期時間
24
小時 - 新建紅包規則:
自定義紅包個數範圍1~100
個 ,金額範圍0.01~1000
元,紅包留言備註字元自定義長度0~25
2.新建紅包
- 確定新建紅包表字段:
欄位名稱 | 型別和長度 | 含義 |
---|---|---|
id | int(11) | 自增主鍵 |
uuid | varchar(100) | UUID |
total_amount | int(11) | 紅包總金額,單位分 |
total_packet | smallint(5) | 紅包總個數 |
residue_amount | int(11) | 剩餘紅包總金額,單位分 |
residue_packet | smallint(5) | 剩餘紅包個數 |
user_id | bigint(20) | 使用者ID |
remark | varchar(25) | 紅包留言 |
created_at | int(11) | 建立時間(時間戳) |
updated_at | int(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.搶紅包
需要判斷紅包是否已經搶完,然後限制同一個使用者最多隻能搶到一個紅包:
- 確定使用者搶紅包表字段:
欄位名稱 | 型別和長度 | 含義 |
---|---|---|
id | int(11) | 自增主鍵 |
uuid | varchar(100) | 紅包的UUID |
amount | int(11) | 紅包金額,單位分 |
user_id | bigint(20) | 使用者ID |
created_at | int(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;
}
掃碼關注