PHP'den Redis'e bağlanmak için Redis belgelerinin önerdiği kütüphane Predis'tir: composer require predis/predis ile kurar, new Predis\Client([...]) ile bir istemci oluşturur ve $r->set('foo', 'bar'), $r->get('foo') gibi komut adlarıyla aynı isimdeki metotları çağırırsınız. Predis üçüncü taraf bir kütüphanedir; Redis ekibi onu doğrudan geliştirmez ama katkı verir ve sorunlarda yardımcı olur, belgeler bunu "en iyi çaba" (best effort) desteği olarak tanımlar. Saf PHP ile yazıldığı için C eklentisi olan phpredis'e göre performansı sınırlıdır; buna karşılık Redis belgelerinde çok daha iyi belgelenmiştir.

Bu yazıda Predis'in kurulumunu, bağlantı parametrelerini, URL ile bağlanırken dikkat edilecek noktaları, cluster ve Sentinel bağlantılarını, pipeline ve transaction kullanımını, cas seçeneğiyle iyimser kilidi, Predis 3 ile değişen hset çağrısını ve istisna hiyerarşisini belgelerdeki örneklerle anlatıyoruz. Yazı PHP dilini değil, Redis kütüphanesini konu alıyor; dil tarafı için PHP öğrenme rehberi yazısına bakabilirsiniz. Serinin geneli Redis öğrenme rehberi sayfasında.

Predis mi phpredis mi?

Predis mi phpredis mi? Uygulama biçimi, Kurulum, Redis ekibinin desteği, Performans

Redis belgeleri PHP için iki seçenekten söz eder ve aralarındaki farkı açıkça koyar. Seçimi yaparken şu tabloyu kullanabilirsiniz:

ÖzellikPredisphpredis
Uygulama biçimiSaf PHP kütüphanesiC ile yazılmış PHP eklentisi
KurulumComposer paketiPHP eklentisi olarak
Redis ekibinin desteğiEn iyi çaba desteği, belgelerde iyi kapsanıyorRedis ekibi tarafından desteklenmiyor ve belgelenmiyor
PerformansPHP ile sınırlıC eklentisi olduğu için daha yüksek

Belgelerdeki karar ağacının PHP sorusu da tam olarak budur: en yüksek performans, tam destek ve belgeden daha önemliyse phpredis; değilse Predis. Bu yazıdaki tüm örnekler Predis içindir.

Predis kurulumu ve bağlantı

bash
composer require predis/predis

Belgelerdeki temel bağlantı örneği parametreleri bir dizi olarak verir. scheme alanı düz TCP için tcp, TLS için tls olur; database mantıksal veritabanı numarasıdır:

php
<?php

require 'vendor/autoload.php';

use Predis\Client as PredisClient;

$r = new PredisClient([
                'scheme'   => 'tcp',
                'host'     => '127.0.0.1',
                'port'     => 6379,
                'password' => '',
                'database' => 0,
            ]);

echo $r->set('foo', 'bar'), PHP_EOL;
// >>> OK

echo $r->get('foo'), PHP_EOL;
// >>> bar

Hash ile çalışmak için hset() ve hgetall() kullanılır. hgetall() bir PHP ilişkisel dizisi döndürür ve belgelerdeki çıktıda görüldüğü gibi sayı olarak yazdığınız 29 değeri '29' metni olarak geri gelir:

php
$r->hset('user-session:123', 'name', 'John');
$r->hset('user-session:123', 'surname', 'Smith');
$r->hset('user-session:123', 'company', 'Redis');
$r->hset('user-session:123', 'age', 29);

echo var_export($r->hgetall('user-session:123')), PHP_EOL;
/* >>>
array (
  'name' => 'John',
  'surname' => 'Smith',
  'company' => 'Redis',
  'age' => '29',
)
*/

Hash komutlarının sunucu tarafındaki davranışını Redis hash komutları yazısında anlatıyoruz.

Predis 3 ve hset çağrısı

Belgelerdeki kullanım örnekleri önemli bir sürüm farkına dikkat çeker. Önceden getirme (prefetch) önbelleği örneğine göre Predis 1.x, hset için ['alan' => 'değer'] biçiminde ilişkisel bir diziyi doğrudan kabul ediyordu; aynı çağrı Predis 3.x'e karşı wrong number of arguments for 'hset' hatasıyla sonuçlanır. Özellik deposu örneği de Predis 3'ün hset metodunun alan ve değerleri sırayla, değişken sayıda argüman olarak beklediğini söyler. Bu yüzden belgeler ilişkisel diziyi önce düz bir listeye çevirir:

php
$flat = [];
foreach ($fields as $name => $value) {
    $flat[] = $name;
    $flat[] = $value;
}
$pipe->hset($key, ...$flat);

Aynı örnek, alan listesi boşken HSET'in hiç argümansız çağrılmasının Predis 3.x'te hata verdiğini de not eder; bu nedenle boş dizi gelmesi ihtimaline karşı çağrıdan önce kontrol koymak gerekir. Belgelerin bazı sayfalarında hâlâ ilişkisel dizili çağrılar göründüğü için, kendi projenizde hangi Predis sürümünü kullandığınızı composer.json dosyanızdan kontrol etmeniz iyi olur.

URL ile Predis bağlantısı ve dikkat edilecekler

Predis, bağlantı URL'sini doğrudan Predis\Client yapıcısına almanıza izin verir: new Predis\Client('rediss://default:şifre@sunucu:6379/0'). Ancak bağlantı URL'leri sayfası Predis için birkaç istisnayı ayrıca listeler:

  • Predis kimlik bilgilerini ve veritabanını adresten yalnızca redis ve rediss şemalarında okur. tcp, tls ve unix şemalarında username, password ve database sorgu parametrelerini kullanmanız gerekir.
  • Adresteki kullanıcı adı ve şifrede yüzde kodlamasını çözmez. Şifreniz @ veya : gibi özel karakterler içeriyorsa onu kodlanmış bir password sorgu parametresi olarak verin.
  • Veritabanı parametresinin adı db değil database'dir.
  • Bütün sorgu parametrelerini bağlantıya iletir; örneğin timeout, read_write_timeout, persistent ve protocol. RESP3 için ?protocol=3 yazılabilir.
  • TLS seçenekleri ssl parametreleriyle verilebilir, örneğin ?ssl[cafile]=<ca-dosyası>&ssl[verify_peer]=1.

Bu kurallara göre, şifresi p@ss:w+rd olan uygulama kullanıcısıyla 2 numaralı veritabanına bağlanan bir tcp URL'si şöyle yazılabilir. Kodlama, bağlantı URL'leri sayfasındaki örnekle aynıdır:

php
$r = new Predis\Client('tcp://10.0.0.5:6379?username=uygulama&password=p%40ss%3Aw%2Brd&database=2');

Diğer dillerde de geçerli olan genel URL kuralları, yani küçük harfli şema, portun her zaman yazılması ve aynı ayarın hem URL'de hem kodda verilmemesi, Redis bağlantı URL ve connection pool yazısında.

Cluster, Sentinel ve TLS bağlantıları

Cluster, Sentinel ve TLS bağlantıları

Cluster'a bağlanmak için ilk parametrede bir veya daha fazla düğüm adresi verir, seçeneklerde 'cluster' => 'redis' ayarlarsınız:

php
$clusterNodes = [
    'tcp://127.0.0.1:30001', // Node 1
    'tcp://127.0.0.1:30002', // Node 2
    'tcp://127.0.0.1:30003', // Node 3
];
$options    = ['cluster' => 'redis'];

// Create a Predis client for the cluster
$rc = new PredisClient($clusterNodes, $options);

echo $rc->set('foo', 'bar'), PHP_EOL;
// >>> OK
echo $rc->get('foo'), PHP_EOL;
// >>> bar

Sentinel için ilk parametre Sentinel adreslerinin listesidir; seçeneklerde 'replication' => 'sentinel' ve service alanına izlenen birincil sunucunun adı yazılır. Belgelerde birkaç önemli ayrıntı var. Sentinel'e bağlanma zaman aşımı varsayılan olarak 100 milisaniyedir; Sentinel'leriniz uzak sunuculardaysa her adrese tcp://<sunucu>:26379?timeout=0.5 gibi daha uzun bir süre ekleyin. Sentinel kimlik bilgileri her Sentinel adresine sorgu parametresi olarak, veri düğümlerininki ise parameters seçeneğinde verilir; Sentinel kullanıcı adı default olsa bile yazılmalıdır, aksi hâlde Predis parameters içindeki kullanıcı adını Sentinel'ler için de kullanır.

Sentinel kipinde Predis, ilk yazma komutuna kadar salt okunur komutları bir kopyaya gönderir; ilk yazmadan sonra her şeyi birincile yollar. Okumaları yeniden kopyaya göndermek için $r->getConnection()->switchToSlave() çağrılır. Replikasyon asenkron olduğu için kopya eski veri döndürebilir. Ayrıca Predis Sentinel'lerin yük devretme duyurularını dinlemez; yük devretmeden sonra Sentinel eski birincili kopyaya çevirip bağlantıyı kapatana kadar onu kullanmaya devam eder ve bu aralıkta kabul edilen yazmalar kaybolur.

TLS bağlantısında scheme değeri tls olur ve sertifika bilgileri options içindeki ssl dizisinde verilir: verify_peer, cafile, local_cert ve local_pk.

Predis ile pipeline

Predis'te pipeline iki biçimde kurulabilir. Birincisinde pipeline() bir nesne döndürür, komutları ona eklersiniz ve execute() çağırdığınızda hepsi tek gidiş dönüşte gönderilir; execute() tüm sonuçları sırasıyla içeren bir dizi döndürür. Belgelerdeki önceden getirme örneği binlerce kaydı bu yolla tek seferde yükler:

php
$pipe = $this->redis->pipeline();
foreach ($records as $record) {
    $cacheKey = $this->cacheKey($record['id']);
    $pipe->del([$cacheKey]);
    $pipe->hset($cacheKey, ...self::flattenFields($record));
    $pipe->expire($cacheKey, $this->ttlSeconds);
}
$pipe->execute();

İkincisinde pipeline() metoduna bir geri çağırma fonksiyonu verirsiniz; Predis fonksiyon dönünce topluluğu kendiliğinden çalıştırır ve sonuç dizisini döndürür. Oturum deposu örneği oturumun son erişim zamanını günceller, süresini yeniler ve güncel hash'i aynı pipeline'da okur; üçüncü komutun sonucu dizinin 2 numaralı elemanıdır:

php
$result = $this->redis->pipeline(function ($pipe) use ($key, $sessionTtl): void {
    $pipe->hset($key, 'last_accessed_at', $this->timestamp());
    $pipe->expire($key, $sessionTtl);
    $pipe->hgetall($key);
});

$refreshed = $result[2] ?? [];

Belgeler bir ayrıntıyı vurgular: Predis'in pipeline'ı varsayılan olarak transaction değildir. Komutlar kuyruğa girip tek gidiş dönüşte gönderilir ama MULTI/EXEC bloğu içinde çalışmaz. Kayıtları atomik olarak uygulamanın gerekmediği toplu yüklemelerde bu doğru tercihtir ve yüklemeyi hızlı tutar. Önbellek zaten okunuyorsa ve okuyanların yarım yüklenmiş bir kayıt görmemesi gerekiyorsa, ya yazanları durdurmanız ya da işi bir transaction'a almanız gerekir. Örneklerdeki del([$cacheKey]) çağrısının anahtarları dizi olarak aldığına da dikkat edin. Pipelining'in sunucu tarafındaki mantığı Redis pipelining yazısında.

Pipeline mı transaction mı?

Belgelerdeki üç PHP örneği bu kararı farklı durumlarda farklı veriyor ve aradaki mantık öğretici. Oturum deposu, oturum hash'ini ve süresini callback'li bir pipeline ile yazar; burada amaç gidiş dönüş sayısını azaltmaktır. Özellik deposu, binlerce kullanıcının özelliklerini toplu yüklerken yine transaction'sız pipeline kullanır, çünkü her kullanıcının HSET ve EXPIRE çifti diğerlerinden bağımsızdır; belgeler çiftin ayrılamaz olması gereken nadir durumlar için transaction() ya da bir Lua betiği önerir. Önceden getirme önbelleği ise canlı eşitleme sırasında bir kaydı silip yeniden yazarken transaction kullanır, çünkü o anda önbellekten okuma yapılmaktadır ve okuyan biri eski alanlarla yeni alanların karışımını görmemelidir. Kısacası soru şudur: komutların arasına başka bir istemcinin komutu girerse bir sorun olur mu? Olmuyorsa pipeline yeterlidir ve daha hafiftir; oluyorsa transaction gerekir. Okuduğunuz değere göre karar verip yazıyorsanız transaction da yetmez, aşağıdaki iyimser kilide ihtiyacınız vardır.

transaction() ile MULTI ve EXEC

Transaction da benzer biçimde kurulur. transaction() metoduna bir geri çağırma verirseniz Predis içindeki komutları kendiliğinden MULTI ve EXEC ile sarar. Argümansız çağırırsanız bir transaction nesnesi döner; komutları ekleyip execute() çağırırsınız. Belgelerdeki eşitleme örneği bir kaydı önce siler, sonra yeniden yazar ve süresini yeniler; üç komut atomik çalıştığı için önbellekte hiçbir zaman eski ve yeni alanların karışımı görünmez:

php
$tx = $this->redis->transaction();
$tx->del([$cacheKey]);
$tx->hset($cacheKey, ...self::flattenFields($fields));
$tx->expire($cacheKey, $this->ttlSeconds);
$tx->execute();

cas ve watch ile iyimser kilit

Bir değeri okuyup ona göre yazmak istediğinizde iyimser kilit kullanılır. Predis'te transaction() metoduna seçenek dizisi verirsiniz: cas kontrol et ve ayarla (check-and-set) davranışını açar, watch izlenecek anahtarı belirtir, retry ise izlenen anahtar EXEC'ten önce değişirse transaction'ın kaç kez kendiliğinden yeniden deneneceğini söyler. Geri çağırma içinde önce okuma yapılır, ardından $tx->multi() ile kuyruğa alma başlar. Transaction iptal edildiğinde Predis AbortedMultiExecException fırlatır; hata yönetimi sayfasındaki örnek bunu üç denemeyle ele alır:

php
use Predis\Transaction\AbortedMultiExecException;

for ($attempt = 0; $attempt < 3; $attempt++) {
    try {
        return $r->transaction(['cas' => true, 'watch' => $key], function ($tx) use ($key) {
            $current = $tx->get($key);
            $tx->multi();
            $tx->set($key, strtoupper($current));
        });
    } catch (AbortedMultiExecException $e) {
        if ($attempt === 2) {
            throw $e;
        }
    }
}

Transaction'ların sunucu tarafında nasıl çalıştığı, neden geri alma (rollback) olmadığı ve EXECABORT hatasının anlamı MULTI, EXEC ve WATCH yazısında.

Lua betiği çalıştırma

Önbellek örneği, aynı anda süresi dolan popüler bir anahtarı tek bir işçinin yeniden yüklemesi için kısa ömürlü bir kilidi Lua betiğiyle alır. Predis'te eval() metodunun imzasına dikkat edin: önce betik, sonra anahtar sayısı, ardından anahtarlar ve argümanlar gelir:

php
$acquired = (int) $this->redis->eval(
    self::ACQUIRE_LOCK_SCRIPT, 1, $lockKey, $token, (string) $this->lockTtlMs
);

Burada 1, kendisinden sonra gelen ilk değerin anahtar, geri kalanların argüman olduğunu belirtir. Betik tarafında bunlar KEYS[1] ve ARGV dizisi olarak okunur. Betiklerin yazımını Lua script, EVAL ve EVALSHA yazısında, kilit deseninin güvenlik tarafını Redis ile dağıtık kilit yazısında anlatıyoruz.

Predis istisna hiyerarşisi

Predis istisna hiyerarşisi: Predis\Connection\ConnectionException, Predis\CommunicationException, Predis\Response\ServerException, Predis\Transaction\AbortedMultiExecException

Predis hataları istisnalarla bildirir ve hepsini PredisException altında toplar. CommunicationException bunun altındadır ve ConnectionException ile ProtocolException'ı içerir; ClientException, ServerException ve AbortedMultiExecException da diğer alt sınıflardır.

İstisnaNe zaman oluşurÖnerilen davranış
Predis\Connection\ConnectionExceptionBağlanamadı veya bağlantı koptuBekleyerek yeniden dene veya yedeğe dön
Predis\CommunicationExceptionOkuma ya da yazma sırasında taşıma katmanı hatasıBekleyerek yeniden dene ve yeniden bağlan
Predis\Response\ServerExceptionRedis WRONGTYPE gibi bir hata yanıtı döndürdüKomutu, argümanları veya veri modelini düzelt
Predis\Transaction\AbortedMultiExecExceptionİzlenen transaction iptal edildiDurumu yeniden oku ve transaction'ı tekrarla

Belgeler üretim kodunda geçici taşıma hatalarıyla sunucu tarafı komut hatalarının ayırt edilmesini ister. Zarif gerileme deseninde yalnızca bağlantı hatasında veritabanına dönülür; yeniden deneme deseninde ise CommunicationException yakalanıp bekleme süresi her seferinde ikiye katlanır:

php
use Predis\Connection\ConnectionException;
use Predis\CommunicationException;

try {
    $cachedValue = $r->get($key);
    if ($cachedValue !== null) {
        return $cachedValue;
    }
} catch (ConnectionException $e) {
    $logger->warning('Cache unavailable, using database');
}

$delayMs = 100;
for ($attempt = 0; $attempt < 3; $attempt++) {
    try {
        return $r->get($key);
    } catch (CommunicationException $e) {
        if ($attempt === 2) {
            throw $e;
        }
        usleep($delayMs * 1000);
        $delayMs *= 2;
    }
}

ServerException için ise hızlı başarısız olma deseni önerilir: yakalayıp yeniden fırlatın, çünkü bu genellikle kodunuzda ya da şemanızda bir hata olduğunu gösterir. Sunucu hata mesajlarının tek tek anlamı Redis hata mesajları yazısında.

Uygulama yapısı açısından da belgelerden bir not: özellik deposu örneği PHP'nin yerleşik geliştirme sunucusunun (php -S) tek iş parçacıklı olduğunu ve üretime uygun olmadığını belirtir. Gerçek bir dağıtımda PHP-FPM, nginx veya Apache arkasında çalışır ve rota işleyicileri Predis\Client bağlantılarını işçi başına yöneten bir PHP çatısına taşınır.

Sık yapılan hatalar

  • Predis 3'te hset'e ilişkisel dizi vermek. Alan ve değerleri düz listeye çevirip ...$flat ile geçin.
  • Özel karakterli şifreyi adreste kodlayıp Predis'in çözeceğini beklemek. Predis adresteki kodlamayı çözmez; password sorgu parametresini kullanın.
  • Veritabanı için db parametresi yazmak. Predis'te adı database'dir.
  • pipeline()'ın atomik olduğunu varsaymak. Predis pipeline'ı varsayılan olarak transaction değildir; atomiklik gerekiyorsa transaction() kullanın.
  • Uzak Sentinel'lerde varsayılan 100 ms zaman aşımıyla kalmak. Adreslere ?timeout=0.5 gibi daha uzun bir süre ekleyin.

Sık Sorulan Sorular

Predis resmî bir Redis kütüphanesi mi?

Hayır, üçüncü taraf bir kütüphanedir. Redis ekibi katkı verir ve sorunlarda yardımcı olur, belgelerde de iyi kapsanır; ancak doğrudan Redis tarafından geliştirilmez ve desteklenmez.

Pipeline sonuçlarına nasıl erişirim?

execute() ya da geri çağırmalı pipeline() sonuçları komut sırasına göre bir dizi olarak döndürür. Üçüncü komutun sonucu $result[2]'dir.

WATCH ile transaction iptal olursa ne olur?

cas ve watch seçenekleriyle kurulan transaction iptal edildiğinde Predis AbortedMultiExecException fırlatır. Durumu yeniden okuyup transaction'ı tekrarlayın ya da retry seçeneğiyle bunu Predis'e bırakın.

RESP3 kullanabilir miyim?

Bağlantı URL'leri sayfasına göre Predis URL'de ?protocol=3 parametresini kabul eder ve bunu bağlantıya iletir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Redis öğrenme rehberi · Redis bağlantı URL ve connection pool · MULTI, EXEC ve WATCH · Redis pipelining · Lua script ve EVAL · PHP öğrenme rehberi