Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 40f4c57

Browse filesBrowse files
committed
feature #19047 [Cache] Add tags based invalidation + TagAwareRedisAdapter (nicolas-grekas)
This PR was merged into the 3.2-dev branch. Discussion ---------- [Cache] Add tags based invalidation + TagAwareRedisAdapter | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #18646 | License | MIT | Doc PR | - Commits ------- 19764af [Cache] Add tags based invalidation + TagAwareRedisAdapter
2 parents 7ccfdb4 + 19764af commit 40f4c57
Copy full SHA for 40f4c57
Expand file treeCollapse file tree

12 files changed

+752
-103
lines changed
+217Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter;
13+
14+
use Psr\Cache\CacheItemInterface;
15+
use Symfony\Component\Cache\CacheItem;
16+
17+
/**
18+
* @author Nicolas Grekas <p@tchwork.com>
19+
*/
20+
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface
21+
{
22+
private $adapter;
23+
private $deferred = array();
24+
private $createCacheItem;
25+
private $getTagsByKey;
26+
27+
/**
28+
* Removes tag-invalidated keys and returns the removed ones.
29+
*
30+
* @param array &$keys The keys to filter
31+
*
32+
* @return array The keys removed from $keys
33+
*/
34+
abstract protected function filterInvalidatedKeys(array &$keys);
35+
36+
/**
37+
* Persists tags for cache keys.
38+
*
39+
* @param array $tags The tags for each cache keys as index
40+
*
41+
* @return bool True on success
42+
*/
43+
abstract protected function doSaveTags(array $tags);
44+
45+
public function __construct(AdapterInterface $adapter, $defaultLifetime)
46+
{
47+
$this->adapter = $adapter;
48+
$this->createCacheItem = \Closure::bind(
49+
function ($key) use ($defaultLifetime) {
50+
$item = new CacheItem();
51+
$item->key = $key;
52+
$item->isHit = false;
53+
$item->defaultLifetime = $defaultLifetime;
54+
55+
return $item;
56+
},
57+
null,
58+
CacheItem::class
59+
);
60+
$this->getTagsByKey = \Closure::bind(
61+
function ($deferred) {
62+
$tagsByKey = array();
63+
foreach ($deferred as $key => $item) {
64+
$tagsByKey[$key] = $item->tags;
65+
}
66+
67+
return $tagsByKey;
68+
},
69+
null,
70+
CacheItem::class
71+
);
72+
}
73+
74+
/**
75+
* {@inheritdoc}
76+
*/
77+
public function hasItem($key)
78+
{
79+
if ($this->deferred) {
80+
$this->commit();
81+
}
82+
if (!$this->adapter->hasItem($key)) {
83+
return false;
84+
}
85+
$keys = array($key);
86+
87+
return !$this->filterInvalidatedKeys($keys);
88+
}
89+
90+
/**
91+
* {@inheritdoc}
92+
*/
93+
public function getItem($key)
94+
{
95+
if ($this->deferred) {
96+
$this->commit();
97+
}
98+
$keys = array($key);
99+
100+
if ($keys = $this->filterInvalidatedKeys($keys)) {
101+
foreach ($this->generateItems(array(), $keys) as $item) {
102+
return $item;
103+
}
104+
}
105+
106+
return $this->adapter->getItem($key);
107+
}
108+
109+
/**
110+
* {@inheritdoc}
111+
*/
112+
public function getItems(array $keys = array())
113+
{
114+
if ($this->deferred) {
115+
$this->commit();
116+
}
117+
$invalids = $this->filterInvalidatedKeys($keys);
118+
$items = $this->adapter->getItems($keys);
119+
120+
return $this->generateItems($items, $invalids);
121+
}
122+
123+
/**
124+
* {@inheritdoc}
125+
*/
126+
public function clear()
127+
{
128+
$this->deferred = array();
129+
130+
return $this->adapter->clear();
131+
}
132+
133+
/**
134+
* {@inheritdoc}
135+
*/
136+
public function deleteItem($key)
137+
{
138+
return $this->adapter->deleteItem($key);
139+
}
140+
141+
/**
142+
* {@inheritdoc}
143+
*/
144+
public function deleteItems(array $keys)
145+
{
146+
return $this->adapter->deleteItems($keys);
147+
}
148+
149+
/**
150+
* {@inheritdoc}
151+
*/
152+
public function save(CacheItemInterface $item)
153+
{
154+
if (!$item instanceof CacheItem) {
155+
return false;
156+
}
157+
if ($this->deferred) {
158+
$this->commit();
159+
}
160+
$this->deferred[$item->getKey()] = $item;
161+
162+
return $this->commit();
163+
}
164+
165+
/**
166+
* {@inheritdoc}
167+
*/
168+
public function saveDeferred(CacheItemInterface $item)
169+
{
170+
if (!$item instanceof CacheItem) {
171+
return false;
172+
}
173+
$this->deferred[$item->getKey()] = $item;
174+
175+
return true;
176+
}
177+
178+
/**
179+
* {@inheritdoc}
180+
*/
181+
public function commit()
182+
{
183+
$ok = true;
184+
185+
if ($this->deferred) {
186+
foreach ($this->deferred as $key => $item) {
187+
if (!$this->adapter->saveDeferred($item)) {
188+
unset($this->deferred[$key]);
189+
$ok = false;
190+
}
191+
}
192+
$f = $this->getTagsByKey;
193+
$ok = $this->doSaveTags($f($this->deferred)) && $ok;
194+
$this->deferred = array();
195+
}
196+
197+
return $this->adapter->commit() && $ok;
198+
}
199+
200+
public function __destruct()
201+
{
202+
$this->commit();
203+
}
204+
205+
private function generateItems($items, $invalids)
206+
{
207+
foreach ($items as $key => $item) {
208+
yield $key => $item;
209+
}
210+
211+
$f = $this->createCacheItem;
212+
213+
foreach ($invalids as $key) {
214+
yield $key => $f($key);
215+
}
216+
}
217+
}

‎src/Symfony/Component/Cache/Adapter/RedisAdapter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/RedisAdapter.php
+3-99Lines changed: 3 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
namespace Symfony\Component\Cache\Adapter;
1313

1414
use Predis\Connection\Factory;
15-
use Predis\Connection\Aggregate\PredisCluster;
16-
use Predis\Connection\Aggregate\RedisCluster;
1715
use Symfony\Component\Cache\Exception\InvalidArgumentException;
1816

1917
/**
@@ -22,29 +20,23 @@
2220
*/
2321
class RedisAdapter extends AbstractAdapter
2422
{
23+
use RedisAdapterTrait;
24+
2525
private static $defaultConnectionOptions = array(
2626
'class' => null,
2727
'persistent' => 0,
2828
'timeout' => 0,
2929
'read_timeout' => 0,
3030
'retry_interval' => 0,
3131
);
32-
private $redis;
3332

3433
/**
3534
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
3635
*/
3736
public function __construct($redisClient, $namespace = '', $defaultLifetime = 0)
3837
{
3938
parent::__construct($namespace, $defaultLifetime);
40-
41-
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
42-
throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
43-
}
44-
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
45-
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
46-
}
47-
$this->redis = $redisClient;
39+
$this->setRedis($redisClient, $namespace);
4840
}
4941

5042
/**
@@ -157,51 +149,6 @@ protected function doHave($id)
157149
return (bool) $this->redis->exists($id);
158150
}
159151

160-
/**
161-
* {@inheritdoc}
162-
*/
163-
protected function doClear($namespace)
164-
{
165-
// When using a native Redis cluster, clearing the cache cannot work and always returns false.
166-
// Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
167-
168-
$hosts = array($this->redis);
169-
$evalArgs = array(array($namespace), 0);
170-
171-
if ($this->redis instanceof \Predis\Client) {
172-
$evalArgs = array(0, $namespace);
173-
174-
$connection = $this->redis->getConnection();
175-
if ($connection instanceof PredisCluster) {
176-
$hosts = array();
177-
foreach ($connection as $c) {
178-
$hosts[] = new \Predis\Client($c);
179-
}
180-
} elseif ($connection instanceof RedisCluster) {
181-
return false;
182-
}
183-
} elseif ($this->redis instanceof \RedisArray) {
184-
foreach ($this->redis->_hosts() as $host) {
185-
$hosts[] = $this->redis->_instance($host);
186-
}
187-
} elseif ($this->redis instanceof \RedisCluster) {
188-
return false;
189-
}
190-
foreach ($hosts as $host) {
191-
if (!isset($namespace[0])) {
192-
$host->flushDb();
193-
} else {
194-
// As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
195-
// can hang your server when it is executed against large databases (millions of items).
196-
// Whenever you hit this scale, it is advised to deploy one Redis database per cache pool
197-
// instead of using namespaces, so that FLUSHDB is used instead.
198-
$host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end", $evalArgs[0], $evalArgs[1]);
199-
}
200-
}
201-
202-
return true;
203-
}
204-
205152
/**
206153
* {@inheritdoc}
207154
*/
@@ -248,47 +195,4 @@ protected function doSave(array $values, $lifetime)
248195

249196
return $failed;
250197
}
251-
252-
private function execute($command, $id, array $args, $redis = null)
253-
{
254-
array_unshift($args, $id);
255-
call_user_func_array(array($redis ?: $this->redis, $command), $args);
256-
}
257-
258-
private function pipeline(\Closure $callback)
259-
{
260-
$redis = $this->redis;
261-
262-
try {
263-
if ($redis instanceof \Predis\Client) {
264-
$redis->pipeline(function ($pipe) use ($callback) {
265-
$this->redis = $pipe;
266-
$callback(array($this, 'execute'));
267-
});
268-
} elseif ($redis instanceof \RedisArray) {
269-
$connections = array();
270-
$callback(function ($command, $id, $args) use (&$connections) {
271-
if (!isset($connections[$h = $this->redis->_target($id)])) {
272-
$connections[$h] = $this->redis->_instance($h);
273-
$connections[$h]->multi(\Redis::PIPELINE);
274-
}
275-
$this->execute($command, $id, $args, $connections[$h]);
276-
});
277-
foreach ($connections as $c) {
278-
$c->exec();
279-
}
280-
} else {
281-
$pipe = $redis->multi(\Redis::PIPELINE);
282-
try {
283-
$callback(array($this, 'execute'));
284-
} finally {
285-
if ($pipe) {
286-
$redis->exec();
287-
}
288-
}
289-
}
290-
} finally {
291-
$this->redis = $redis;
292-
}
293-
}
294198
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.