PLAN A:30 分钟未付款取消订单
周日有个妹子加我,附言上写着
听说你那方面很厉害
听说? 你要是听说我搬砖厉害,我这小身板一看就是吹牛,至于那方面,你问下我们班的女生,当仁不让呀,要不你到隔壁护理学院问问,那里仍有我的传说。
第一次亲密接触
马上她发过来一条消息
舞飞杨:小哥哥,先问你个事,我这边有个需求,用户下单后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.php
的 schedule
方法里,加入下面一行:
$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.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: 这个,用
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
里出来一次,不会重复执行,多台机器可以分担任务,又互不影响。消息队列也可以换成业界成熟的rabbitmq
、kafka
之类的专业消息队列,那又是另外一个话题了。反正业务量大了,变复杂了,消息总线跑不掉,天猫京东也差不多如此。舞飞杨:京东我知道你去过,可是京东是 .net 的技术栈,天猫你又没去过,你怎么知道。
握草,她怎么知道我去过京东没去过天猫,我赶紧重新看了一下她的个人资料,介绍不知道什么时候变成了“老杨他妹”,不对呀,我记得老杨跟我说过他是家中独子呀。老杨那个160斤的月半子竟然玩 cos ,难怪人家叫“轻舞飞杨”,她叫“舞飞杨”,还能舞得起来吗?
我了个去。
我抄起手机拨通了老杨的电话。
电话还没拿到耳边, 就听到那边传来一阵猪叫声,伴随着塑料凳子压塌的声音,一屁股蹲啪的声音,手机掉地上的biaji声。
尼
妹
本作品采用《CC 协议》,转载必须注明作者和本文链接
高认可度评论:
666 不写小说可惜了
@Blings 第三个也简单。按文中描述的,把
app\Console\Commands\OrderExpireListen.php
里的$order->cancel(); // 执行取消操作
换成Redis::rpush('ORDER_CANCEL',$order->id);
这样放到队列里。另写一个命令,象 OrderExpire 一样监听队列,从Redis::blpop
里取出,就有订单ID,然后按往常一样 cancel 处理。上述是伪码。因为大部分人的问题可以通过第二种方案解决,若业务量真的大到需要多台单独的机器来处理,估计项目已经用其他语言重构了,所以只留了思路,没有代码。
延迟队列了解一下
6666
O(∩_∩)O哈哈~看着你的故事,畅饮了杯~
有趣有趣
有意思
666 不写小说可惜了
有意思
哈哈,同样的需求我之前只也做到使用第二个方法实现,第三个要如何实践啊
@Blings 第三个也简单。按文中描述的,把
app\Console\Commands\OrderExpireListen.php
里的$order->cancel(); // 执行取消操作
换成Redis::rpush('ORDER_CANCEL',$order->id);
这样放到队列里。另写一个命令,象 OrderExpire 一样监听队列,从Redis::blpop
里取出,就有订单ID,然后按往常一样 cancel 处理。上述是伪码。因为大部分人的问题可以通过第二种方案解决,若业务量真的大到需要多台单独的机器来处理,估计项目已经用其他语言重构了,所以只留了思路,没有代码。
@qufo 对集群还是不熟悉,先看一看,不过思路大致明白了
延迟队列了解一下
用mq来做代替redis--driver, 起一个专门的队列, 下单的时候加入一个延时事件
可以多起几个消费者跑起来
看到标题的第一反应就是用队列delay30分钟检查订单的状态就好了
@赵亚博 部分延迟队列也是使用的过期,只不过内部封装好了。
@ruke 是的,第三种方案就是这种。
@hello-bug 是的。Laravel 里直接有了
delay
.考虑下要不要把方案三也简单地实现下。
写的很好,幽默风趣很有才
这故事编的可以,最主要的是点也讲述的听明白
为什么不用延时队列?
哎呀,一开头就是开车呀
不愧是技术大佬啊,讲的很好啊,还附带一点幽默的故事情节。
写的很棒
太流氓了!!
已经到了超速的边缘了,不过还好,我还没翻车
一波猝不及防的飙车! 666 点赞!
再咨询一下,对于时间跨度比较大的,比如一年后才会触发的这种定时任务,有没有好的方案? 感谢
其实延时队列本身没什么问题,只是既然使用队列,那么一般发送之后不考虑取消的,如果是这样的话,一旦发送,没办法变更,在一个每日数十万甚至百万以上订单系统,这是一个灾难吧
@snower 没事的,别放
redis
里,放其他专业的消息队列里,一天百万不是什么事儿。象rabbitmq
,zeromq
,kafka
,至少是c10k
的,每秒万条并发,一天下来10亿不是事。至于处理那些过期事件,要看消费者的能力了,多台群集可以尝试。如果担心一台mq
的处理能力,可以配置多台,根据类型发到不同的消息队列分担任务就行。该死的标题党。
段子手,技术还讲的这么好
我也想问 为什么不用延时队列?
@GDDD Laravel 里有
delay
,可以直接用,因为有很多讲delay
的文章,想必大家都熟悉了,所以这次就略过了。好了,我以后面试不问这个问题了,手动狗头
老哥666
技术很厉害,没想到还是个段子手啊!
有意思..
看你这篇文章使我有种看《大话设计模式》里对白的感觉。。
保证不重复就用队列。
这个写文章的方式挺好的,讲解的非常清晰,剧情引人入胜。
有意思;牛逼,点个赞
redis zset 维护也不错,保存订单id,然后用订单的创建时间戳当成 score,订单完成的时候,就 zrem key orderid, 其他定时任务查某个时间区间的数据,zrangebyscore ,数据占用少到极致,甚至都不多查一遍数据库
@BradStevens 有点不解
@qufo 使用zset 比利用 redis的大key set这样的形式,节约一些空间占用,定时任务处理方面,可以使用swoole的swoole_timer_tick 全都是内存级的操作,会提升很多效率。参考这个文章:https://alpha2016.github.io/2019/02/24/%E5...
@BradStevens
ORDER_CONFIRM.$order->id
并不大, value 是order_id
本身,不是序列化的订单内容。这个 value 甚至可以设为1
,并不影响。对于存储空间来说,差异不大。crontab
来做定时任务,而是使用swoole
,这样一来,引入了新的复杂性。swoole
没有细用,但看其文档,swoole_timer_tick
限制比较多。@qufo
个人观点还是我的那种方法其实更简单,更高效率,而且提前避免一些小细节。
已经将配置文件改成notify-keyspace-events "Ex" ,key过期之后没有任何反应啊?还是说我哪里写错了……,
单独使用
Redis::publish('test-channel', json_encode(['foo' => 'bar']));
Redis::subscribe(['test-channel'], function ($message) {
$this->info($message);
});
这是可以收到消息的
@james_xue 确认下版本,是否重启过服务。然后 redis 命令形式订阅下过期消息看能不能收到。
很用心,支持
make 很棒 写的超好呀
这是个令人不太愉快的故事,
但是个看完令人愉快的技术贴。
如此可以说明,对女人再好人家也对你爱理不理。相对的,对男同胞友人好点,就记住你了 :smirk:
@命中水 小男孩在外面要保护好自己,不要被别的小男孩骗了。
@zqiqicn 那不会,如果真要被骗了,我要让他知道我那方面的厉害
Predis\Response\ServerException : ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context
这个是什么问题
全看聊天记录了,我点进来是干嘛来着???
如果不是你写故事,我都不一定看得完。
@james_xue 我遇到的问题跟你一样,我还尝试在cli中更改这个参数,但是按照他的说法或者官网的文档,都是监听不到过期。
@wells 你将代码放在服务器试试,我的本地一直跑不通,放在服务器上可以的
@james_xue 我就是在虚拟机里面搞得,监听不到事件,但是命令行里能监听。然后也自动退出
本以为看的是技术博客, 没想到一下就误上了老司机的车, 这车开的是真好, 博主是个大大的好人 :kissing_heart:
老司机也能翻车的吗?还是一个人聊天不过瘾,注册两个账号玩的 :smile: :smile:
为什么不用事件监听 加延迟队列呢
小白问一个简单的问题,确认订单,要往redis 加 ORDER_CONFIRM:订单ID , 如果支付成功,是不是要把这个key,删除掉呢?
@chenrenhui
学习了。谢谢大神分享
php artisan order:expire &您好,我运行了这个,但是把xshell断开再重新连接,然后ps,发现进程没有再执行这个命令
redis版本:5.0.3
laravel版本:5.8.26
redis配置已修改 notify-keyspace-events "Ex"
下订单时添加
但是在监听时,接收不到,
在redis-cli 中是可以接收到的
5.7版本用这个方法可以
但是换成5.8就不行了
请问是什么问题呢
666
第二种方法,为什么我的不行.还有在redis里面怎么看到缓存数据.求解!!!
Laravel 5.8 或 6.0的版本可以使用队列的延迟分发处理
安全带已上,请开车 :heart_eyes:
当我运行时遇到这问题 read error on connection,想问大大怎样解决? 已在 app\config\database.php 中 redis 节,增加一个 read_write_timeout=-1,但还是不可以解决
阿里的redis服务 能用吗 没看到
这个文章写得很好,但实际有问题。我测试在 Laravel 7 的版本如果使用
use Illuminate\Support\Facades\Redis;
则不会监听到过期事件。同理连接超时也无效,我实在不知道是什么情况,博主可以测试下,看看是什么问题但 redis-cli 是可以监听到的
换成原生 Redis 就可以了。
@qufo @vinhson @wells @会尿尿的大鲨鱼 @taohua
不成功可以看下这个
666!超级人才哇!
什么时候出一下章?
作者怕是没考虑到redis的pub/sub机制存在一个硬伤吧,以下官网:
你那方面确实有两下子 :flushed: