PLAN A:30 分鐘未付款取消訂單

qufo發表於2018-12-23

週日有個妹子加我,附言上寫著

聽說你那方面很厲害

聽說? 你要是聽說我搬磚厲害,我這小身板一看就是吹牛,至於那方面,你問下我們班的女生,當仁不讓呀,要不你到隔壁護理學院問問,那裡仍有我的傳說。

馬上她發過來一條訊息

舞飛楊:小哥哥,先問你個事,我這邊有個需求,使用者下單後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 即可測試本地取消訂單。

舞飛楊:嗯,按你寫的還真可以了,現在用命令可以取消訂單了。
舞飛楊:可是,我不能整天坐在這裡打命令吧?
qufo:這個好說,我們弄個計劃任務。

執行系統命令 crontab -e ,在裡面加入

* * * * * cd /專案的根目錄 && php artisan schedule:run >> /dev/null 2>&1 

然後 app\Console\Kernel.phpschedule 方法裡,加入下面一行:

$schedule->command('order:cancel')->everyMinute();

這樣,取消訂單就會每分鐘自動執行一次了,省事了。

舞飛楊:我去試一下。
舞飛楊:哎,還真好了,謝謝。
qufo: 對了,你從哪知道我那方面很厲害的,要不要改天試一下?
qufo:在麼?
qufo:在麼?

靜靜過了兩天,飛楊倒是沒怎麼找我,我找她也不回,哎,這人啦,她的問題解決了我的問題怎麼辦。
剛想關電腦休息,她的訊息又來了。

舞飛楊:上次那個計劃任務真不錯,可以自動執行,可是近來訂單增多,經常是前一個任務還沒執行完下一個任務又開始啟動了,然後鎖著表改不了資料更慘了。

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 ,並配置好相應的服務地址,密碼之類的。
然後,在控制器中,處理好訂單確認寫入資料庫後,增加一行

Cache::store('redis')->put('ORDER_CONFIRM:'.$order->id,$order->id,30); // 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

啟動,就可以了。

舞飛楊:我去試試。

舞飛楊:enn,好了。可是才用了幾分鐘,自動斷掉了。

qufo: 是呀,redis 的預設連線是有超時的。

你改下 app\config\database.phpredis 節,增加一個 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: 這個,用 supervisor 就好了,你們的運維會弄這個。如果運維不懂弄的話,直接執行 php artisan order:expire & 也行。

舞飛楊:這樣也行麼,我去試下。

qufo: 這個方案的好處在於不需要計劃任務了,不會有大的時間偏差,而且我們可以定義各種鍵的名稱,各自監聽各種鍵的過期事件,集中管理,多好。

qufo: 嗯。實在不行,告訴我位置,我過去幫你弄呀。你在哪兒呀?

qufo: 最近有幾部新電影,要不要一起去看。

qufo: 你在哪呀?

qufo: 在麼?

又失蹤了幾周。我都要心如死灰了。
聲音再次響起。

舞飛楊:小哥哥,上次的東西真好,我把計劃任務全改成那個了,好用,而且時間準,互不影響,

qufo: 嗯。

舞飛楊:可是我們的業務增長很快,一臺機器處理不了,已經組了應用群集了,每臺機器上都要裝 redis 嗎?

qufo: 嗯。

舞飛楊:不是吧,那麼多 redis 伺服器一臺一個,能集中處理嗎?所有的應用都把鍵存到一臺機器上,然後只要一份監聽程式監聽那個過期事件?

qufo: 嗯。

舞飛楊:我聽說你很厲害才找你。要是一臺監聽處理的機器處理來不及,再加一臺去處理嗎?

qufo: 嗯。

舞飛楊:嗯什麼嗯,是你不知道吧?!

qufo: 什麼叫不知道,當業務量大起來的時候,直接增加監聽處理的機器是不行的,他們監聽同一個過期事件,兩臺機器會同時接到過期事件,除非進行 hash 分工,要不然處理兩遍事件就傻了。業務量足夠大的時候,得用訊息佇列了。

舞飛楊:哦,訊息佇列怎麼用?

qufo: 上次的監聽處理程式只要一臺處理,把監聽處理的過程改一下,取出訂單ID之後不要去處理,透過 rpush 放到一個 redis 的佇列裡去。另外起幾臺伺服器,連到這個 redis 伺服器,透過 blpop 接收訊息佇列裡出來的訂單ID。這樣,多臺機器可以同時工作,一個訂單隻會從 blpop 裡出來一次,不會重複執行,多臺機器可以分擔任務,又互不影響。訊息佇列也可以換成業界成熟的 rabbitmqkafka 之類的專業訊息佇列,那又是另外一個話題了。反正業務量大了,變複雜了,訊息匯流排跑不掉,天貓京東也差不多如此。

舞飛楊:京東我知道你去過,可是京東是 .net 的技術棧,天貓你又沒去過,你怎麼知道。

握草,她怎麼知道我去過京東沒去過天貓,我趕緊重新看了一下她的個人資料,介紹不知道什麼時候變成了“老楊他妹”,不對呀,我記得老楊跟我說過他是家中獨子呀。老楊那個160斤的月半子竟然玩 cos ,難怪人家叫“輕舞飛楊”,她叫“舞飛楊”,還能舞得起來嗎?
我了個去。
我抄起手機撥通了老楊的電話。
電話還沒拿到耳邊, 就聽到那邊傳來一陣豬叫聲,伴隨著塑膠凳子壓塌的聲音,一屁股蹲啪的聲音,手機掉地上的biaji聲。


本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章