Skip to content

Commit 229e33e

Browse files
[Cache] Add tags based invalidation + RedisTaggedAdapter
1 parent 22f7ed7 commit 229e33e

12 files changed

+752
-103
lines changed
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

+3-99
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)