laravel中30 分鍾未付款取消訂單,怎么做
一、總結
一句話總結:
可以用redis:30分鍾后過期--執行取消訂單Cache::store('redis')->put('ORDER_CONFIRM:'.$order->id,$order->id,30)
二、laravel中30 分鍾未付款取消訂單,怎么做
轉自或參考:30 分鍾未付款取消訂單,怎么做?
https://mp.weixin.qq.com/s?__biz=MzU1NTEzMDAxNQ==&mid=2247485021&idx=2&sn=4b4f6664d13345aa433d914e06a29459&chksm=fbd84badccafc2bb5a1c9b368213bf70ce4936544cf623b2db3549117e3163aee562846a3dc6&mpshare=1&scene=23&srcid=&sharer_sharetime=1586393263602&sharer_shareid=3dfb54f6b438c03008a5b04527be9c8f#rd
第一次親密接觸
問題:我這邊有個需求,用戶下單后 30 分鍾如果沒付款就取消掉,這個要怎么寫呀。
qufo: 這個還不簡單,寫個取消訂單的命令,弄個計划任務定時不就行了。
舞飛楊:哦,就是 crontab ?
qufo: 是呀,follow me
先來個
$php artisan make:command OrderCancel
Console command created successfully.
然后修改 app\Console\Commands\OrderCancel.php 為如下:
<?php
namespace App\Console\Commands;
use App\Http\Models\Order;
use Illuminate\Console\Command;
class OrderCancel extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'order:cancel';
/**
* The console command description.
*
* @var string
*/
protected $description = '30分鍾未付款取消訂單';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
* @throws
*/
public function handle()
{
try {
$unPaid = Order::where('created','<',time()-30*60) //創建時間在30分鍾以前
->where('order_status',1) // 剛下單未支付
->get();
foreach ($unPaid as $order) {
$order->cancel(); // 執行取消動作
}
} catch (\Exception $e) {
throw $e;
}
return true;
}
}
試一下在項目根下執行 php artisan list 應該能看到下面那一行了。
order
order:cancel 30分鍾未付款取消訂單
直接執行命令 php artisan order:cancel 即可測試本地取消訂單。
執行系統命令 crontab -e , 在里面加入
* * * * * cd /項目的根目錄 && php artisan schedule:run >> /dev/null 2>&1
然后 app\Console\Kernel.php 的 schedule 方法里,加入下面一行:
$schedule->command('order:cancel')->everyMinute();
這樣,取消訂單就會每分鍾自動執行一次了,省事了。
代志大條了
舞飛楊:上次那個計划任務真不錯,可以自動執行,可是近來訂單增多,經常是前一個任務還沒執行完下一個任務又開始啟動了,然后鎖着表改不了數據更慘了。
qufo: 那是,業務量小的時候這個方案好用方便,可是業務量大了,重入會出問題;而且定時任務涉及到 crontab 的權限控制問題。訂單量大一點就不好用了。而且,因為我們的任務每分鍾執行一次,所以有些訂單會在 30 分鍾的時候執行取消,有些會在接近 31 分的時候執行。就算沒訂單,一天也重復執行 1440 次。隨着業務的擴展,除了取消訂單,還會有提醒支付,催商家發貨,催用戶確認收貨,催騎手接單等等一堆事情,這些加進去,計划任務越來越庸腫,執行效率大大降低,搞不好容易出大事。
舞飛楊:對呀對呀,現在的計划任務已經有 20 多個了,再加進去不是辦法呀。之前的任務現在執行得亂 78 糟,全亂套了。現在還有什么好辦法么?
qufo:有倒是有,不過我需要你有用過一樣東西。
舞飛楊:你要什么?流氓。
qufo: 什么流氓,我說要用 redis 。
舞飛楊:哦,我知道,我裝過,用過一陣子,不過,這有什么關系?
qufo:在訂單確認成功之后,往 redis 里加入 key, 用 ORDER_CONFIRM:訂單ID 這樣的格式來,然后定義他 30 分鍾后過期,我們監聽這個鍵過期事件就好了。
先保證 redis 的版本大於 2.8 ,現在絕大部分不成問題了,然后修改 redis 的配置文件,加入
notify-keyspace-events "Ex"
以啟用鍵過期的通知。
然后重新啟動 redis 。
在 .env 里,確認 CACHE_DRIVER=redis ,並配置好相應的服務地址,密碼之類的。
然后,在控制器中,處理好訂單確認寫入數據庫后,增加一行
// 30分鍾后過期--執行取消訂單
Cache::store('redis')->put('ORDER_CONFIRM:'.$order->id,$order->id,30);
然后我們來監聽 ORDER_CONFIRM:ORDER_ID 的過期事件
先建個命令,我們一會兒的監聽全靠他了。
$php artisan make:command OrderExpireListen
Console command created successfully.
然后把命令執行文件 app\Console\Commands\OrderExpireListen.php 寫成這樣:
<?php
namespace App\Console\Commands;
use App\Http\Models\Order;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis as Redis;
class OrderExpireListen extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'order:expire';
/**
* The console command description.
*
* @var string
*/
protected $description = '監聽訂單創建,在30分鍾后如果沒付款取消訂單。';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//
$cachedb = config('database.redis.cache.database',0);
$pattern = '__keyevent@'.$cachedb.'__:expired';
Redis::subscribe([$pattern],function ($channel){ // 訂閱鍵過期事件
$key_type = str_before($channel,':');
switch ($key_type) {
case 'ORDER_CONFIRM':
$order_id = str_after($channel,':'); // 取出訂單 ID
$order = Order::find($order_id);
if ($order) {
$order->cancel(); // 執行取消操作
}
break;
case 'ORDER_OTHEREVENT':
break;
default:
break;
}
});
}
}
文件好了之后,使用
$php artisan order:expire
啟動,就可以了。
redis 的默認連接是有超時的。
你改下 app\config\database.php 中 redis 節,增加一個 read_write_timeout :
'redis' => [
'client' => 'predis',
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
'read_write_timeout' => env('REDIS_RW_TIMEOUT', 5), // 讀寫超時設定
],
],
然后在 .env 中配置 REDIS_RW_TIMEOUT=-1 這樣就不會超時了。
舞飛楊:小哥哥,上次的東西真好,我把計划任務全改成那個了,好用,而且時間准,互不影響,
qufo: 嗯。
舞飛楊:可是我們的業務增長很快,一台機器處理不了,已經組了應用群集了,每台機器上都要裝 redis 嗎?
qufo: 嗯。
舞飛楊:不是吧,那么多 redis 服務器一台一個,能集中處理嗎?所有的應用都把鍵存到一台機器上,然后只要一份監聽程序監聽那個過期事件?
qufo: 嗯。
舞飛楊:我聽說你很厲害才找你。要是一台監聽處理的機器處理來不及,再加一台去處理嗎?
qufo: 嗯。
舞飛楊:嗯什么嗯,是你不知道吧?!
qufo: 什么叫不知道,當業務量大起來的時候,直接增加監聽處理的機器是不行的,他們監聽同一個過期事件,兩台機器會同時接到過期事件,除非進行 hash 分工,要不然處理兩遍事件就傻了。業務量足夠大的時候,得用消息隊列了。
舞飛楊:哦,消息隊列怎么用?
qufo: 上次的監聽處理程序只要一台處理,把監聽處理的過程改一下,取出訂單 ID 之后不要去處理,通過 rpush 放到一個 redis 的隊列里去。另外起幾台服務器,連到這個 redis 服務器,通過 blpop 接收消息隊列里出來的訂單 ID。這樣,多台機器可以同時工作,一個訂單只會從 blpop 里出來一次,不會重復執行,多台機器可以分擔任務,又互不影響。消息隊列也可以換成業界成熟的 rabbitmq 、 kafka 之類的專業消息隊列,那又是另外一個話題了。反正業務量大了,變復雜了,消息總線跑不掉,天貓京東也差不多如此。