zenstruck/redis

php-redis 的懒代理,包含 DX 助手、实用工具和统一的 API。

资助包维护!
kbond

v0.1.0 2022-03-18 16:55 UTC

This package is auto-updated.

Last update: 2024-08-30 01:28:59 UTC


README

CI Status Code Coverage

php-redis 的懒代理,包含 DX 助手、实用工具和统一的 API。

Zenstruck\Redis\Redis|\RedisArray|\RedisCluster 的统一代理。在少数例外和考虑的情况下,API 在底层客户端之间是相同的。这允许你在开发时使用相同的 API,你很可能只使用 \Redis,在生产中,你可能使用 \RedisArray\RedisCluster

代理是懒加载的,即通过 DSN 创建时,直到执行命令才会实例化底层客户端。

此库与 Symfony 集成良好,并提供了一个配方。

安装

composer require zenstruck/redis

Redis 工厂

通过 DSN 字符串创建 Redis 客户端实例。DSN 必须使用以下格式

redis[s]://[pass@][ip|host|socket[:port]][/db-index][?{option}={value}...]

Redis 代理工厂

建议尽可能使用代理。与使用 真实 客户端相比,它有以下优点

  1. 懒加载:在真正调用 Redis 命令之前不会建立连接。
  2. 封装:大部分情况下,不需要了解 真实 客户端。您不需要根据使用的客户端更改您的使用方式。这里有一些 例外
  3. 开发者体验 (DX):使用 流畅的序列和事务 API

以下是从 DSN 创建代理的一些示例。

use Zenstruck\Redis;

$proxy = Redis::create('redis://localhost'); // Zenstruck\Redis<\Redis>
$proxy = Redis::create('redis://localhost?redis_sentinel=sentinel_service'); // Zenstruck\Redis<\Redis> (using Redis Sentinel)
$proxy = Redis::create('redis:?host[host1]&host[host2]'); // Zenstruck\Redis<\RedisArray>
$proxy = Redis::create('redis:?host[host1]&host[host2]&redis_cluster=1'); // Zenstruck\Redis<\RedisCluster>

您还可以从现有的 \Redis|\RedisArray|\RedisCluster 实例创建代理

use Zenstruck\Redis;

/** @var \Redis|\RedisArray|\RedisCluster $client */

$proxy = Redis::wrap($client)

Redis 真实 客户端工厂

可以直接创建 \Redis|\RedisArray|\RedisCluster 实例

use Zenstruck\Redis;

$client = Redis::createClient('redis://localhost'); // \Redis
$client = Redis::createClient('redis://localhost?redis_sentinel=sentinel_service'); // \Redis (using Redis Sentinel)
$client = Redis::createClient('redis:?host[host1]&host[host2]'); // \RedisArray
$client = Redis::createClient('redis:?host[host1]&host[host2]&redis_cluster=1'); // \RedisCluster

工厂选项

某些 Redis 选项可以通过您的 DSN 的查询参数设置,或者作为数组传递给 Zenstruck\Redis::create/createClient() 的第二个参数。

前缀

您可以设置所有键的前缀

use Zenstruck\Redis;

$proxy = Redis::create('redis://localhost?prefix=app:');
$proxy = Redis::create('redis://localhost', ['prefix' => 'app:']); // equivalent to above

序列化器选项

默认情况下,Redis 将所有标量/空值存储为字符串,将对象/数组存储为 "Array"/"Object"。为了正确存储具有类型的值和对象/数组,您必须配置 Redis 序列化器

use Zenstruck\Redis;

// PHP: serialize/unserialize values
$proxy = Redis::create('redis://localhost?serializer=php');
$proxy = Redis::create('redis://localhost', ['serializer' => \Redis::SERIALIZER_PHP]); // equivalent to above

// JSON: json_encode/json_decode values (doesn't work for objects)
$proxy = Redis::create('redis://localhost?serializer=json');
$proxy = Redis::create('redis://localhost', ['serializer' => \Redis::SERIALIZER_JSON]); // equivalent to above

注意:使用 Redis 序列化时存在性能权衡。考虑为需要序列化的操作/逻辑创建单独的客户端。

Redis 代理 API

/** @var Zenstruck\Redis $proxy */

// call any \Redis|\RedisArray|\RedisCluster method
$proxy->set('mykey', 'value');
$proxy->get('mykey'); // "value"

// get the "real" client
$proxy->realClient(); // \Redis|\RedisArray|\RedisCluster

序列/管道和事务

代理具有用于 Redis 管道和事务的流畅、自动完成的 API

/** @var Zenstruck\Redis $proxy */

// use \Redis::multi()
$results = $proxy->transaction()
    ->set('x', '42')
    ->incr('x')
    ->get('x')->as('value') // alias the result of this command
    ->del('x')
    ->execute() // the results of the above transaction as an array (keyed by index of command or alias if set)
;

$results['value']; // "43" (result of ->get())
$results[3]; // true (result of ->del())

// use \Redis::pipeline() - see note below about \RedisCluster
$proxy->sequence()
    ->set('x', '42')
    ->incr('x')
    ->get('x')->as('value') // alias the result of this command
    ->del('x')
    ->execute() // the results of the above sequence as an array (keyed by index of command of alias if set)
;

$results['value']; // "43" (result of ->get())
$results[3]; // true (result of ->del())

注意:当使用 sequence()\RedisCluster 时,命令作为原子操作执行,因为不支持管道。

注意:当使用 sequence()/transaction()\RedisArray 实例时,序列/事务中的第一个命令必须是 "基于键的命令"(例如 get()/set())。这是为了选择事务运行的节点。

可计数的\可迭代的

Zenstruck\Redis 是可计数的和可迭代的。在计数/迭代时,根据底层客户端存在一些差异

  • \Redis:计数始终为1,并对其自身迭代一次
  • \RedisArray:计数为主机数量,并迭代每个由代理包装的主机。
  • \RedisCluser:计数为主机数量,并迭代每个预先设置了节点参数主节点。这允许在主节点上执行节点命令,而无需将这些命令的节点参数传递给这些命令(在迭代时)
/** @var Zenstruck\Redis $proxy */

$proxy->count(); // 1 if \Redis, # hosts if \RedisArray, # "masters" if \RedisCluster

foreach ($proxy as $node) {
    $proxy->flushAll(); // this is permitted even for \RedisCluster (which typically requires a $nodeParams argument)
}

注意:如果运行需要在每个主机/主节点上运行的命令,即使使用\Redis也建议迭代并运行。这允许将来无缝过渡到\RedisArray/\RedisCluster

实用工具

ExpiringSet

Zenstruck\Redis\Utility\ExpiringSet封装了Redis过期集合的概念:一个集合(无序列表,无重复项),其成员在一段时间后过期。对集合的每次读写操作都会修剪过期的成员。

/** @var Zenstruck\Redis $client */

$set = $client->expiringSet('my-set'); // redis key to store the set

$set->add('member1', 600); // set add "member1" that expires in 10 minutes
$set->add('member1', new \DateInterval::createFromDateString('5 minutes')); // can use \DateInterval for the TTL
$set->add('member1', new \DateTime('+5 minutes')); // use \DateTimeInterface to set specific expiry timestamp

$set->remove('member1'); // explicitly remove a member

$set->all(); // array - all unexpired members

$set->contains('member'); // true/false

$set->clear(); // clear all items

$set->prune(); // explicitly "prune" the set (remove expired members)

count($set); // int - number of unexpired members

foreach ($set as $member) {
    // iterate over unexpired members
}

// fluent
$set
    ->add('member1', 600)
    ->add('member2', 600)
    ->remove('member1')
    ->remove('member2')
    ->prune()
    ->clear()
;

注意:为了使用复杂类型(数组/对象)作为成员,您的Redis客户端必须配置了序列化器

以下是使用此对象跟踪网站活跃用户的伪代码示例。当认证用户登录或请求页面时,将他们的用户名添加到集合中,具有5分钟的空闲生存时间(TTL)。用户在此时间内被认为是活跃的。在登出时,他们将从集合中移除。如果用户在其最后TTL内没有发出请求,他们将从不活跃集合中移除。

/** @var Zenstruck\Redis $client */

$set = $client->expiringSet('active-users');
$ttl = \DateInterval::createFromDateString('5 minutes');

// LOGIN EVENT:
$set->add($event->getUsername(), $ttl);

// LOGOUT EVENT:
$set->remove($event->getUsername());

// REQUEST EVENT:
$set->add($event->getUsername(), $ttl);

// ADMIN MONITORING DASHBOARD WIDGET
$activeUserCount = count($set);
$activeUsernames = $set->all(); // [user1, user2, ...]

// ADMIN USER CRUD LISTING
foreach ($users as $user) {
    $isActive = $set->contains($user->getUsername()); // bool
    // ...
}

集成

Symfony框架

添加支持的Redis DSN环境变量

# .env

REDIS_DSN=redis://localhost

配置服务

# config/packages/zenstruck_redis.yaml

services:

    # Proxy that is autowireable
    Zenstruck\Redis:
        factory: ['Zenstruck\Redis', 'create']
        arguments: ['%env(REDIS_DSN)%']

    # Separate proxy's that have different prefixes
    redis1:
        class: Zenstruck\Redis
        factory: ['Zenstruck\Redis', 'create']
        arguments: ['%env(REDIS_DSN)%', { prefix: 'prefix1:' }]
    redis2:
        class: Zenstruck\Redis
        factory: ['Zenstruck\Redis', 'create']
        arguments: ['%env(REDIS_DSN)%', { prefix: 'prefix2:' }]

    # Separate proxy that uses PHP serialization
    serialization_redis:
        class: Zenstruck\Redis
        factory: ['Zenstruck\Redis', 'create']
        arguments: ['%env(REDIS_DSN)%', { serializer: php }]

    # expiring set service
    active_users:
        class: Zenstruck\Redis\Utility\ExpiringSet
        factory: ['@Zenstruck\Redis', 'expiringSet']
        arguments:
            - active_users # redis key

    # Specific clients that are autowireable
    Redis:
        class: Redis
        factory: ['Zenstruck\Redis', 'createClient']
        arguments: ['%env(REDIS_DSN)%'] # note REDIS_DSN must be for \Redis client

    RedisArray:
        class: RedisArray
        factory: ['Zenstruck\Redis', 'createClient']
        arguments: ['%env(REDIS_DSN)%'] # note REDIS_DSN must be for \RedisArray client

    RedisCluster:
        class: RedisCluster
        factory: ['Zenstruck\Redis', 'createClient']
        arguments: ['%env(REDIS_DSN)%'] # note REDIS_DSN must be for \RedisCluster client

使用Zenstruck\Redis进行会话存储(有关更多详细信息/选项,请参阅Symfony文档

# config/services.yaml

# Assumes "Zenstruck\Redis" is available as a service and symfony/expression-language is installed
services:
    redis_session_handler:
        class:  Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
        arguments:
            - "@=service('Zenstruck\\\\Redis').realClient()"

# config/packages/framework.yaml
framework:
    # ...
    session:
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler

贡献

运行测试套件

composer install
docker compose up -d # setup redis, redis-cluster, redis-sentinel
vendor/bin/phpunit -c phpunit.docker.xml

致谢

创建从DSN创建php-redis客户端的大部分代码已被从Symfony框架中提取和修改。