ORM缓存
最后更新于:2022-04-02 05:14:28
[TOC]
# ORM缓存
每个应用程序都不同。但是,在大多数应用程序中,有些数据不经常更改。性能方面最常见的瓶颈之一是访问数据库。这是由于PHP执行的复杂连接/通信过程以及从数据库获取数据的每个请求。因此,如果我们想要获得良好的性能,我们需要在应用程序需要的地方添加一些缓存层。
本章介绍了可以实现缓存以提高性能的潜在领域。Phalcon为开发人员提供了在应用程序需要时实现缓存所需的工具。
## 缓存结果集
避免持续访问数据库的一种成熟技术是使用具有更快访问权限的系统(通常是内存)来缓存不经常更改的结果集。
当`Phalcon\Mvc\Model`需要服务来缓存结果集时,它将从依赖注入容器中请求它。服务名称称为modelsCache。Phalcon提供了一个可以存储任何类型数据的缓存组件。我们现在将看到如何将它与我们的模型集成。
首先,我们需要将缓存组件注册为DI容器中的服务。
```php
set(
'modelsCache',
function () {
// 缓存数据一天(默认设置)
$frontCache = new FrontendData(
[
'lifetime' => 86400,
]
);
// Memcached连接设置
$cache = new BackendMemcache(
$frontCache,
[
'host' => 'localhost',
'port' => '11211',
]
);
return $cache;
}
);
```
在将缓存组件注册为DI容器中的服务之前,Phalcon提供了对缓存组件的创建和自定义的完全控制。正确设置缓存组件后,可以按如下方式缓存结果集:
```php
[
'key' => 'my-cache',
],
]
);
// Cache the resultset for only for 5 minutes
$products = Products::find(
[
'cache' => [
'key' => 'my-cache',
'lifetime' => 300,
],
]
);
// Use the 'cache' service from the DI instead of 'modelsCache'
$products = Products::find(
[
'cache' => [
'key' => 'my-cache',
'service' => 'cache',
],
]
);
```
缓存也可以应用于使用关系生成的结果集:
```php
getComments(
[
'cache' => [
'key' => 'my-key',
],
]
);
// Get comments related to a post, setting lifetime
$comments = $post->getComments(
[
'cache' => [
'key' => 'my-key',
'lifetime' => 3600,
],
]
);
```
当缓存的结果集需要失效时,您只需使用上面指定的`key`从缓存中删除它。
在评估了应用程序的需求之后,缓存哪个结果集以及开发人员需要多长时间。不应缓存经常更改的结果集,因为缓存结果将很快失效。此外,缓存结果集会消耗处理周期,因此旨在加速应用程序的缓存实际上会降低其速度。应缓存不经常更改的结果集以最小化数据库交互。关于在何处使用缓存以及使用多长时间的决定取决于应用程序的需求。
## 强制缓存
之前我们看到了`Phalcon\Mvc\Model`如何与框架提供的缓存组件集成。为了使记录/结果集可缓存,我们在参数数组中传递密钥缓存:
```php
[
'key' => 'my-cache',
'lifetime' => 300,
],
]
);
```
这使我们可以自由地缓存特定查询,但是如果我们想要全局缓存在模型上执行的每个查询,我们可以覆盖`find()`/`findFirst()`方法来强制缓存每个查询:
```php
$value) {
if (is_scalar($value)) {
$uniqueKey[] = $key . ':' . $value;
} elseif (is_array($value)) {
$uniqueKey[] = $key . ':[' . self::_createKey($value) . ']';
}
}
return join(',', $uniqueKey);
}
public static function find($parameters = null)
{
// Convert the parameters to an array
if (!is_array($parameters)) {
$parameters = [$parameters];
}
// Check if a cache key wasn't passed
// and create the cache parameters
if (!isset($parameters['cache'])) {
$parameters['cache'] = [
'key' => self::_createKey($parameters),
'lifetime' => 300,
];
}
return parent::find($parameters);
}
public static function findFirst($parameters = null)
{
// ...
}
}
```
访问数据库比计算缓存键慢几倍。您可以自由地实施任何key生成策略,以更好地满足您的需求。请注意,良好的key尽可能避免冲突 - 这意味着不同的密钥应返回不相关的记录。
这使您可以完全控制如何为每个模型实现缓存。如果此策略对于多个模型是通用的,则可以为所有模型创建基类:
```php
modelsManager->createQuery($phql);
$query->cache(
[
'key' => 'cars-by-name',
'lifetime' => 300,
]
);
$cars = $query->execute(
[
'name' => 'Audi',
]
);
```
## 可重复使用的相关记录
某些模型可能与其他模型有关系。这允许我们轻松检查与内存中的实例相关的记录:
```php
customer;
// Print his/her name
echo $customer->name, "\n";
```
此示例非常简单,可以查询客户并根据需要使用,例如,显示其名称。如果我们检索一组发票以显示与这些发票相对应的客户,这也适用:
```php
customer;
// Print his/her name
echo $customer->name, "\n";
}
```
客户可能有一个或多个账单,因此,在此示例中,可能会多次不必要地查询相同的客户记录。为避免这种情况,我们可以将关系标记为可重用;通过这样做,我们告诉ORM自动重用内存中的记录,而不是一次又一次地重新查询它们:
```php
belongsTo(
'customers_id',
'Customer',
'id',
[
'reusable' => true,
]
);
}
}
```
请注意,此类缓存仅在内存中工作,这意味着在请求终止时释放缓存数据。
## 缓存相关记录
查询相关记录时,ORM在内部构建适当的条件,并根据下表使用目标模型中的 `find()`/`findFirst()` 获取所需的记录:
| 类型 | 描述 | 隐含方法|
| ---------- | --------------------------------------------------------------- | --------------- |
| Belongs-To | 直接返回相关记录的模型实例 | `findFirst()` |
| Has-One | 直接返回相关记录的模型实例 | `findFirst()` |
| Has-Many | 返回引用模型的模型实例的集合 | `find()` |
这意味着当您获得相关记录时,您可以通过实现相应的方法来拦截数据的获取方式:
```php
customer; // Invoices::findFirst('...');
// Same as above
$customer = $invoice->getCustomer(); // Invoices::findFirst('...');
```
因此,我们可以替换Invoices模型中的`findFirst() `方法,并实现我们认为最合适的缓存:
```php
customer;
// Assign it to the record
$invoice->customer = $customer;
$results[] = $invoice;
}
// Store the invoices in the cache + their customers
self::_setCache($key, $results);
return $results;
}
public function initialize()
{
// Add relations and initialize other stuff
}
}
```
从缓存中获取发票只需一次点击即可获得客户数据,从而降低了操作的总体开销。请注意,此过程也可以使用PHQL执行以下替代解决方案:
```php
getModelsManager()->executeQuery($phql);
$query->cache(
[
'key' => self::_createKey($conditions, $params),
'lifetime' => 300,
]
);
return $query->execute($params);
}
}
```
## 基于条件的缓存
在这种情况下,缓存的实现方式取决于收到的条件。我们可能会决定缓存后端应该由主键确定:
| Type | Cache Backend |
| ------------- | ------------- |
| 1 - 10000 | mongo1 |
| 10000 - 20000 | mongo2 |
| > 20000 | mongo3 |
实现此目的的最简单方法是向模型添加静态方法,以选择要使用的正确缓存:
```php
= 1 && $final < 10000) {
$service = 'mongo1';
} elseif ($initial >= 10000 && $final <= 20000) {
$service = 'mongo2';
} elseif ($initial > 20000) {
$service = 'mongo3';
}
return self::find(
[
'id >= ' . $initial . ' AND id <= ' . $final,
'cache' => [
'service' => $service,
],
]
);
}
}
```
这种方法解决了这个问题,但是,如果我们想要添加其他参数,例如命令或条件,我们将需要创建一个更复杂的方法。此外,如果使用相关记录或`find()`/`findFirst()`获取数据,则此方法不起作用:
```php
100 AND type = 'A'");
$robots = Robots::find("(id > 100 AND type = 'A') AND id < 2000");
$robots = Robots::find(
[
"(id > ?0 AND type = 'A') AND id < ?1",
'bind' => [100, 2000],
'order' => 'type',
]
);
```
为了实现这一点,我们需要拦截PHQL解析器生成的中间表示(IR),从而尽可能地自定义缓存:
第一个是创建自定义构建器,因此我们可以生成完全自定义的查询:
```php
getPhql());
$query->setDI($this->getDI());
if ( is_array($this->_bindParams) ) {
$query->setBindParams($this->_bindParams);
}
if ( is_array($this->_bindTypes) ) {
$query->setBindTypes($this->_bindTypes);
}
if ( is_array($this->_sharedLock) ) {
$query->setSharedLock($this->_sharedLock);
}
return $query;
}
}
```
我们的自定义构建器返回一个CustomQuery实例,而不是直接返回 `Phalcon\Mvc\Model\Query`,这个类看起来像:
```php
parse();
if ( is_array($this->_bindParams) ) {
$params = array_merge($this->_bindParams, (array)$params);
}
if ( is_array($this->_bindTypes) ) {
$types = array_merge($this->_bindTypes, (array)$types);
}
// Check if the query has conditions
if (isset($ir['where'])) {
// The fields in the conditions can have any order
// We need to recursively check the conditions tree
// to find the info we're looking for
$visitor = new CustomNodeVisitor();
// Recursively visits the nodes
$visitor->visit($ir['where']);
$initial = $visitor->getInitial();
$final = $visitor->getFinal();
// Select the cache according to the range
// ...
// Check if the cache has data
// ...
}
// Execute the query
$result = $this->_executeSelect($ir, $params, $types);
$result = $this->_uniqueRow ? $result->getFirst() : $result;
// Cache the result
// ...
return $result;
}
}
```
实现一个帮助程序(`CustomNodeVisitor`),它递归地检查条件,查找告诉我们在缓存中使用的可能范围的字段:
```php
visit($node['left']);
$right = $this->visit($node['right']);
if (!$left || !$right) {
return false;
}
if ($left === 'id') {
if ($node['op'] === '>') {
$this->_initial = $right;
}
if ($node['op'] === '=') {
$this->_initial = $right;
}
if ($node['op'] === '>=') {
$this->_initial = $right;
}
if ($node['op'] === '<') {
$this->_final = $right;
}
if ($node['op'] === '<=') {
$this->_final = $right;
}
}
break;
case 'qualified':
if ($node['name'] === 'id') {
return 'id';
}
break;
case 'literal':
return $node['value'];
default:
return false;
}
}
public function getInitial()
{
return $this->_initial;
}
public function getFinal()
{
return $this->_final;
}
}
```
最后,我们可以替换Robots模型中的find方法来使用我们创建的自定义类:
```php
from(get_called_class());
$query = $builder->getQuery();
if (isset($parameters['bind'])) {
return $query->execute($parameters['bind']);
} else {
return $query->execute();
}
}
}
```
## 缓存PHQL执行计划
与大多数现代数据库系统一样,PHQL在内部缓存执行计划,如果同一语句执行多次PHQL重用以前生成的计划提高性能,开发人员可以更好地利用这一点,强烈建议构建所有SQL语句传递变量参数作为绑定参数:
```php
modelsManager->executeQuery($phql);
// ...
}
```
在上面的示例中,生成了10个计划,增加了应用程序中的内存使用和处理。重写代码以利用绑定参数可以减少ORM和数据库系统的处理:
```php
modelsManager->executeQuery(
$phql,
[
$i,
]
);
// ...
}
```
重用PHQL查询还可以提高性能:
```php
modelsManager->createQuery($phql);
for ($i = 1; $i <= 10; $i++) {
$robots = $query->execute(
$phql,
[
$i,
]
);
// ...
}
```
大多数数据库系统也会缓存涉及[预处理语句](http://en.wikipedia.org/wiki/Prepared_statement)的查询的执行计划,从而减少总体执行时间,同时保护您的应用程序免受[SQL注入](http://en.wikipedia.org/wiki/SQL_injection)。
';