DatabaseBackend.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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\SchemaObjectExistsException;
  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 cache_get() or cache_get_multiple().
  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. if (!is_string($item['data'])) {
  212. $fields['data'] = serialize($item['data']);
  213. $fields['serialized'] = 1;
  214. }
  215. else {
  216. $fields['data'] = $item['data'];
  217. $fields['serialized'] = 0;
  218. }
  219. $values[] = $fields;
  220. }
  221. // Use an upsert query which is atomic and optimized for multiple-row
  222. // merges.
  223. $query = $this->connection
  224. ->upsert($this->bin)
  225. ->key('cid')
  226. ->fields(['cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized']);
  227. foreach ($values as $fields) {
  228. // Only pass the values since the order of $fields matches the order of
  229. // the insert fields. This is a performance optimization to avoid
  230. // unnecessary loops within the method.
  231. $query->values(array_values($fields));
  232. }
  233. $query->execute();
  234. }
  235. /**
  236. * {@inheritdoc}
  237. */
  238. public function delete($cid) {
  239. $this->deleteMultiple([$cid]);
  240. }
  241. /**
  242. * {@inheritdoc}
  243. */
  244. public function deleteMultiple(array $cids) {
  245. $cids = array_values(array_map([$this, 'normalizeCid'], $cids));
  246. try {
  247. // Delete in chunks when a large array is passed.
  248. foreach (array_chunk($cids, 1000) as $cids_chunk) {
  249. $this->connection->delete($this->bin)
  250. ->condition('cid', $cids_chunk, 'IN')
  251. ->execute();
  252. }
  253. }
  254. catch (\Exception $e) {
  255. // Create the cache table, which will be empty. This fixes cases during
  256. // core install where a cache table is cleared before it is set
  257. // with {cache_render} and {cache_data}.
  258. if (!$this->ensureBinExists()) {
  259. $this->catchException($e);
  260. }
  261. }
  262. }
  263. /**
  264. * {@inheritdoc}
  265. */
  266. public function deleteAll() {
  267. try {
  268. $this->connection->truncate($this->bin)->execute();
  269. }
  270. catch (\Exception $e) {
  271. // Create the cache table, which will be empty. This fixes cases during
  272. // core install where a cache table is cleared before it is set
  273. // with {cache_render} and {cache_data}.
  274. if (!$this->ensureBinExists()) {
  275. $this->catchException($e);
  276. }
  277. }
  278. }
  279. /**
  280. * {@inheritdoc}
  281. */
  282. public function invalidate($cid) {
  283. $this->invalidateMultiple([$cid]);
  284. }
  285. /**
  286. * {@inheritdoc}
  287. */
  288. public function invalidateMultiple(array $cids) {
  289. $cids = array_values(array_map([$this, 'normalizeCid'], $cids));
  290. try {
  291. // Update in chunks when a large array is passed.
  292. foreach (array_chunk($cids, 1000) as $cids_chunk) {
  293. $this->connection->update($this->bin)
  294. ->fields(['expire' => REQUEST_TIME - 1])
  295. ->condition('cid', $cids_chunk, 'IN')
  296. ->execute();
  297. }
  298. }
  299. catch (\Exception $e) {
  300. $this->catchException($e);
  301. }
  302. }
  303. /**
  304. * {@inheritdoc}
  305. */
  306. public function invalidateAll() {
  307. try {
  308. $this->connection->update($this->bin)
  309. ->fields(['expire' => REQUEST_TIME - 1])
  310. ->execute();
  311. }
  312. catch (\Exception $e) {
  313. $this->catchException($e);
  314. }
  315. }
  316. /**
  317. * {@inheritdoc}
  318. */
  319. public function garbageCollection() {
  320. try {
  321. // Bounded size cache bin, using FIFO.
  322. if ($this->maxRows !== static::MAXIMUM_NONE) {
  323. $first_invalid_create_time = $this->connection->select($this->bin)
  324. ->fields($this->bin, ['created'])
  325. ->orderBy("{$this->bin}.created", 'DESC')
  326. ->range($this->maxRows, $this->maxRows + 1)
  327. ->execute()
  328. ->fetchField();
  329. if ($first_invalid_create_time) {
  330. $this->connection->delete($this->bin)
  331. ->condition('created', $first_invalid_create_time, '<=')
  332. ->execute();
  333. }
  334. }
  335. $this->connection->delete($this->bin)
  336. ->condition('expire', Cache::PERMANENT, '<>')
  337. ->condition('expire', REQUEST_TIME, '<')
  338. ->execute();
  339. }
  340. catch (\Exception $e) {
  341. // If the table does not exist, it surely does not have garbage in it.
  342. // If the table exists, the next garbage collection will clean up.
  343. // There is nothing to do.
  344. }
  345. }
  346. /**
  347. * {@inheritdoc}
  348. */
  349. public function removeBin() {
  350. try {
  351. $this->connection->schema()->dropTable($this->bin);
  352. }
  353. catch (\Exception $e) {
  354. $this->catchException($e);
  355. }
  356. }
  357. /**
  358. * Check if the cache bin exists and create it if not.
  359. */
  360. protected function ensureBinExists() {
  361. try {
  362. $database_schema = $this->connection->schema();
  363. if (!$database_schema->tableExists($this->bin)) {
  364. $schema_definition = $this->schemaDefinition();
  365. $database_schema->createTable($this->bin, $schema_definition);
  366. return TRUE;
  367. }
  368. }
  369. // If another process has already created the cache table, attempting to
  370. // recreate it will throw an exception. In this case just catch the
  371. // exception and do nothing.
  372. catch (SchemaObjectExistsException $e) {
  373. return TRUE;
  374. }
  375. return FALSE;
  376. }
  377. /**
  378. * Act on an exception when cache might be stale.
  379. *
  380. * If the table does not yet exist, that's fine, but if the table exists and
  381. * yet the query failed, then the cache is stale and the exception needs to
  382. * propagate.
  383. *
  384. * @param $e
  385. * The exception.
  386. * @param string|null $table_name
  387. * The table name. Defaults to $this->bin.
  388. *
  389. * @throws \Exception
  390. */
  391. protected function catchException(\Exception $e, $table_name = NULL) {
  392. if ($this->connection->schema()->tableExists($table_name ?: $this->bin)) {
  393. throw $e;
  394. }
  395. }
  396. /**
  397. * Normalizes a cache ID in order to comply with database limitations.
  398. *
  399. * @param string $cid
  400. * The passed in cache ID.
  401. *
  402. * @return string
  403. * An ASCII-encoded cache ID that is at most 255 characters long.
  404. */
  405. protected function normalizeCid($cid) {
  406. // Nothing to do if the ID is a US ASCII string of 255 characters or less.
  407. $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
  408. if (strlen($cid) <= 255 && $cid_is_ascii) {
  409. return $cid;
  410. }
  411. // Return a string that uses as much as possible of the original cache ID
  412. // with the hash appended.
  413. $hash = Crypt::hashBase64($cid);
  414. if (!$cid_is_ascii) {
  415. return $hash;
  416. }
  417. return substr($cid, 0, 255 - strlen($hash)) . $hash;
  418. }
  419. /**
  420. * Defines the schema for the {cache_*} bin tables.
  421. *
  422. * @internal
  423. */
  424. public function schemaDefinition() {
  425. $schema = [
  426. 'description' => 'Storage for the cache API.',
  427. 'fields' => [
  428. 'cid' => [
  429. 'description' => 'Primary Key: Unique cache ID.',
  430. 'type' => 'varchar_ascii',
  431. 'length' => 255,
  432. 'not null' => TRUE,
  433. 'default' => '',
  434. 'binary' => TRUE,
  435. ],
  436. 'data' => [
  437. 'description' => 'A collection of data to cache.',
  438. 'type' => 'blob',
  439. 'not null' => FALSE,
  440. 'size' => 'big',
  441. ],
  442. 'expire' => [
  443. 'description' => 'A Unix timestamp indicating when the cache entry should expire, or ' . Cache::PERMANENT . ' for never.',
  444. 'type' => 'int',
  445. 'not null' => TRUE,
  446. 'default' => 0,
  447. ],
  448. 'created' => [
  449. 'description' => 'A timestamp with millisecond precision indicating when the cache entry was created.',
  450. 'type' => 'numeric',
  451. 'precision' => 14,
  452. 'scale' => 3,
  453. 'not null' => TRUE,
  454. 'default' => 0,
  455. ],
  456. 'serialized' => [
  457. 'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
  458. 'type' => 'int',
  459. 'size' => 'small',
  460. 'not null' => TRUE,
  461. 'default' => 0,
  462. ],
  463. 'tags' => [
  464. 'description' => 'Space-separated list of cache tags for this entry.',
  465. 'type' => 'text',
  466. 'size' => 'big',
  467. 'not null' => FALSE,
  468. ],
  469. 'checksum' => [
  470. 'description' => 'The tag invalidation checksum when this entry was saved.',
  471. 'type' => 'varchar_ascii',
  472. 'length' => 255,
  473. 'not null' => TRUE,
  474. ],
  475. ],
  476. 'indexes' => [
  477. 'expire' => ['expire'],
  478. 'created' => ['created'],
  479. ],
  480. 'primary key' => ['cid'],
  481. ];
  482. return $schema;
  483. }
  484. /**
  485. * The maximum number of rows that this cache bin table is allowed to store.
  486. *
  487. * @return int
  488. */
  489. public function getMaxRows() {
  490. return $this->maxRows;
  491. }
  492. }