具体情况大概是这样的,我们有一个 MySQL 的模型,有一个 Mongo 的模型,然后在 MySQL 的模型中 hasOne Mongo 的模型,结果:

MySQL with 的时候可以查询出数据,但是使用 whereHas 的时候查询不到数据了。

环境

  • laravel/lumen-framework: 5.5.2
  • jenssegers/mongodb: 3.3.2

重现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A extends \Illuminate\Database\Eloquent\Model
{
// 可以让我们关联 MongoDB 的一个 trait
use \Jenssegers\Mongodb\Eloquent\HybridRelations;

public function b()
{
return $this->hasOne(B::class, '_id', 'b_id');
}
}

class B extends \Jenssegers\Mongodb\Eloquent\Model
{

}

Laravel 的关联查询如下:

1
2
3
4
5
6
7
$a = A::query()->with('b')->whereKey(10)->first();
// $a 可以查询出来
dump($a);

$a = A::query()->whereHas('b')->whereKey(10)->first();
// $a 为 null
dump($a);

正常情况下,其实第二个查询我们也应该查询出来才对,可是我们查不到,然后尝试打印一下查询:

1
2
3
4
[2021-06-08 09:54:30] lumen.INFO: time: 75.18 select * from `as` where `as`.`id` = 10 limit 1  
[2021-06-08 09:54:30] lumen.INFO: time: 48.25, connection: mongo_core, db.bs.find({"_id":{"$in":["60becd2306bce821e55ffce3"]}})
[2021-06-08 09:54:30] lumen.INFO: time: 2.52, connection: mongo_core, db.bs.find({"projection":{"_id":true}})
[2021-06-08 09:54:30] lumen.INFO: time: 18.81 select * from `as` where `id` in ("60becd2306bce821e55ffce3") and `as`.`id` = 10 limit 1

我们发现最后一条语句并不是如我们期待的那样,where b_id in 而是 where id in,这是一个不正常的行为,因为根据我们的定义,关联 A 模型的字段应该是 b_id 而不是 id

原因

因为想找到查询使用 id 而不是 b_id,对关联部分的代码 debug 了一下,最后发现,位于 \Jenssegers\Mongodb\Helpers\QueriesRelationships 的方法 getRelatedConstraintKey 返回了一个错误的关联键:

QueriesRelationships.php v3.3.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Returns key we are constraining this parent model's query with
* @param $relation
* @return string
* @throws \Exception
*/
protected function getRelatedConstraintKey($relation)
{
if ($relation instanceof HasOneOrMany) {
// 这里返回了关联模型的主键(id),但是实际上应该返回的是关联里面指定的那个键(b_id)
return $this->model->getKeyName();
}

if ($relation instanceof BelongsTo) {
return $relation->getForeignKey();
}

throw new \Exception(class_basename($relation) . ' Is Not supported for hybrid query constraints!');
}

感觉这是一个 bug,然后去新版本的库里面看了一下,发现新版本里面已经修改过来了:

QueriesRelationships.php v3.7.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Returns key we are constraining this parent model's query with
* @param Relation $relation
* @return string
* @throws Exception
*/
protected function getRelatedConstraintKey(Relation $relation)
{
if ($relation instanceof HasOneOrMany) {
// 返回了正确的关联键
return $relation->getLocalKeyName();
}

if ($relation instanceof BelongsTo) {
return $relation->getForeignKeyName();
}

if ($relation instanceof BelongsToMany && !$this->isAcrossConnections($relation)) {
return $this->model->getKeyName();
}

throw new Exception(class_basename($relation) . ' is not supported for hybrid query constraints.');
}

结论

由于修复的版本与当前使用的版本相差太多,而且依赖于 Laravel 的版本,所以无法升级其版本,只有换一种写法了。(whereHas 拆分出来,先查询到关联数据的 _id,然后作为 whereIn 条件传递)。

有时候需要根据需要对 MongoDB 的集合进行分集合存储(类似 MySQL 的分表),这样一来 db 下就产生了很多相同前缀的集合,这样一来,我们想知道这一批集合的一些关键状态信息的时候就比较不便,比如想知道某一个前缀所有集合的总大小等等。

一个可行的方法是,通过找到一个模式前缀的所有集合,然后逐个调用 MongoDB 的 collStats 命令来获取单个集合的状态,然后做一个聚合,下面是基于 jenssegers mongodb 的一个实现:

统计 product 库下 xx_ 前缀的所有集合的大小等

输出的字段说明:

  • collection_count: xx_ 前缀的集合总数量
  • total_document_count: xx_ 前缀文档总数量
  • total_size: xx_ 前缀集合总大小
  • avg_obj_size: xx_ 前缀集合平均单个文档的大小
  • total_storage_size: xx_ 前缀集合占的总存储大小
  • total_index_size: xx_ 前缀集合索引总大小
  • total_bytes_in_cache: xx_ 前缀集合数据当前占用的缓存大小
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
$patterns = [
'product.xx_'
];

function format($bytes): string
{
if ($bytes < 1024) {
return "{$bytes} bytes";
}

if ($bytes < 1024 * 1024) {
$bytes = round($bytes / 1024, 2);

return "{$bytes} Kb";
}

if ($bytes < 1024 * 1024 * 1024) {
$bytes = round($bytes / 1024 / 1024, 2);

return "{$bytes} Mb";
}

$bytes = round($bytes / 1024 / 1024 / 1024, 2);

return "{$bytes} Gb";
}

class CollectionStats
{
private $stats;

public function __construct(array $stats)
{
$this->stats = $stats;
}

public function toArray(): array
{
return [
'collection' => $this->stats['ns'],
'size' => format($this->stats['size']),
'count' => $this->stats['count'],
'avgObjSize' => format($this->stats['avgObjSize'] ?? 0),
'storageSize' => format($this->stats['storageSize']),
'totalIndexSize' => format($this->stats['totalIndexSize']),

'raw' => [
'count' => $this->stats['count'],
'size' => $this->stats['size'],
'avgObjSize' => $this->stats['avgObjSize'] ?? 0,
'storageSize' => $this->stats['storageSize'],
'totalIndexSize' => $this->stats['totalIndexSize'],
'bytes_in_cache' => $this->stats['wiredTiger']['cache']['bytes currently in the cache'],
],
];
}
}

foreach ($patterns as $pattern) {
// $connectionName 连接名
// $collectionPrefix Collection 前缀
[$connectionName, $collectionPrefix] = explode('.', $pattern);

dump(compact('connectionName', 'collectionPrefix'));

$collections = get_collection_names($connectionName);
$collections = collect($collections)
->filter(function ($collection) use ($collectionPrefix) {
return Str::startsWith($collection, $collectionPrefix);
})
->values()
->toArray();

// 查看 Collection 的状态:
// https://docs.mongodb.com/manual/reference/command/
// https://docs.mongodb.com/manual/reference/command/collStats/#mongodb-dbcommand-dbcmd.collStats

$result = [];

foreach ($collections as $collection) {
$cursor = DB::connection($connectionName)->getMongoDB()->command(['collStats' => $collection]);
$res = json_decode(json_encode($cursor->toArray()[0]), true);
$reply = json_encode($res, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

$result[] = (new CollectionStats($res))->toArray();
}

$results = collect($result);

dump('collection_count: ' . $results->count());
dump('total_document_count: ' . $results->sum('raw.count'));
dump('total_size: ' . format($results->sum('raw.size')));
dump('avg_obj_size: ' . format($results->avg('raw.avgObjSize')));
dump('total_storage_size: ' . format($results->sum('raw.storageSize')));
dump('total_index_size: ' . format($results->sum('raw.totalIndexSize')));
dump('total_bytes_in_cache: ' . format($results->sum('raw.bytes_in_cache')));
echo PHP_EOL;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
array:2 [
"connectionName" => "product"
"collectionPrefix" => "xx_"
]
"collection_count: 222"
"total_document_count: 1664601"
"total_size: 2.24 Gb"
"avg_obj_size: 1.4 Kb"
"total_storage_size: 996.66 Mb"
"total_index_size: 156.34 Mb"
"total_bytes_in_cache: 3.17 Gb"

其他

这个例子是基于 Laravel 的一个库来实现的,不过只是简单地调用了一个 command 方法,其他语言利用 MongoDB 提供的库也比较容易可以实现。

是这样的,在服务器上的 php 需要 zip 库,但我自己手动编译了一个 zip 库,安装在了 /usr/local/lib64 下。但 php 编译 zip 扩展的时候提示找不到 libzip。

解决方法

/usr/local/lib64 也加入到系统共享库的搜索路径,我们可以通过 ldconfig

script
1
2
echo "/usr/local/lib64" > /etc/ld.so.conf.d/local.conf
ldconfig

然后重新 configure。

在 Laravel 5.8 以下的版本中,队列消费者处理消息超时的时候会静默失败,即使超过了重试次数,这就导致一个问题,超时的时候问题无法被及时发现。

在 5.8 及以上版本中,超时次数重试次数之后也会当作失败处理,会记录到 failed_jobs 表里面,而不像之前版本那样一直重试。

具体可以查看 \Illuminate\Queue\Worker::registerTimeoutHandler 方法。

不过在 5.8 以下的版本中,我们依然有办法可以得知队列超时,registerTimeoutHandler 里面在超时退出消费者进程之前,先 fire 了一个 WorkerStopping 事件,并且传递了参数 1。

也就是说,我们可以通过监听 WorkerStopping 事件,然后通过判断 WorkerStopping 的 status 状态字段是否为 1,如果是 1,则表明队列超时了,这个时候我们就可以采取一些行动,比如发送报警通知,及时告知开发者。

我们知道在 redis 中,有一个排他锁,set ... nx,但是这个锁有一个问题是,有可能造成所有请求阻塞在等待这个锁上。

如果是允许同时执行的,比如秒杀,是可以有多个请求成功的,那么可以尝试一下 redis 的乐观锁。

如何使用

  1. 利用 redis 的 watch 功能,监控这个 redis key 的状态值。
  2. 获取 redis key 的值
  3. 创建 redis 事务
  4. 修改这个 key 的值
  5. 执行这个事务,如果 key 的值被修改过则回滚,key 不修改。(如果没修改过,则执行成功)

示例

事务执行

1
2
3
4
5
6
7
8
9
10
➜  ~ redis-cli
127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a 12
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
127.0.0.1:6379>

如果事务执行成功,exec 会返回 OK,如果执行失败,则会返回 nil。我们可以根据这个返回值来判断事务是否执行成功。

0%