🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # ORM缓存 每个应用程序都不同。但是,在大多数应用程序中,有些数据不经常更改。性能方面最常见的瓶颈之一是访问数据库。这是由于PHP执行的复杂连接/通信过程以及从数据库获取数据的每个请求。因此,如果我们想要获得良好的性能,我们需要在应用程序需要的地方添加一些缓存层。 本章介绍了可以实现缓存以提高性能的潜在领域。Phalcon为开发人员提供了在应用程序需要时实现缓存所需的工具。 ## 缓存结果集 避免持续访问数据库的一种成熟技术是使用具有更快访问权限的系统(通常是内存)来缓存不经常更改的结果集。 当`Phalcon\Mvc\Model`需要服务来缓存结果集时,它将从依赖注入容器中请求它。服务名称称为modelsCache。Phalcon提供了一个可以存储任何类型数据的缓存组件。我们现在将看到如何将它与我们的模型集成。 首先,我们需要将缓存组件注册为DI容器中的服务。 ```php <?php use Phalcon\Cache\Frontend\Data as FrontendData; use Phalcon\Cache\Backend\Memcache as BackendMemcache; // 设置模型缓存服务 $di->set( 'modelsCache', function () { // 缓存数据一天(默认设置) $frontCache = new FrontendData( [ 'lifetime' => 86400, ] ); // Memcached连接设置 $cache = new BackendMemcache( $frontCache, [ 'host' => 'localhost', 'port' => '11211', ] ); return $cache; } ); ``` 在将缓存组件注册为DI容器中的服务之前,Phalcon提供了对缓存组件的创建和自定义的完全控制。正确设置缓存组件后,可以按如下方式缓存结果集: ```php <?php // Get products without caching $products = Products::find(); // Just cache the resultset. The cache will expire in 1 hour (3600 seconds) $products = Products::find( [ 'cache' => [ '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 <?php // Query some post $post = Post::findFirst(); // Get comments related to a post, also cache it $comments = $post->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 <?php // Cache the resultset for only for 5 minutes $products = Products::find( [ 'cache' => [ 'key' => 'my-cache', 'lifetime' => 300, ], ] ); ``` 这使我们可以自由地缓存特定查询,但是如果我们想要全局缓存在模型上执行的每个查询,我们可以覆盖`find()`/`findFirst()`方法来强制缓存每个查询: ```php <?php use Phalcon\Mvc\Model; class Robots extends Model { /** * Implement a method that returns a string key based * on the query parameters */ protected static function _createKey($parameters) { $uniqueKey = []; foreach ($parameters as $key => $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 <?php use Phalcon\Mvc\Model; class CacheableModel extends Model { protected static function _createKey($parameters) { // ... Create a cache key based on the parameters } public static function find($parameters = null) { // ... Custom caching strategy } public static function findFirst($parameters = null) { // ... Custom caching strategy } } ``` 然后将此类用作每个 `Cacheable` 模型的基类: ```php <?php class Robots extends CacheableModel { } ``` ## 缓存PHQL查询 无论我们用于创建它们的语法如何,ORM中的所有查询都是使用PHQL在内部处理的。这种语言使您可以更自由地创建各种查询。当然,这些查询可以缓存: ```php <?php $phql = 'SELECT * FROM Cars WHERE name = :name:'; $query = $this->modelsManager->createQuery($phql); $query->cache( [ 'key' => 'cars-by-name', 'lifetime' => 300, ] ); $cars = $query->execute( [ 'name' => 'Audi', ] ); ``` ## 可重复使用的相关记录 某些模型可能与其他模型有关系。这允许我们轻松检查与内存中的实例相关的记录: ```php <?php // Get some invoice $invoice = Invoices::findFirst(); // Get the customer related to the invoice $customer = $invoice->customer; // Print his/her name echo $customer->name, "\n"; ``` 此示例非常简单,可以查询客户并根据需要使用,例如,显示其名称。如果我们检索一组发票以显示与这些发票相对应的客户,这也适用: ```php <?php // Get a set of invoices // SELECT * FROM invoices; $invoices = Invoices::find(); foreach ($invoices as $invoice) { // Get the customer related to the invoice // SELECT * FROM customers WHERE id = ?; $customer = $invoice->customer; // Print his/her name echo $customer->name, "\n"; } ``` 客户可能有一个或多个账单,因此,在此示例中,可能会多次不必要地查询相同的客户记录。为避免这种情况,我们可以将关系标记为可重用;通过这样做,我们告诉ORM自动重用内存中的记录,而不是一次又一次地重新查询它们: ```php <?php use Phalcon\Mvc\Model; class Invoices extends Model { public function initialize() { $this->belongsTo( 'customers_id', 'Customer', 'id', [ 'reusable' => true, ] ); } } ``` 请注意,此类缓存仅在内存中工作,这意味着在请求终止时释放缓存数据。 ## 缓存相关记录 查询相关记录时,ORM在内部构建适当的条件,并根据下表使用目标模型中的 `find()`/`findFirst()` 获取所需的记录: | 类型 | 描述 | 隐含方法| | ---------- | --------------------------------------------------------------- | --------------- | | Belongs-To | 直接返回相关记录的模型实例 | `findFirst()` | | Has-One | 直接返回相关记录的模型实例 | `findFirst()` | | Has-Many | 返回引用模型的模型实例的集合 | `find()` | 这意味着当您获得相关记录时,您可以通过实现相应的方法来拦截数据的获取方式: ```php <?php // Get some invoice $invoice = Invoices::findFirst(); // Get the customer related to the invoice $customer = $invoice->customer; // Invoices::findFirst('...'); // Same as above $customer = $invoice->getCustomer(); // Invoices::findFirst('...'); ``` 因此,我们可以替换Invoices模型中的`findFirst() `方法,并实现我们认为最合适的缓存: ```php <?php use Phalcon\Mvc\Model; class Invoices extends Model { public static function findFirst($parameters = null) { // ... Custom caching strategy } } ``` ## 递归缓存相关记录 在这种情况下,我们假设每次查询结果时,我们也会检索其关联的记录。如果我们存储与其相关实体一起找到的记录,也许我们可以减少获取所有实体所需的开销: ```php <?php use Phalcon\Mvc\Model; class Invoices extends Model { protected static function _createKey($parameters) { // ... Create a cache key based on the parameters } protected static function _getCache($key) { // Returns data from a cache } protected static function _setCache($key, $results) { // Stores data in the cache } public static function find($parameters = null) { // Create a unique key $key = self::_createKey($parameters); // Check if there are data in the cache $results = self::_getCache($key); // Valid data is an object if (is_object($results)) { return $results; } $results = []; $invoices = parent::find($parameters); foreach ($invoices as $invoice) { // Query the related customer $customer = $invoice->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 <?php use Phalcon\Mvc\Model; class Invoices extends Model { public function initialize() { // Add relations and initialize other stuff } protected static function _createKey($conditions, $params) { // ... Create a cache key based on the parameters } public function getInvoicesCustomers($conditions, $params = null) { $phql = 'SELECT Invoices.*, Customers.* FROM Invoices JOIN Customers WHERE ' . $conditions; $query = $this->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 <?php use Phalcon\Mvc\Model; class Robots extends Model { public static function queryCache($initial, $final) { if ($initial >= 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 <?php $robots = Robots::find('id < 1000'); $robots = Robots::find("id > 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 <?php use Phalcon\Mvc\Model\Query\Builder as QueryBuilder; class CustomQueryBuilder extends QueryBuilder { public function getQuery() { $query = new CustomQuery($this->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 <?php use Phalcon\Mvc\Model\Query as ModelQuery; class CustomQuery extends ModelQuery { /** * The execute method is overridden */ public function execute($params = null, $types = null) { // Parse the intermediate representation for the SELECT $ir = $this->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 <?php class CustomNodeVisitor { protected $_initial = 0; protected $_final = 25000; public function visit($node) { switch ($node['type']) { case 'binary-op': $left = $this->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 <?php use Phalcon\Mvc\Model; class Robots extends Model { public static function find($parameters = null) { if (!is_array($parameters)) { $parameters = [$parameters]; } $builder = new CustomQueryBuilder($parameters); $builder->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 <?php for ($i = 1; $i <= 10; $i++) { $phql = 'SELECT * FROM Store\Robots WHERE id = ' . $i; $robots = $this->modelsManager->executeQuery($phql); // ... } ``` 在上面的示例中,生成了10个计划,增加了应用程序中的内存使用和处理。重写代码以利用绑定参数可以减少ORM和数据库系统的处理: ```php <?php $phql = 'SELECT * FROM Store\Robots WHERE id = ?0'; for ($i = 1; $i <= 10; $i++) { $robots = $this->modelsManager->executeQuery( $phql, [ $i, ] ); // ... } ``` 重用PHQL查询还可以提高性能: ```php <?php $phql = 'SELECT * FROM Store\Robots WHERE id = ?0'; $query = $this->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)。