Merge pull request #230 from zhamao-robot/redis

新增 Redis 支持
This commit is contained in:
Jerry 2023-01-03 01:04:34 +08:00 committed by GitHub
commit cd52bfbfa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 325 additions and 10 deletions

View File

@ -89,7 +89,7 @@ $config['database'] = [
'dbname' => 'a.db',
'pool_size' => 10,
],
'zm' => [
'default' => [
'enable' => false,
'type' => 'mysql',
'host' => '127.0.0.1', // 填写数据库服务器地址后才会创建数据库连接
@ -102,11 +102,24 @@ $config['database'] = [
],
];
/* Redis 连接配置,框架将自动生成连接池,支持多个连接池 */
$config['redis'] = [
'default' => [
'enable' => false,
'host' => '127.0.0.1',
'port' => 6379,
'index' => 0,
'auth' => '',
'pool_size' => 10,
],
];
/* KV 数据库的配置 */
$config['kv'] = [
'use' => \LightCache::class, // 默认在单进程模式下使用 LightCache多进程需要使用 ZMRedis
'light_cache_dir' => $config['data_dir'] . '/lc', // 默认的 LightCache 保存持久化数据的位置
'light_cache_autosave_time' => 600, // LightCache 自动保存时间(秒)
'redis_config' => 'default',
];
return $config;

View File

@ -16,6 +16,7 @@ class_alias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin');
class_alias(\OneBot\V12\Object\OneBotEvent::class, 'OneBotEvent');
class_alias(\ZM\Context\BotContext::class, 'BotContext');
class_alias(\ZM\Store\KV\LightCache::class, 'LightCache');
class_alias(\ZM\Store\KV\Redis\KVRedis::class, 'KVRedis');
// 下面是 OneBot 相关类的全局别称
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');

View File

@ -21,6 +21,7 @@ use ZM\Store\Database\DBException;
use ZM\Store\Database\DBPool;
use ZM\Store\FileSystem;
use ZM\Store\KV\LightCache;
use ZM\Store\KV\Redis\RedisPool;
use ZM\Utils\ZMUtil;
class WorkerEventListener
@ -219,6 +220,9 @@ class WorkerEventListener
foreach (DBPool::getAllPools() as $name => $pool) {
DBPool::destroyPool($name);
}
foreach (RedisPool::getAllPools() as $name => $pool) {
RedisPool::destroyPool($name);
}
// 读取 MySQL 配置文件
$conf = config('global.database');
@ -228,5 +232,12 @@ class WorkerEventListener
DBPool::create($name, $conn_conf);
}
}
$redis_conf = config('global.redis');
foreach ($redis_conf as $name => $conn_conf) {
if (($conn_conf['enable'] ?? true) !== false) {
RedisPool::create($name, $conn_conf);
}
}
}
}

View File

@ -317,17 +317,23 @@ class Framework
foreach (config('global.servers') as $k => $v) {
$properties['listen_' . $k] = $v['type'] . '://' . $v['host'] . ':' . $v['port'];
}
// 打印 MySQL 连接信息
if ((config('global.mysql_config.host') ?? '') !== '') {
$conf = config('global', 'mysql_config');
$properties['mysql'] = $conf['dbname'] . '@' . $conf['host'] . ':' . $conf['port'];
// 打印 database 连接信息
foreach (config('global.database') as $name => $db) {
if (!$db['enable']) {
continue;
}
$properties['db[' . $name . ']'] = match ($db['type']) {
'sqlite' => $db['type'] . '://' . $db['dbname'],
'mysql' => $db['type'] . '://' . $db['host'] . ':' . $db['port'] . '/' . $db['dbname'],
default => '未知数据库类型',
};
}
// 打印 Redis 连接信息
if ((config('global', 'redis_config')['host'] ?? '') !== '') {
$conf = config('global', 'redis_config');
$properties['redis_pool'] = $conf['host'] . ':' . $conf['port'];
// 打印 redis 连接信息
foreach (config('global.redis') as $name => $redis) {
if ($redis['enable']) {
$properties['redis[' . $name . ']'] = $redis['host'] . ':' . $redis['port'];
}
}
if (LOAD_MODE === 0) {
logger()->info('框架正以源码模式启动');
}

View File

@ -63,6 +63,20 @@ class DBPool
case SwooleDriver::class:
self::$pools[$name] = new SwooleObjectPool($size, \PDO::class, $connect_str, ...$args);
}
switch ($config['type']) {
case 'sqlite':
/** @var \PDO $pool */
$pool = self::$pools[$name]->get();
$a = $pool->query('select sqlite_version();')->fetchAll()[0][0] ?? '';
if (str_starts_with($a, '3')) {
logger()->debug('sqlite ' . $name . ' connected');
}
self::$pools[$name]->put($pool);
break;
case 'mysql':
// TODO: 编写验证 MySQL 连接有效性的功能
break;
}
}
/**

View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace ZM\Store\KV\Redis;
use Psr\SimpleCache\CacheInterface;
use ZM\Store\KV\KVInterface;
class KVRedis implements CacheInterface, KVInterface
{
private string $pool_name;
public function __construct(private string $name = '')
{
$this->pool_name = config('global.kv.redis_config', 'default');
}
public static function open(string $name = ''): CacheInterface
{
return new KVRedis($name);
}
/**
* {@inheritDoc}
*/
public function get(string $key, mixed $default = null): mixed
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = $redis->get($this->name . ':' . $key);
if ($ret === false) {
$ret = $default;
} else {
$ret = unserialize($ret);
}
RedisPool::pool($this->pool_name)->put($redis);
return $ret;
}
/**
* {@inheritDoc}
*/
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = $redis->set($this->name . ':' . $key, serialize($value), $ttl);
RedisPool::pool($this->pool_name)->put($redis);
return (bool) $ret;
}
/**
* {@inheritDoc}
*/
public function delete(string $key): bool
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = $redis->del($this->name . ':' . $key);
RedisPool::pool($this->pool_name)->put($redis);
return (bool) $ret;
}
/**
* {@inheritDoc}
*/
public function clear(): bool
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = $redis->del($this->name . ':*');
RedisPool::pool($this->pool_name)->put($redis);
return (bool) $ret;
}
/**
* {@inheritDoc}
*/
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
foreach ($keys as $key) {
$value = $redis->get($this->name . ':' . $key);
if ($value === false) {
$value = $default;
} else {
$value = unserialize($value);
}
yield $key => $value;
}
RedisPool::pool($this->pool_name)->put($redis);
}
/**
* {@inheritDoc}
*/
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = true;
foreach ($values as $key => $value) {
$ret = $ret && $redis->set($this->name . ':' . $key, serialize($value), $ttl);
}
RedisPool::pool($this->pool_name)->put($redis);
return $ret;
}
/**
* {@inheritDoc}
*/
public function deleteMultiple(iterable $keys): bool
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = true;
foreach ($keys as $key) {
$ret = $ret && $redis->del($this->name . ':' . $key);
}
RedisPool::pool($this->pool_name)->put($redis);
return $ret;
}
/**
* {@inheritDoc}
*/
public function has(string $key): bool
{
/** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get();
$ret = $redis->exists($this->name . ':' . $key);
RedisPool::pool($this->pool_name)->put($redis);
return (bool) $ret;
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Store\KV\Redis;
use ZM\Exception\ZMException;
class RedisException extends ZMException
{
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace ZM\Store\KV\Redis;
use OneBot\Driver\Driver;
use OneBot\Driver\Interfaces\PoolInterface;
use OneBot\Driver\Swoole\ObjectPool as SwooleObjectPool;
use OneBot\Driver\Swoole\SwooleDriver;
use OneBot\Driver\Workerman\ObjectPool as WorkermanObjectPool;
use OneBot\Driver\Workerman\WorkermanDriver;
class RedisPool
{
/**
* @var array<string, SwooleObjectPool|WorkermanObjectPool> 连接池列表
*/
public static array $pools = [];
/**
* @throws RedisException
*/
public static function create(string $name, array $config): void
{
$size = $config['pool_size'] ?? 10;
self::checkRedisExtension();
switch (Driver::getActiveDriverClass()) {
case WorkermanDriver::class:
self::$pools[$name] = new WorkermanObjectPool($size, ZMRedis::class, $config);
break;
case SwooleDriver::class:
self::$pools[$name] = new SwooleObjectPool($size, ZMRedis::class, $config);
break;
}
/** @var ZMRedis $r */
$r = self::$pools[$name]->get();
try {
/* @phpstan-ignore-next-line */
$result = $r->ping('123');
if (str_contains($result, '123')) {
self::$pools[$name]->put($r);
logger()->debug('Redis pool ' . $name . ' created');
}
} catch (\RedisException $e) {
self::$pools[$name] = null;
logger()->error(zm_internal_errcode('E00047') . 'Redis init failed! ' . $e->getMessage());
}
}
/**
* 获取一个数据库连接池
*
* @param string $name 连接池名称
*/
public static function pool(string $name): PoolInterface
{
if (!isset(self::$pools[$name]) && count(self::$pools) !== 1) {
throw new \RuntimeException("Pool {$name} not found");
}
return self::$pools[$name] ?? self::$pools[array_key_first(self::$pools)];
}
/**
* @throws RedisException
*/
public static function checkRedisExtension(): void
{
if (!extension_loaded('redis')) {
throw new RedisException(zm_internal_errcode('E00029') . '未安装 redis 扩展');
}
}
/**
* 销毁数据库连接池
*
* @param string $name 数据库连接池名称
*/
public static function destroyPool(string $name)
{
unset(self::$pools[$name]);
}
/**
* 获取所有数据库连接池
*
* @return PoolInterface[]
*/
public static function getAllPools(): array
{
return self::$pools;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace ZM\Store\KV\Redis;
class ZMRedis extends \Redis
{
/**
* @throws \RedisException
*/
public function __construct(array $config)
{
parent::__construct();
$this->connect($config['host'], $config['port'], $config['timeout'] ?? 0);
if (!empty($config['auth'])) {
$this->auth($config['auth']);
}
$this->select($config['index']);
}
/**
* @throws \RedisException
*/
public function __destruct()
{
$this->close();
}
}