api 项目上线之后 sentry 上收集到了一个奇怪的问题。
1
2
3
|
Predis\Response\ServerExceptionvendor/predis/predis/src/Client.php in onErrorResponse
errorERR unknown command ' EVAL 'php
|
看到这个 EVAL
这个命令,感觉是因为使用 lua 的原因,但是项目中我们又没有在 redis 中使用 lua 脚本,这个就很奇怪了。(腾讯的 redis 服务阉割了许多命令: https://cloud.tencent.com/document/product/239/13419)
排查调用栈如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
Predis\Response\ServerException: ERR unknown command ' EVAL '
#24 vendor/predis/predis/src/Client.php(370): onErrorResponse
#23 vendor/predis/predis/src/Client.php(335): executeCommand
#22 vendor/predis/predis/src/Client.php(315): __call
#21 vendor/illuminate/redis/Connections/Connection.php(72): eval
#20 vendor/illuminate/redis/Connections/Connection.php(72): command
#19 vendor/illuminate/redis/Connections/Connection.php(84): __call
#18 vendor/illuminate/cache/RedisStore.php(129): eval
#17 vendor/illuminate/cache/RedisStore.php(129): add
#16 vendor/illuminate/cache/Repository.php(219): add
#15 vendor/illuminate/console/Scheduling/CacheMutex.php(37): create
#14 vendor/illuminate/console/Scheduling/Event.php(170): run
#13 vendor/illuminate/console/Scheduling/ScheduleRunCommand.php(59): fire
#12 vendor/illuminate/console/Scheduling/ScheduleRunCommand.php(0): call_user_func_array
#11 vendor/illuminate/container/BoundMethod.php(30): Illuminate\Container\{closure}
#10 vendor/illuminate/container/BoundMethod.php(87): callBoundMethod
#9 vendor/illuminate/container/BoundMethod.php(31): call
#8 vendor/illuminate/container/Container.php(539): call
#7 vendor/illuminate/console/Command.php(182): execute
#6 vendor/symfony/console/Command/Command.php(252): run
#5 vendor/illuminate/console/Command.php(168): run
#4 vendor/symfony/console/Application.php(946): doRunCommand
#3 vendor/symfony/console/Application.php(248): doRun
#2 vendor/symfony/console/Application.php(148): run
#1 vendor/laravel/lumen-framework/src/Console/Kernel.php(84): handle
#0 artisan(35): null
|
看起来是 Schedule
的 CacheMutex
类中的 create
方法 使用了某些 lua 脚本导致的。
继续往下追踪的话会发现, 这个是使用了 Illuminate\Contracts\Cache\Repository
中的 add
方法。 这个方法的描述为 Store an item in the cache if the key does not exist.
。 不存在 key 的话,就把数据存进去。
追踪进实现之后发现了有这么个逻辑, 缓存驱动可以实现一个 add
方法, 来实现原子性操作。
1
2
3
4
5
6
7
8
|
// If the store has an "add" method we will call the method on the store so it
// has a chance to override this logic. Some drivers better support the way
// this operation should work with a total "atomic" implementation of it.
if (method_exists($this->store, 'add')) {
return $this->store->add(
$this->itemKey($key), $value, $minutes
);
}
|
因为我们使用了 Redis 作为缓存, 那么只需要看 Illuminate\Cache\RedisStore
中的 add 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param float|int $minutes
* @return bool
*/
public function add($key, $value, $minutes)
{
$lua = "return redis.call('exists',KEYS[1])<1 and redis.call('setex',KEYS[1],ARGV[2],ARGV[1])";
return (bool) $this->connection()->eval(
$lua, 1, $this->prefix.$key, $this->serialize($value), (int) max(1, $minutes * 60)
);
}
|
到这里问题已经很明朗了。剩下的问题就是如何解决了。
我想到的方案:
方案一:继承 Illuminate\Cache\RedisStore
把其中的 add
方法设置为 private
。试了一下 method_exists
对于 private
的判定是 true
。 惊不惊喜?意不意外? 这样的话就不能通过简单的继承来完成该方法了。
方案二:继承 Illuminate\Console\Scheduling\CacheMutex
类重写其中的 create
方法。判断 Store
如果是 RedisStore
则把 Illuminate\Contracts\Cache\Repository
的中 add
方法提到 create 方法中并删除原子操作判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
<?php
namespace App\Console;
use Carbon\Carbon;
use DateTime;
use Illuminate\Cache\RedisStore;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Contracts\Cache\Repository as Cache;
/**
* Class CacheMutex
* @package App\Console
* 修复腾讯云 redis 不支持 lua 脚本的问题
*/
class CacheMutex extends \Illuminate\Console\Scheduling\CacheMutex
{
/**
* @inheritDoc
*/
public function __construct(Cache $cache)
{
parent::__construct($cache);
dd();
}
/**
* @inheritDoc
*/
public function create(Event $event)
{
$key = $event->mutexName();
$minutes = $event->expiresAt;
$value = true;
if ($this->cache->getStore() instanceof RedisStore) {
if (is_null($minutes = $this->getMinutes($minutes))) {
return false;
}
// If the value did not exist in the cache, we will put the value in the cache
// so it exists for subsequent requests. Then, we will return true so it is
// easy to know if the value gets added. Otherwise, we will return false.
if (is_null($this->cache->get($key))) {
$this->cache->put($key, $value, $minutes);
return true;
}
return false;
}
return parent::create($event);
}
/**
* Calculate the number of minutes with the given duration.
*
* @param \DateTime|float|int $duration
* @return float|int|null
*/
protected function getMinutes($duration)
{
if ($duration instanceof DateTime) {
$duration = Carbon::now()->diffInSeconds(Carbon::instance($duration), false) / 60;
}
return (int) ($duration * 60) > 0 ? $duration : null;
}
}
|