DatabaseBackend.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <?php
  2. namespace Drupal\Core\Cache;
  3. use Drupal\Component\Assertion\Inspector;
  4. use Drupal\Component\Utility\Crypt;
  5. use Drupal\Core\Database\Connection;
  6. use Drupal\Core\Database\DatabaseException;
  7. /**
  8. * Defines a default cache implementation.
  9. *
  10. * This is Drupal's default cache implementation. It uses the database to store
  11. * cached data. Each cache bin corresponds to a database table by the same name.
  12. *
  13. * @ingroup cache
  14. */
  15. class DatabaseBackend implements CacheBackendInterface {
  16. /**
  17. * The default maximum number of rows that this cache bin table can store.
  18. *
  19. * This maximum is introduced to ensure that the database is not filled with
  20. * hundred of thousand of cache entries with gigabytes in size.
  21. *
  22. * Read about how to change it in the @link cache Cache API topic. @endlink
  23. */
  24. const DEFAULT_MAX_ROWS = 5000;
  25. /**
  26. * -1 means infinite allows numbers of rows for the cache backend.
  27. */
  28. const MAXIMUM_NONE = -1;
  29. /**
  30. * The maximum number of rows that this cache bin table is allowed to store.
  31. *
  32. * @see ::MAXIMUM_NONE
  33. *
  34. * @var int
  35. */
  36. protected $maxRows;
  37. /**
  38. * @var string
  39. */
  40. protected $bin;
  41. /**
  42. * The database connection.
  43. *
  44. * @var \Drupal\Core\Database\Connection
  45. */
  46. protected $connection;
  47. /**
  48. * The cache tags checksum provider.
  49. *
  50. * @var \Drupal\Core\Cache\CacheTagsChecksumInterface
  51. */
  52. protected $checksumProvider;
  53. /**
  54. * Constructs a DatabaseBackend object.
  55. *
  56. * @param \Drupal\Core\Database\Connection $connection
  57. * The database connection.
  58. * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
  59. * The cache tags checksum provider.
  60. * @param string $bin
  61. * The cache bin for which the object is created.
  62. * @param int $max_rows
  63. * (optional) The maximum number of rows that are allowed in this cache bin
  64. * table.
  65. */
  66. public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin, $max_rows = NULL) {
  67. // All cache tables should be prefixed with 'cache_'.
  68. $bin = 'cache_' . $bin;
  69. $this->bin = $bin;
  70. $this->connection = $connection;
  71. $this->checksumProvider = $checksum_provider;
  72. $this->maxRows = $max_rows === NULL ? static::DEFAULT_MAX_ROWS : $max_rows;
  73. }
  74. /**
  75. * {@inheritdoc}
  76. */
  77. public function get($cid, $allow_invalid = FALSE) {
  78. $cids = [$cid];
  79. $cache = $this->getMultiple($cids, $allow_invalid);
  80. return reset($cache);
  81. }
  82. /**
  83. * {@inheritdoc}
  84. */
  85. public function getMultiple(&$cids, $allow_invalid = FALSE) {
  86. $cid_mapping = [];
  87. foreach ($cids as $cid) {
  88. $cid_mapping[$this->normalizeCid($cid)] = $cid;
  89. }
  90. // When serving cached pages, the overhead of using ::select() was found
  91. // to add around 30% overhead to the request. Since $this->bin is a
  92. // variable, this means the call to ::query() here uses a concatenated
  93. // string. This is highly discouraged under any other circumstances, and
  94. // is used here only due to the performance overhead we would incur
  95. // otherwise. When serving an uncached page, the overhead of using
  96. // ::select() is a much smaller proportion of the request.
  97. $result = [];
  98. try {
  99. $result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN ( :cids[] ) ORDER BY cid', [':cids[]' => array_keys($cid_mapping)]);
  100. }
  101. catch (\Exception $e) {
  102. // Nothing to do.
  103. }
  104. $cache = [];
  105. foreach ($result as $item) {
  106. // Map the cache ID back to the original.
  107. $item->cid = $cid_mapping[$item->cid];
  108. $item = $this->prepareItem($item, $allow_invalid);
  109. if ($item) {
  110. $cache[$item->cid] = $item;
  111. }
  112. }
  113. $cids = array_diff($cids, array_keys($cache));
  114. return $cache;
  115. }
  116. /**
  117. * Prepares a cached item.
  118. *
  119. * Checks that items are either permanent or did not expire, and unserializes
  120. * data as appropriate.
  121. *
  122. * @param object $cache
  123. * An item loaded from self::get() or self::getMultiple().
  124. * @param bool $allow_invalid
  125. * If FALSE, the method returns FALSE if the cache item is not valid.
  126. *
  127. * @return mixed|false
  128. * The item with data unserialized as appropriate and a property indicating
  129. * whether the item is valid, or FALSE if there is no valid item to load.
  130. */
  131. protected function prepareItem($cache, $allow_invalid) {
  132. if (!isset($cache->data)) {
  133. return FALSE;
  134. }
  135. $cache->tags = $cache->tags ? explode(' ', $cache->tags) : [];
  136. // Check expire time.
  137. $cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME;
  138. // Check if invalidateTags() has been called with any of the items's tags.
  139. if (!$this->checksumProvider->isValid($cache->checksum, $cache->tags)) {
  140. $cache->valid = FALSE;
  141. }
  142. if (!$allow_invalid && !$cache->valid) {
  143. return FALSE;
  144. }
  145. // Unserialize and return the cached data.
  146. if ($cache->serialized) {
  147. $cache->data = unserialize($cache->data);
  148. }
  149. return $cache;
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
  155. $this->setMultiple([
  156. $cid => [
  157. 'data' => $data,
  158. 'expire' => $expire,
  159. 'tags' => $tags,
  160. ],
  161. ]);
  162. }
  163. /**
  164. * {@inheritdoc}
  165. */
  166. public function setMultiple(array $items) {
  167. $try_again = FALSE;
  168. try {
  169. // The bin might not yet exist.
  170. $this->doSetMultiple($items);
  171. }
  172. catch (\Exception $e) {
  173. // If there was an exception, try to create the bins.
  174. if (!$try_again = $this->ensureBinExists()) {
  175. // If the exception happened for other reason than the missing bin
  176. // table, propagate the exception.
  177. throw $e;
  178. }
  179. }
  180. // Now that the bin has been created, try again if necessary.
  181. if ($try_again) {
  182. $this->doSetMultiple($items);
  183. }
  184. }
  185. /**
  186. * Stores multiple items in the persistent cache.
  187. *
  188. * @param array $items
  189. * An array of cache items, keyed by cid.
  190. *
  191. * @see \Drupal\Core\Cache\CacheBackendInterface::setMultiple()
  192. */
  193. protected function doSetMultiple(array $items) {
  194. $values = [];
  195. foreach ($items as $cid => $item) {
  196. $item += [
  197. 'expire' => CacheBackendInterface::CACHE_PERMANENT,
  198. 'tags' => [],
  199. ];
  200. assert(Inspector::assertAllStrings($item['tags']), 'Cache Tags must be strings.');
  201. $item['tags'] = array_unique($item['tags']);
  202. // Sort the cache tags so that they are stored consistently in the DB.
  203. sort($item['tags']);
  204. $fields = [
  205. 'cid' => $this->normalizeCid($cid),
  206. 'expire' => $item['expire'],
  207. 'created' => round(microtime(TRUE), 3),
  208. 'tags' => implode(' ', $item['tags']),
  209. 'checksum' => $this->checksumProvider->getCurrentChecksum($item['tags']),
  210. ];
  211. // Avoid useless writes.
  212. if ($fields['checksum'] === CacheTagsChecksumInterface::INVALID_CHECKSUM_WHILE_IN_TRANSACTION) {
  213. continue;
  214. }
  215. if (!is_string($item['data'])) {
  216. $fields['data'] = serialize($item['data']);
  217. $fields['serialized'] = 1;
  218. }
  219. else {
  220. $fields['data'] = $item['data'];
  221. $fields['serialized'] = 0;
  222. }
  223. $values[] = $fields;
  224. }
  225. // If all $items were useless writes, we may end up with zero writes.
  226. if (empty($values)) {
  227. return;
  228. }
  229. // Use an upsert query which is atomic and optimized for multiple-row
  230. // merges.
  231. $query = $this->connection
  232. ->upsert($this->bin)
  233. ->key('cid')
  234. ->fields(['cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized']);
  235. foreach ($values as $fields) {
  236. // Only pass the values since the order of $fields matches the order of
  237. // the insert fields. This is a performance optimization to avoid
  238. // unnecessary loops within the method.
  239. $query->values(array_values($fields));
  240. }
  241. $query->execute();
  242. }
  243. /**
  244. * {@inheritdoc}
  245. */
  246. public function delete($cid) {
  247. $this->deleteMultiple([$cid]);
  248. }
  249. /**
  250. * {@inheritdoc}
  251. */
  252. public function deleteMultiple(array $cids) {
  253. $cids = array_values(array_map([$this, 'normalizeCid'], $cids));
  254. try {
  255. // Delete in chunks when a large array is passed.
  256. foreach (array_chunk($cids, 1000) as $cids_chunk) {
  257. $this->connection->delete($this->bin)
  258. ->condition('cid', $cids_chunk, 'IN')
  259. ->execute();
  260. }
  261. }
  262. catch (\Exception $e) {
  263. // Create the cache table, which will be empty. This fixes cases during
  264. // core install where a cache table is cleared before it is set
  265. // with {cache_render} and {cache_data}.
  266. if (!$this->ensureBinExists()) {
  267. $this->catchException($e);
  268. }
  269. }
  270. }
  271. /**
  272. * {@inheritdoc}
  273. */
  274. public function deleteAll() {
  275. try {
  276. $this->connection->truncate($this->bin)->execute();
  277. }
  278. catch (\Exception $e) {
  279. // Create the cache table, which will be empty. This fixes cases during
  280. // core install where a cache table is cleared before it is set
  281. // with {cache_render} and {cache_data}.
  282. if (!$this->ensureBinExists()) {
  283. $this->catchException($e);
  284. }
  285. }
  286. }
  287. /**
  288. * {@inheritdoc}
  289. */
  290. public function invalidate($cid) {
  291. $this->invalidateMultiple([$cid]);
  292. }
  293. /**
  294. * {@inheritdoc}
  295. */
  296. public function invalidateMultiple(array $cids) {
  297. $cids = array_values(array_map([$this, 'normalizeCid'], $cids));
  298. try {
  299. // Update in chunks when a large array is passed.
  300. foreach (array_chunk($cids, 1000) as $cids_chunk) {
  301. $this->connection->update($this->bin)
  302. ->fields(['expire' => REQUEST_TIME - 1])
  303. ->condition('cid', $cids_chunk, 'IN')
  304. ->execute();
  305. }
  306. }
  307. catch (\Exception $e) {
  308. $this->catchException($e);
  309. }
  310. }
  311. /**
  312. * {@inheritdoc}
  313. */
  314. public function invalidateAll() {
  315. try {
  316. $this->connection->update($this->bin)
  317. ->fields(['expire' => REQUEST_TIME - 1])
  318. ->execute();
  319. }
  320. catch (\Exception $e) {
  321. $this->catchException($e);
  322. }
  323. }
  324. /**
  325. * {@inheritdoc}
  326. */
  327. public function garbageCollection() {
  328. try {
  329. // Bounded size cache bin, using FIFO.
  330. if ($this->maxRows !== static::MAXIMUM_NONE) {
  331. $first_invalid_create_time = $this->connection->select($this->bin)
  332. ->fields($this->bin, ['created'])
  333. ->orderBy("{$this->bin}.created", 'DESC')
  334. ->range($this->maxRows, $this->maxRows + 1)
  335. ->execute()
  336. ->fetchField();
  337. if ($first_invalid_create_time) {
  338. $this->connection->delete($this->bin)
  339. ->condition('created', $first_invalid_create_time, '<=')
  340. ->execute();
  341. }
  342. }
  343. $this->connection->delete($this->bin)
  344. ->condition('expire', Cache::PERMANENT, '<>')
  345. ->condition('expire', REQUEST_TIME, '<')
  346. ->execute();
  347. }
  348. catch (\Exception $e) {
  349. // If the table does not exist, it surely does not have garbage in it.
  350. // If the table exists, the next garbage collection will clean up.
  351. // There is nothing to do.
  352. }
  353. }
  354. /**
  355. * {@inheritdoc}
  356. */
  357. public function removeBin() {
  358. try {
  359. $this->connection->schema()->dropTable($this->bin);
  360. }
  361. catch (\Exception $e) {
  362. $this->catchException($e);
  363. }
  364. }
  365. /**
  366. * Check if the cache bin exists and create it if not.
  367. */
  368. protected function ensureBinExists() {
  369. try {
  370. $database_schema = $this->connection->schema();
  371. if (!$database_schema->tableExists($this->bin)) {
  372. $schema_definition = $this->schemaDefinition();
  373. $database_schema->createTable($this->bin, $schema_definition);
  374. return TRUE;
  375. }
  376. }
  377. // If another process has already created the cache table, attempting to
  378. // recreate it will throw an exception. In this case just catch the
  379. // exception and do nothing.
  380. catch (DatabaseException $e) {
  381. return TRUE;
  382. }
  383. return FALSE;
  384. }
  385. /**
  386. * Act on an exception when cache might be stale.
  387. *
  388. * If the table does not yet exist, that's fine, but if the table exists and
  389. * yet the query failed, then the cache is stale and the exception needs to
  390. * propagate.
  391. *
  392. * @param $e
  393. * The exception.
  394. * @param string|null $table_name
  395. * The table name. Defaults to $this->bin.
  396. *
  397. * @throws \Exception
  398. */
  399. protected function catchException(\Exception $e, $table_name = NULL) {
  400. if ($this->connection->schema()->tableExists($table_name ?: $this->bin)) {
  401. throw $e;
  402. }
  403. }
  404. /**
  405. * Normalizes a cache ID in order to comply with database limitations.
  406. *
  407. * @param string $cid
  408. * The passed in cache ID.
  409. *
  410. * @return string
  411. * An ASCII-encoded cache ID that is at most 255 characters long.
  412. */
  413. protected function normalizeCid($cid) {
  414. // Nothing to do if the ID is a US ASCII string of 255 characters or less.
  415. $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
  416. if (strlen($cid) <= 255 && $cid_is_ascii) {
  417. return $cid;
  418. }
  419. // Return a string that uses as much as possible of the original cache ID
  420. // with the hash appended.
  421. $hash = Crypt::hashBase64($cid);
  422. if (!$cid_is_ascii) {
  423. return $hash;
  424. }
  425. return substr($cid, 0, 255 - strlen($hash)) . $hash;
  426. }
  427. /**
  428. * Defines the schema for the {cache_*} bin tables.
  429. *
  430. * @internal
  431. */
  432. public function schemaDefinition() {
  433. $schema = [
  434. 'description' => 'Storage for the cache API.',
  435. 'fields' => [
  436. 'cid' => [
  437. 'description' => 'Primary Key: Unique cache ID.',
  438. 'type' => 'varchar_ascii',
  439. 'length' => 255,
  440. 'not null' => TRUE,
  441. 'default' => '',
  442. 'binary' => TRUE,
  443. ],
  444. 'data' => [
  445. 'description' => 'A collection of data to cache.',
  446. 'type' => 'blob',
  447. 'not null' => FALSE,
  448. 'size' => 'big',
  449. ],
  450. 'expire' => [
  451. 'description' => 'A Unix timestamp indicating when the cache entry should expire, or ' . Cache::PERMANENT . ' for never.',
  452. 'type' => 'int',
  453. 'not null' => TRUE,
  454. 'default' => 0,
  455. ],
  456. 'created' => [
  457. 'description' => 'A timestamp with millisecond precision indicating when the cache entry was created.',
  458. 'type' => 'numeric',
  459. 'precision' => 14,
  460. 'scale' => 3,
  461. 'not null' => TRUE,
  462. 'default' => 0,
  463. ],
  464. 'serialized' => [
  465. 'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
  466. 'type' => 'int',
  467. 'size' => 'small',
  468. 'not null' => TRUE,
  469. 'default' => 0,
  470. ],
  471. 'tags' => [
  472. 'description' => 'Space-separated list of cache tags for this entry.',
  473. 'type' => 'text',
  474. 'size' => 'big',
  475. 'not null' => FALSE,
  476. ],
  477. 'checksum' => [
  478. 'description' => 'The tag invalidation checksum when this entry was saved.',
  479. 'type' => 'varchar_ascii',
  480. 'length' => 255,
  481. 'not null' => TRUE,
  482. ],
  483. ],
  484. 'indexes' => [
  485. 'expire' => ['expire'],
  486. 'created' => ['created'],
  487. ],
  488. 'primary key' => ['cid'],
  489. ];
  490. return $schema;
  491. }
  492. /**
  493. * The maximum number of rows that this cache bin table is allowed to store.
  494. *
  495. * @return int
  496. */
  497. public function getMaxRows() {
  498. return $this->maxRows;
  499. }
  500. }