Added MemoryDriver for hyperf/cache (#6542)

Co-authored-by: 张城铭 <z@hyperf.io>
This commit is contained in:
Deeka Wong 2024-02-20 11:22:01 +08:00 committed by GitHub
parent b4679a442e
commit 2e709cda58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 419 additions and 0 deletions

View File

@ -1,5 +1,9 @@
# v3.1.10 - TBD # v3.1.10 - TBD
## Added
- [#6542](https://github.com/hyperf/hyperf/pull/6542) Added `MemoryDriver` for `hyperf/cache`.
## Optimized ## Optimized
- [#6539](https://github.com/hyperf/hyperf/pull/6539) Optimized `retry` helper function can accept array as first parameter. - [#6539](https://github.com/hyperf/hyperf/pull/6539) Optimized `retry` helper function can accept array as first parameter.

View File

@ -298,6 +298,20 @@ class UserBookService
`Hyperf\Cache\Driver\RedisDriver` will store cache data in `Redis`, and users need to configure the corresponding `Redis configuration`. This mode is the default mode. `Hyperf\Cache\Driver\RedisDriver` will store cache data in `Redis`, and users need to configure the corresponding `Redis configuration`. This mode is the default mode.
### Process memory driver
If you need to cache data into memory, you can try this driver. The configuration is as follows:
```php
<?php
return [
'memory' => [
'driver' => Hyperf\Cache\Driver\MemoryDriver::class,
],
];
```
### Coroutine memory driver ### Coroutine memory driver
If you need to cache data into `Context`, you can try this driver. For example, in the following application scenario, `Demo::get` will be called multiple times in multiple places, but you do not want to query `Redis` every time. If you need to cache data into `Context`, you can try this driver. For example, in the following application scenario, `Demo::get` will be called multiple times in multiple places, but you do not want to query `Redis` every time.

View File

@ -299,6 +299,22 @@ class UserBookService
`Hyperf\Cache\Driver\RedisDriver` 会把缓存数据存放到 `Redis` 中,需要用户配置相应的 `Redis 配置`。此方式为默认方式。 `Hyperf\Cache\Driver\RedisDriver` 会把缓存数据存放到 `Redis` 中,需要用户配置相应的 `Redis 配置`。此方式为默认方式。
### 进程内存驱动
如果您需要将数据缓存到内存中,可以尝试此驱动。
配置如下:
```php
<?php
return [
'memory' => [
'driver' => Hyperf\Cache\Driver\MemoryDriver::class,
],
];
```
### 协程内存驱动 ### 协程内存驱动
如果您需要将数据缓存到 `Context` 中,可以尝试此驱动。例如以下应用场景 `Demo::get` 会在多个地方调用多次,但是又不想每次都到 `Redis` 中进行查询。 如果您需要将数据缓存到 `Context` 中,可以尝试此驱动。例如以下应用场景 `Demo::get` 会在多个地方调用多次,但是又不想每次都到 `Redis` 中进行查询。

View File

@ -299,6 +299,22 @@ class UserBookService
`Hyperf\Cache\Driver\RedisDriver` 會把緩存數據存放到 `Redis` 中,需要用户配置相應的 `Redis 配置`。此方式為默認方式。 `Hyperf\Cache\Driver\RedisDriver` 會把緩存數據存放到 `Redis` 中,需要用户配置相應的 `Redis 配置`。此方式為默認方式。
### 進程內存驅動
如果您需要將數據緩存到內存中,可以嘗試此驅動。
配置如下:
```php
<?php
return [
'memory' => [
'driver' => Hyperf\Cache\Driver\MemoryDriver::class,
],
];
```
### 協程內存驅動 ### 協程內存驅動
如果您需要將數據緩存到 `Context` 中,可以嘗試此驅動。例如以下應用場景 `Demo::get` 會在多個地方調用多次,但是又不想每次都到 `Redis` 中進行查詢。 如果您需要將數據緩存到 `Context` 中,可以嘗試此驅動。例如以下應用場景 `Demo::get` 會在多個地方調用多次,但是又不想每次都到 `Redis` 中進行查詢。

View File

@ -299,6 +299,22 @@ class UserBookService
`Hyperf\Cache\Driver\RedisDriver` 會把快取資料存放到 `Redis` 中,需要使用者配置相應的 `Redis 配置`。此方式為預設方式。 `Hyperf\Cache\Driver\RedisDriver` 會把快取資料存放到 `Redis` 中,需要使用者配置相應的 `Redis 配置`。此方式為預設方式。
### 程序記憶體驅動
如果您需要將資料快取到記憶體中,可以嘗試此驅動。
配置如下:
```php
<?php
return [
'memory' => [
'driver' => Hyperf\Cache\Driver\MemoryDriver::class,
],
];
```
### 協程記憶體驅動 ### 協程記憶體驅動
如果您需要將資料快取到 `Context` 中,可以嘗試此驅動。例如以下應用場景 `Demo::get` 會在多個地方呼叫多次,但是又不想每次都到 `Redis` 中進行查詢。 如果您需要將資料快取到 `Context` 中,可以嘗試此驅動。例如以下應用場景 `Demo::get` 會在多個地方呼叫多次,但是又不想每次都到 `Redis` 中進行查詢。

120
src/cache/src/Collector/Memory.php vendored Normal file
View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Cache\Collector;
use Carbon\Carbon;
use Hyperf\Coordinator\Constants;
use Hyperf\Coordinator\CoordinatorManager;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Stringable\Str;
final class Memory
{
/**
* @var array<string, null|Carbon>
*/
private array $keys = [];
/**
* @var array<string, mixed>
*/
private array $values = [];
private ?int $loopCid = null;
private ?int $waitCloseCid = null;
private bool $stopped = false;
public static function instance(): static
{
static $instance;
return $instance ??= new self();
}
public function size(): int
{
return count($this->keys);
}
public function has(string $key): bool
{
return isset($this->keys[$key]);
}
public function get(string $key, mixed $default = null): mixed
{
return $this->values[$key] ?? $default;
}
public function set(string $key, mixed $value, ?Carbon $ttl = null): bool
{
$this->loop();
$this->keys[$key] = $ttl;
$this->values[$key] = $value;
return true;
}
public function delete(string $key): bool
{
unset($this->keys[$key], $this->values[$key]);
return true;
}
public function clear(): bool
{
$this->keys = [];
$this->values = [];
return true;
}
public function clearPrefix(string $prefix): bool
{
foreach ($this->keys as $key => $ttl) {
if (Str::startsWith($key, $prefix)) {
$this->delete($key);
}
}
return true;
}
private function loop(): void
{
$this->loopCid ??= Coroutine::create(function () {
while (true) {
foreach ($this->keys as $key => $ttl) {
if ($ttl instanceof Carbon && Carbon::now()->gt($ttl)) {
$this->delete($key);
}
}
sleep(1);
if ($this->stopped || $this->size() === 0) {
break;
}
}
$this->loopCid = null;
$this->stopped = false;
});
$this->waitCloseCid ??= Coroutine::create(function () {
CoordinatorManager::until(Constants::WORKER_EXIT)->yield();
$this->stopped = true;
$this->clear();
$this->waitCloseCid = null;
});
}
}

136
src/cache/src/Driver/MemoryDriver.php vendored Normal file
View File

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Cache\Driver;
use Carbon\Carbon;
use Hyperf\Cache\Collector\Memory;
use Hyperf\Cache\Exception\InvalidArgumentException;
use Hyperf\Cache\Exception\OverflowException;
use Psr\Container\ContainerInterface;
class MemoryDriver extends Driver implements DriverInterface
{
protected ?int $size = null;
protected bool $throwWhenSizeExceeded = false;
public function __construct(ContainerInterface $container, array $config)
{
parent::__construct($container, $config);
if (isset($config['size'])) {
$this->size = (int) $config['size'];
}
if (isset($config['throw_when_size_exceeded'])) {
$this->throwWhenSizeExceeded = (bool) $config['throw_when_size_exceeded'];
}
}
public function fetch($key, $default = null): array
{
return [
$this->has($key),
$this->get($key, $default),
];
}
public function has($key): bool
{
return $this->getCollector()->has($this->getCacheKey($key));
}
public function get($key, $default = null): mixed
{
return $this->getCollector()->get(
$this->getCacheKey($key),
$default
);
}
public function set($key, $value, $ttl = null): bool
{
if (
$this->size > 0
&& $this->getCollector()->size() >= $this->size
) {
if ($this->throwWhenSizeExceeded) {
throw new OverflowException('The memory cache is full!');
}
return false;
}
$seconds = $this->secondsUntil($ttl);
return $this->getCollector()->set(
$this->getCacheKey($key),
$value,
$seconds <= 0 ? null : Carbon::now()->addSeconds($seconds)
);
}
public function delete($key): bool
{
return $this->getCollector()->delete($this->getCacheKey($key));
}
public function clear(): bool
{
return $this->getCollector()->clear();
}
public function getMultiple($keys, $default = null): iterable
{
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->get($key, $default);
}
return $result;
}
public function setMultiple($values, $ttl = null): bool
{
if (! is_array($values)) {
throw new InvalidArgumentException('The values is invalid!');
}
foreach ($values as $key => $value) {
$this->set($key, $value, $ttl);
}
return true;
}
public function deleteMultiple($keys): bool
{
foreach ($keys as $key) {
$this->delete($key);
}
return true;
}
public function clearPrefix(string $prefix): bool
{
return $this->getCollector()->clearPrefix($this->getCacheKey($prefix));
}
public function getConnection(): mixed
{
return $this;
}
protected function getCollector(): Memory
{
return Memory::instance();
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Cache\Exception;
class OverflowException extends CacheException
{
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Cache\Cases;
use Hyperf\Cache\Collector\Memory;
use Hyperf\Cache\Driver\MemoryDriver;
use Hyperf\Cache\Exception\OverflowException;
use Hyperf\Codec\Packer\PhpSerializerPacker;
use Mockery;
use Psr\Container\ContainerInterface;
/**
* @internal
* @coversNothing
*/
class MemoryDriverTest extends \PHPUnit\Framework\TestCase
{
protected function tearDown(): void
{
Mockery::close();
Memory::instance()->clear();
}
public function testSetAndGet()
{
$driver = new MemoryDriver($this->getContainer(), []);
$driver->set('test', 'xxx');
$this->assertSame('xxx', $driver->get('test'));
}
public function testSetWithTtl()
{
$driver = new MemoryDriver($this->getContainer(), []);
$driver->set('test', 'xxx', 1);
$this->assertSame('xxx', $driver->get('test'));
sleep(3);
$this->assertNull($driver->get('test'));
}
public function testSetWithSize()
{
$driver = new MemoryDriver($this->getContainer(), ['size' => 1, 'throw_when_size_exceeded' => true]);
$driver->set('test1', 'xxx');
$this->assertSame('xxx', $driver->get('test1'));
$this->expectException(OverflowException::class);
$this->expectExceptionMessage('The memory cache is full!');
$driver->set('test2', 'xxx');
}
public function testSetWithSizeAndThrowWhenSizeExceededIsFalse()
{
$driver = new MemoryDriver($this->getContainer(), ['size' => 1, 'throw_when_size_exceeded' => false]);
$driver->set('test1', 'xxx');
$this->assertSame('xxx', $driver->get('test1'));
$this->assertFalse($driver->set('test2', 'xxx'));
}
private function getContainer(): ContainerInterface
{
$container = Mockery::mock(ContainerInterface::class);
$container->shouldReceive('get')->with(PhpSerializerPacker::class)->andReturn(new PhpSerializerPacker());
return $container;
}
}