1 | <?php |
||
2 | |||
3 | namespace MatthiasMullie\Scrapbook\Scale; |
||
4 | |||
5 | use MatthiasMullie\Scrapbook\KeyValueStore; |
||
6 | use SplObjectStorage; |
||
7 | |||
8 | /** |
||
9 | * This class lets you scale your cache cluster by sharding the data across |
||
10 | * multiple cache servers. |
||
11 | * |
||
12 | * Pass the individual KeyValueStore objects that compose the cache server pool |
||
13 | * into this constructor how you want the data to be sharded. The cache data |
||
14 | * will be sharded over them according to the order they were in when they were |
||
15 | * passed into this constructor (so make sure to always keep the order the same) |
||
16 | * |
||
17 | * The sharding is spread evenly and all cache servers will roughly receive the |
||
18 | * same amount of cache keys. If some servers are bigger than others, you can |
||
19 | * offset this by adding the KeyValueStore object more than once. |
||
20 | * |
||
21 | * Data can even be sharded among different adapters: one server in the shard |
||
22 | * pool can be Redis while another can be Memcached. Not sure why you would even |
||
23 | * want that, but you could! |
||
24 | * |
||
25 | * @author Matthias Mullie <[email protected]> |
||
26 | * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved |
||
27 | * @license LICENSE MIT |
||
28 | */ |
||
29 | class Shard implements KeyValueStore |
||
30 | { |
||
31 | /** |
||
32 | * @var KeyValueStore[] |
||
33 | */ |
||
34 | protected $caches = array(); |
||
35 | |||
36 | /** |
||
37 | * Overloadable with multiple KeyValueStore objects. |
||
38 | */ |
||
39 | public function __construct(KeyValueStore $cache1, KeyValueStore $cache2 = null /* , [KeyValueStore $cache3, [...]] */) |
||
40 | { |
||
41 | $caches = func_get_args(); |
||
42 | $caches = array_filter($caches); |
||
43 | $this->caches = $caches; |
||
44 | } |
||
45 | |||
46 | public function addCache(KeyValueStore $cache) |
||
47 | { |
||
48 | $this->caches[] = $cache; |
||
49 | } |
||
50 | |||
51 | /** |
||
52 | * {@inheritdoc} |
||
53 | */ |
||
54 | public function get($key, &$token = null) |
||
55 | { |
||
56 | return $this->getShard($key)->get($key, $token); |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * {@inheritdoc} |
||
61 | */ |
||
62 | public function getMulti(array $keys, array &$tokens = null) |
||
63 | { |
||
64 | $shards = $this->getShards($keys); |
||
65 | $results = array(); |
||
66 | $tokens = array(); |
||
67 | |||
68 | /** @var KeyValueStore $shard */ |
||
69 | foreach ($shards as $shard) { |
||
70 | $keysOnShard = $shards[$shard]; |
||
71 | $results += $shard->getMulti($keysOnShard, $shardTokens); |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
72 | $tokens += $shardTokens ?: array(); |
||
73 | } |
||
74 | |||
75 | return $results; |
||
76 | } |
||
77 | |||
78 | /** |
||
79 | * {@inheritdoc} |
||
80 | */ |
||
81 | public function set($key, $value, $expire = 0) |
||
82 | { |
||
83 | return $this->getShard($key)->set($key, $value, $expire); |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * {@inheritdoc} |
||
88 | */ |
||
89 | public function setMulti(array $items, $expire = 0) |
||
90 | { |
||
91 | $shards = $this->getShards(array_keys($items)); |
||
92 | $results = array(); |
||
93 | |||
94 | /** @var KeyValueStore $shard */ |
||
95 | foreach ($shards as $shard) { |
||
96 | $keysOnShard = $shards[$shard]; |
||
97 | $itemsOnShard = array_intersect_key($items, array_flip($keysOnShard)); |
||
98 | $results += $shard->setMulti($itemsOnShard, $expire); |
||
99 | } |
||
100 | |||
101 | return $results; |
||
102 | } |
||
103 | |||
104 | /** |
||
105 | * {@inheritdoc} |
||
106 | */ |
||
107 | public function delete($key) |
||
108 | { |
||
109 | return $this->getShard($key)->delete($key); |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * {@inheritdoc} |
||
114 | */ |
||
115 | public function deleteMulti(array $keys) |
||
116 | { |
||
117 | $shards = $this->getShards($keys); |
||
118 | $results = array(); |
||
119 | |||
120 | /** @var KeyValueStore $shard */ |
||
121 | foreach ($shards as $shard) { |
||
122 | $keysOnShard = $shards[$shard]; |
||
123 | $results += $shard->deleteMulti($keysOnShard); |
||
124 | } |
||
125 | |||
126 | return $results; |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * {@inheritdoc} |
||
131 | */ |
||
132 | public function add($key, $value, $expire = 0) |
||
133 | { |
||
134 | return $this->getShard($key)->add($key, $value, $expire); |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * {@inheritdoc} |
||
139 | */ |
||
140 | public function replace($key, $value, $expire = 0) |
||
141 | { |
||
142 | return $this->getShard($key)->replace($key, $value, $expire); |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * {@inheritdoc} |
||
147 | */ |
||
148 | public function cas($token, $key, $value, $expire = 0) |
||
149 | { |
||
150 | return $this->getShard($key)->cas($token, $key, $value, $expire); |
||
151 | } |
||
152 | |||
153 | /** |
||
154 | * {@inheritdoc} |
||
155 | */ |
||
156 | public function increment($key, $offset = 1, $initial = 0, $expire = 0) |
||
157 | { |
||
158 | return $this->getShard($key)->increment($key, $offset, $initial, $expire); |
||
159 | } |
||
160 | |||
161 | /** |
||
162 | * {@inheritdoc} |
||
163 | */ |
||
164 | public function decrement($key, $offset = 1, $initial = 0, $expire = 0) |
||
165 | { |
||
166 | return $this->getShard($key)->decrement($key, $offset, $initial, $expire); |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * {@inheritdoc} |
||
171 | */ |
||
172 | public function touch($key, $expire) |
||
173 | { |
||
174 | return $this->getShard($key)->touch($key, $expire); |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * {@inheritdoc} |
||
179 | */ |
||
180 | public function flush() |
||
181 | { |
||
182 | $result = true; |
||
183 | |||
184 | foreach ($this->caches as $cache) { |
||
185 | $result &= $cache->flush(); |
||
186 | } |
||
187 | |||
188 | return (bool) $result; |
||
189 | } |
||
190 | |||
191 | /** |
||
192 | * {@inheritdoc} |
||
193 | */ |
||
194 | public function getCollection($name) |
||
195 | { |
||
196 | $shard = new static($this->caches[0]->getCollection($name)); |
||
197 | |||
198 | $count = count($this->caches); |
||
199 | for ($i = 1; $i < $count; ++$i) { |
||
200 | $shard->addCache($this->caches[$i]->getCollection($name)); |
||
201 | } |
||
202 | |||
203 | return $shard; |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * Get the shard (KeyValueStore object) that corresponds to a particular |
||
208 | * cache key. |
||
209 | * |
||
210 | * @param string $key |
||
211 | * |
||
212 | * @return KeyValueStore |
||
213 | */ |
||
214 | protected function getShard($key) |
||
215 | { |
||
216 | /* |
||
217 | * The hash is so we can deterministically randomize the spread of keys |
||
218 | * over servers: if we were to just spread them based on key name, we |
||
219 | * may end up with a large chunk of similarly prefixed keys on the same |
||
220 | * server. Hashing the key will ensure similar looking keys can still |
||
221 | * result in very different values, yet they result will be the same |
||
222 | * every time it's repeated for the same key. |
||
223 | * Since we don't use the hash for encryption, the fastest algorithm |
||
224 | * will do just fine here. |
||
225 | */ |
||
226 | $hash = crc32($key); |
||
227 | |||
228 | // crc32 on 32-bit machines can produce a negative int |
||
229 | $hash = abs($hash); |
||
230 | |||
231 | $index = $hash % count($this->caches); |
||
232 | |||
233 | return $this->caches[$index]; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Get a [KeyValueStore => array of cache keys] map (SplObjectStorage) for |
||
238 | * multiple cache keys. |
||
239 | * |
||
240 | * @return \SplObjectStorage |
||
241 | */ |
||
242 | protected function getShards(array $keys) |
||
243 | { |
||
244 | $shards = new \SplObjectStorage(); |
||
245 | |||
246 | foreach ($keys as $key) { |
||
247 | $shard = $this->getShard($key); |
||
248 | if (!isset($shards[$shard])) { |
||
249 | $shards[$shard] = array(); |
||
250 | } |
||
251 | |||
252 | $shards[$shard] = array_merge($shards[$shard], array($key)); |
||
253 | } |
||
254 | |||
255 | return $shards; |
||
256 | } |
||
257 | } |
||
258 |