使用高速缓存提高性能

缓存软件设计模式是你存储一个需要长时间才能生成的结果。这可能是一个冗长的视图脚本或复杂的数据库查询的形式。当然,如果你希望改善网站访问者的用户体验,那么存储目标需要具有很高的性能。由于不同的安装会有不同的潜在存储目标,缓存机制也适合于适配器模式。潜在的存储目标的例子包括内存、数据库和文件系统。

如何做…

1.与本章的其他几个示例一样,由于有共享的常量,我们定义了一个谨慎的Application\Cache\Constants类。

  1. <?php
  2. namespace Application\Cache;
  3. class Constants
  4. {
  5. const DEFAULT_GROUP = 'default';
  6. const DEFAULT_PREFIX = 'CACHE_';
  7. const DEFAULT_SUFFIX = '.cache';
  8. const ERROR_GET = 'ERROR: unable to retrieve from cache';
  9. // not all constants are shown to conserve space
  10. }
  1. 由于我们遵循的是适配器设计模式,所以接下来我们定义一个接口。
  1. namespace Application\Cache;
  2. interface CacheAdapterInterface
  3. {
  4. public function hasKey($key);
  5. public function getFromCache($key, $group);
  6. public function saveToCache($key, $data, $group);
  7. public function removeByKey($key);
  8. public function removeByGroup($group);
  9. }
  1. 现在我们准备好定义我们的第一个缓存适配器,在这个例子中,通过使用MySQL数据库。我们需要定义一些属性,这些属性将保存列名以及准备好的语句。
  1. namespace Application\Cache;
  2. use PDO;
  3. use Application\Database\Connection;
  4. class Database implements CacheAdapterInterface
  5. {
  6. protected $sql;
  7. protected $connection;
  8. protected $table;
  9. protected $dataColumnName;
  10. protected $keyColumnName;
  11. protected $groupColumnName;
  12. protected $statementHasKey = NULL;
  13. protected $statementGetFromCache = NULL;
  14. protected $statementSaveToCache = NULL;
  15. protected $statementRemoveByKey = NULL;
  16. protected $statementRemoveByGroup= NULL;
  1. 构造函数允许我们提供关键列名以及 Application\Database\Connection 实例和用于缓存的表的名称。
  1. public function __construct(Connection $connection,
  2. $table,
  3. $idColumnName,
  4. $keyColumnName,
  5. $dataColumnName,
  6. $groupColumnName = Constants::DEFAULT_GROUP)
  7. {
  8. $this->connection = $connection;
  9. $this->setTable($table);
  10. $this->setIdColumnName($idColumnName);
  11. $this->setDataColumnName($dataColumnName);
  12. $this->setKeyColumnName($keyColumnName);
  13. $this->setGroupColumnName($groupColumnName);
  14. }
  1. 接下来的几个方法是准备语句,并在我们访问数据库时被调用。我们没有展示所有的方法,但足够给你一个概念。
  1. public function prepareHasKey()
  2. {
  3. $sql = 'SELECT `' . $this->idColumnName . '` '
  4. . 'FROM `' . $this->table . '` '
  5. . 'WHERE `' . $this->keyColumnName . '` = :key ';
  6. $this->sql[__METHOD__] = $sql;
  7. $this->statementHasKey =
  8. $this->connection->pdo->prepare($sql);
  9. }
  10. public function prepareGetFromCache()
  11. {
  12. $sql = 'SELECT `' . $this->dataColumnName . '` '
  13. . 'FROM `' . $this->table . '` '
  14. . 'WHERE `' . $this->keyColumnName . '` = :key '
  15. . 'AND `' . $this->groupColumnName . '` = :group';
  16. $this->sql[__METHOD__] = $sql;
  17. $this->statementGetFromCache =
  18. $this->connection->pdo->prepare($sql);
  19. }
  1. 现在,我们定义一个方法来确定给定键的数据是否存在。
  1. public function hasKey($key)
  2. {
  3. $result = 0;
  4. try {
  5. if (!$this->statementHasKey) $this->prepareHasKey();
  6. $this->statementHasKey->execute(['key' => $key]);
  7. } catch (Throwable $e) {
  8. error_log(__METHOD__ . ':' . $e->getMessage());
  9. throw new Exception(Constants::ERROR_REMOVE_KEY);
  10. }
  11. return (int) $this->statementHasKey
  12. ->fetch(PDO::FETCH_ASSOC)[$this->idColumnName];
  13. }
  1. 核心方法是从缓存中读取和写入缓存的方法。这里是从缓存中检索的方法。我们需要做的就是执行准备好的语句,执行一个SELECT,其中有一个WHERE子句,其中包含了key和group。
  1. public function getFromCache(
  2. $key, $group = Constants::DEFAULT_GROUP)
  3. {
  4. try {
  5. if (!$this->statementGetFromCache)
  6. $this->prepareGetFromCache();
  7. $this->statementGetFromCache->execute(
  8. ['key' => $key, 'group' => $group]);
  9. while ($row = $this->statementGetFromCache
  10. ->fetch(PDO::FETCH_ASSOC)) {
  11. if ($row && count($row)) {
  12. yield unserialize($row[$this->dataColumnName]);
  13. }
  14. }
  15. } catch (Throwable $e) {
  16. error_log(__METHOD__ . ':' . $e->getMessage());
  17. throw new Exception(Constants::ERROR_GET);
  18. }
  19. }
  1. 当向缓存写入时,我们首先确定这个缓存键的条目是否存在。如果存在,我们执行UPDATE;否则,我们执行INSERT
  1. public function saveToCache($key, $data, $group = Constants::DEFAULT_GROUP)
  2. {
  3. $id = $this->hasKey($key);
  4. $result = 0;
  5. try {
  6. if ($id) {
  7. if (!$this->statementUpdateCache)
  8. $this->prepareUpdateCache();
  9. $result = $this->statementUpdateCache
  10. ->execute(['key' => $key,
  11. 'data' => serialize($data),
  12. 'group' => $group,
  13. 'id' => $id]);
  14. } else {
  15. if (!$this->statementSaveToCache)
  16. $this->prepareSaveToCache();
  17. $result = $this->statementSaveToCache
  18. ->execute(['key' => $key,
  19. 'data' => serialize($data),
  20. 'group' => $group]);
  21. }
  22. } catch (Throwable $e) {
  23. error_log(__METHOD__ . ':' . $e->getMessage());
  24. throw new Exception(Constants::ERROR_SAVE);
  25. }
  26. return $result;
  27. }
  1. 然后,我们定义了两种方法,可以按键或按组删除缓存。如果有大量的项目需要删除,按组删除是一个方便机制。
  1. public function removeByKey($key)
  2. {
  3. $result = 0;
  4. try {
  5. if (!$this->statementRemoveByKey)
  6. $this->prepareRemoveByKey();
  7. $result = $this->statementRemoveByKey->execute(
  8. ['key' => $key]);
  9. } catch (Throwable $e) {
  10. error_log(__METHOD__ . ':' . $e->getMessage());
  11. throw new Exception(Constants::ERROR_REMOVE_KEY);
  12. }
  13. return $result;
  14. }
  15. public function removeByGroup($group)
  16. {
  17. $result = 0;
  18. try {
  19. if (!$this->statementRemoveByGroup)
  20. $this->prepareRemoveByGroup();
  21. $result = $this->statementRemoveByGroup->execute(
  22. ['group' => $group]);
  23. } catch (Throwable $e) {
  24. error_log(__METHOD__ . ':' . $e->getMessage());
  25. throw new Exception(Constants::ERROR_REMOVE_GROUP);
  26. }
  27. return $result;
  28. }
  1. 最后,我们为每个属性定义getter和setter。为了节省篇幅,这里没有全部显示。
  1. public function setTable($name)
  2. {
  3. $this->table = $name;
  4. }
  5. public function getTable()
  6. {
  7. return $this->table;
  8. }
  9. // etc.
  10. }
  1. 文件系统缓存适配器定义了与前面定义的相同的方法。请注意md5()的使用,不是为了安全,而是作为一种从密钥中快速生成文本字符串的方法。
  1. namespace Application\Cache;
  2. use RecursiveIteratorIterator;
  3. use RecursiveDirectoryIterator;
  4. class File implements CacheAdapterInterface
  5. {
  6. protected $dir;
  7. protected $prefix;
  8. protected $suffix;
  9. public function __construct(
  10. $dir, $prefix = NULL, $suffix = NULL)
  11. {
  12. if (!file_exists($dir)) {
  13. error_log(__METHOD__ . ':' . Constants::ERROR_DIR_NOT);
  14. throw new Exception(Constants::ERROR_DIR_NOT);
  15. }
  16. $this->dir = $dir;
  17. $this->prefix = $prefix ?? Constants::DEFAULT_PREFIX;
  18. $this->suffix = $suffix ?? Constants::DEFAULT_SUFFIX;
  19. }
  20. public function hasKey($key)
  21. {
  22. $action = function ($name, $md5Key, &$item) {
  23. if (strpos($name, $md5Key) !== FALSE) {
  24. $item ++;
  25. }
  26. };
  27. return $this->findKey($key, $action);
  28. }
  29. public function getFromCache($key, $group = Constants::DEFAULT_GROUP)
  30. {
  31. $fn = $this->dir . '/' . $group . '/'
  32. . $this->prefix . md5($key) . $this->suffix;
  33. if (file_exists($fn)) {
  34. foreach (file($fn) as $line) { yield $line; }
  35. } else {
  36. return array();
  37. }
  38. }
  39. public function saveToCache(
  40. $key, $data, $group = Constants::DEFAULT_GROUP)
  41. {
  42. $baseDir = $this->dir . '/' . $group;
  43. if (!file_exists($baseDir)) mkdir($baseDir);
  44. $fn = $baseDir . '/' . $this->prefix . md5($key)
  45. . $this->suffix;
  46. return file_put_contents($fn, json_encode($data));
  47. }
  48. protected function findKey($key, callable $action)
  49. {
  50. $md5Key = md5($key);
  51. $iterator = new RecursiveIteratorIterator(
  52. new RecursiveDirectoryIterator($this->dir),
  53. RecursiveIteratorIterator::SELF_FIRST);
  54. $item = 0;
  55. foreach ($iterator as $name => $obj) {
  56. $action($name, $md5Key, $item);
  57. }
  58. return $item;
  59. }
  60. public function removeByKey($key)
  61. {
  62. $action = function ($name, $md5Key, &$item) {
  63. if (strpos($name, $md5Key) !== FALSE) {
  64. unlink($name);
  65. $item++;
  66. }
  67. };
  68. return $this->findKey($key, $action);
  69. }
  70. public function removeByGroup($group)
  71. {
  72. $removed = 0;
  73. $baseDir = $this->dir . '/' . $group;
  74. $pattern = $baseDir . '/' . $this->prefix . '*'
  75. . $this->suffix;
  76. foreach (glob($pattern) as $file) {
  77. unlink($file);
  78. $removed++;
  79. }
  80. return $removed;
  81. }
  82. }
  1. 现在我们准备介绍核心的缓存机制。在构造函数中,我们接受一个实现CacheAdapterInterface的类作为参数。
  1. namespace Application\Cache;
  2. use Psr\Http\Message\RequestInterface;
  3. use Application\MiddleWare\ { Request, Response, TextStream };
  4. class Core
  5. {
  6. public function __construct(CacheAdapterInterface $adapter)
  7. {
  8. $this->adapter = $adapter;
  9. }
  1. 接下来是一系列的包装方法,它们从适配器中调用相同名称的方法,但接受一个Psr\Http\Message\RequestInterface类作为参数,并返回一个Psr\Http\Message\ResponseInterface作为响应。我们从一个简单的:hasKey()开始。请注意我们如何从请求参数中提取密钥。
  1. public function hasKey(RequestInterface $request)
  2. {
  3. $key = $request->getUri()->getQueryParams()['key'] ?? '';
  4. $result = $this->adapter->hasKey($key);
  5. }
  1. 为了从缓存中检索信息,我们需要从请求对象中提取键和组参数,然后从适配器中调用同样的方法。如果没有得到结果,我们设置一个204代码,表示请求成功,但没有产生内容。否则,我们设置一个200(成功)代码,并对结果进行迭代。然后,所有的内容都会被塞进一个响应对象,并被返回。
  1. public function getFromCache(RequestInterface $request)
  2. {
  3. $text = array();
  4. $key = $request->getUri()->getQueryParams()['key'] ?? '';
  5. $group = $request->getUri()->getQueryParams()['group']
  6. ?? Constants::DEFAULT_GROUP;
  7. $results = $this->adapter->getFromCache($key, $group);
  8. if (!$results) {
  9. $code = 204;
  10. } else {
  11. $code = 200;
  12. foreach ($results as $line) $text[] = $line;
  13. }
  14. if (!$text || count($text) == 0) $code = 204;
  15. $body = new TextStream(json_encode($text));
  16. return (new Response())->withStatus($code)
  17. ->withBody($body);
  18. }
  1. 奇怪的是,向缓存写入的结果几乎是一样的,只是希望结果是一个数字(即受影响的行数),或者是一个布尔结果。
  1. public function saveToCache(RequestInterface $request)
  2. {
  3. $text = array();
  4. $key = $request->getUri()->getQueryParams()['key'] ?? '';
  5. $group = $request->getUri()->getQueryParams()['group']
  6. ?? Constants::DEFAULT_GROUP;
  7. $data = $request->getBody()->getContents();
  8. $results = $this->adapter->saveToCache($key, $data, $group);
  9. if (!$results) {
  10. $code = 204;
  11. } else {
  12. $code = 200;
  13. $text[] = $results;
  14. }
  15. $body = new TextStream(json_encode($text));
  16. return (new Response())->withStatus($code)
  17. ->withBody($body);
  18. }
  1. 正如预期的那样,移除的方法非常相似。
  1. public function removeByKey(RequestInterface $request)
  2. {
  3. $text = array();
  4. $key = $request->getUri()->getQueryParams()['key'] ?? '';
  5. $results = $this->adapter->removeByKey($key);
  6. if (!$results) {
  7. $code = 204;
  8. } else {
  9. $code = 200;
  10. $text[] = $results;
  11. }
  12. $body = new TextStream(json_encode($text));
  13. return (new Response())->withStatus($code)
  14. ->withBody($body);
  15. }
  16. public function removeByGroup(RequestInterface $request)
  17. {
  18. $text = array();
  19. $group = $request->getUri()->getQueryParams()['group']
  20. ?? Constants::DEFAULT_GROUP;
  21. $results = $this->adapter->removeByGroup($group);
  22. if (!$results) {
  23. $code = 204;
  24. } else {
  25. $code = 200;
  26. $text[] = $results;
  27. }
  28. $body = new TextStream(json_encode($text));
  29. return (new Response())->withStatus($code)
  30. ->withBody($body);
  31. }
  32. } // closing brace for class Core

如何运行…

为了演示Acl类的使用,你需要定义本示例中所描述的类,在这里总结一下。

Class 在这些步骤中讨论
Application\Cache\Constants 1
Application\Cache\CacheAdapterInterface 2
Application\Cache\Database 3 - 10
Application\Cache\File 11
Application\Cache\Core 12 - 16

接下来,定义一个测试程序,你可以把它叫做 chap_09_middleware_cache_db.php。在这个程序中,像往常一样,为必要的文件定义常量,设置自动加载,使用适当的类,哦……并写一个产生质数的函数(你可能在这一点上重新阅读最后一点。不用担心,我们可以帮助你!

  1. <?php
  2. define('DB_CONFIG_FILE', __DIR__ . '/../config/db.config.php');
  3. define('DB_TABLE', 'cache');
  4. define('CACHE_DIR', __DIR__ . '/cache');
  5. define('MAX_NUM', 100000);
  6. require __DIR__ . '/../Application/Autoload/Loader.php';
  7. Application\Autoload\Loader::init(__DIR__ . '/..');
  8. use Application\Database\Connection;
  9. use Application\Cache\{ Constants, Core, Database, File };
  10. use Application\MiddleWare\ { Request, TextStream };

好吧,需要一个运行时间长的函数,那么质数生成器,我们来了! 数字1、2和3是质数。我们使用 PHP 7 的 yield from 语法来生成前三个数。然后,我们直接跳到 5,并继续往前走,直到要求的最大值。

  1. function generatePrimes($max)
  2. {
  3. yield from [1,2,3];
  4. for ($x = 5; $x < $max; $x++)
  5. {
  6. if($x & 1) {
  7. $prime = TRUE;
  8. for($i = 3; $i < $x; $i++) {
  9. if(($x % $i) === 0) {
  10. $prime = FALSE;
  11. break;
  12. }
  13. }
  14. if ($prime) yield $x;
  15. }
  16. }
  17. }

然后你可以设置一个数据库缓存适配器实例,作为核心的参数。

  1. $conn = new Connection(include DB_CONFIG_FILE);
  2. $dbCache = new Database(
  3. $conn, DB_TABLE, 'id', 'key', 'data', 'group');
  4. $core = new Core($dbCache);

另外,如果你想使用文件缓存适配器,这里有相应的代码。

  1. $fileCache = new File(CACHE_DIR);
  2. $core = new Core($fileCache);

如果你想清除缓存,可以这样做。

  1. $uriString = '/?group=' . Constants::DEFAULT_GROUP;
  2. $cacheRequest = new Request($uriString, 'get');
  3. $response = $core->removeByGroup($cacheRequest);

你可以使用 time()microtime() 来查看这个脚本在有缓存和没有缓存的情况下运行了多久。

  1. $start = time() + microtime(TRUE);
  2. echo "\nTime: " . $start;

接下来,生成一个缓存请求。状态码为200表示你能够从缓存中获得一个质数列表。

  1. $uriString = '/?key=Test1';
  2. $cacheRequest = new Request($uriString, 'get');
  3. $response = $core->getFromCache($cacheRequest);
  4. $status = $response->getStatusCode();
  5. if ($status == 200) {
  6. $primes = json_decode($response->getBody()->getContents());

否则,你可以认为没有从缓存中获得任何东西,这意味着你需要生成质数,并将结果保存到缓存中。

  1. } else {
  2. $primes = array();
  3. foreach (generatePrimes(MAX_NUM) as $num) {
  4. $primes[] = $num;
  5. }
  6. $body = new TextStream(json_encode($primes));
  7. $response = $core->saveToCache(
  8. $cacheRequest->withBody($body));
  9. }

然后,你可以检查停止时间,计算差值,并看看你的新质数列表。

  1. $time = time() + microtime(TRUE);
  2. $diff = $time - $start;
  3. echo "\nTime: $time";
  4. echo "\nDifference: $diff";
  5. var_dump($primes);

这里是在缓存中存储值之前的预期输出。

使用高速缓存提高性能 - 图1

现在你可以再次运行相同的程序,这次是从缓存中检索。

使用高速缓存提高性能 - 图2

考虑到我们的小质数发生器并不是世界上最高效的,也考虑到演示是在笔记本电脑上运行的,时间从30多秒降到了毫秒。

更多…

另一个可能的缓存适配器可以围绕着备用PHP缓存(APC)扩展中的命令来构建。这个扩展包括诸如 apc_exists(), apc_store(), apc_fetch()apc_clear_cache()等函数。这些函数非常适合我们的hasKey()saveToCache()getFromCache()removeBy*()函数。

另见

你可以考虑按照PSR-6对之前描述的缓存适配器类进行轻微的修改,这是一个针对缓存的标准建议。然而,这个标准的接受程度并不像PSR-7那样,所以我们决定在这里介绍的示例中不完全遵循这个标准。有关PSR-6的更多信息,请参考http://www.php-fig.org/psr/psr-6/