TL;DR
Laravel için geliştirdiğim Redis öncelikli roller ve izinler paketi scabarcas/laravel-permissions-redis. Bu paket, Spatie ile uyumlu bir API sunmakta (hasRole, hasPermissionTo, Blade direktifleri, middleware) ancak kullanıcı→roller→izinler eşlemesi her istekte veritabanından doldurulmak yerine Redis SET’lerinde saklanmaktadır.
Optimizasyon karşılaştırması spatie/laravel-permission ^7.2 ile yapıldı; 5 ısınma + her senaryo için 30 ölçüm çalışması, çalıştırmalar arasında GC sıfırlaması, predis istemcisi, SQLite + yerel Redis üzerinde Apple Silicon kullanıldı:
| Workload | Spatie p50 | Redis p50 | Speedup | DB queries reduced |
|---|---|---|---|---|
| 1 authorization-heavy request | 13.76 ms | 1.26 ms | 10.94x | 4 → 1 (75%) |
| 10 iterations | 138.87 ms | 13.01 ms | 10.68x | 40 → 10 (75%) |
| 50 iterations | 696.73 ms | 63.79 ms | 10.92x | 200 → 50 (75%) |
Hız artışı tutarlıdır – Redis aramaları neredeyse sabit zamanlıdır; Spatie’nin istekteki ilişki doldurması lineer ölçekte çalışır. Bench repo herkese açıktır isterseniz yeniden üretmek için: laravel-permissions-redis-benchmark.
Geri kalan kısım neden ve ne zaman kullanılmaması gerektiği üzerinedir.
Spatie ile ilgili sorun, önbellek değil – ilişki doldurma
Spatie ile ilgili sorun, önbellek değil – ilişki doldurma
Bir yüksek trafikli uygulamada spatie/laravel-permission kullandıysanız, muhtemelen yetkilendirme kontrollerinin görünenden daha maliyetli olduğunu fark etmişsinizdir. İşte hasPermissionTo() 27 kez, hasRole() 4 kez, ayrıca getAllPermissions() ve getRoleNames() çağıran tek bir istekten bir sorgu kaydı:
-- 1. User lookup
select * from "users" where "users"."id" = 1
-- 2. Roles via pivot
select "roles".*, "model_has_roles"."model_id" as "pivot_model_id", ...
from "roles" inner join "model_has_roles" on "roles"."id" = "model_has_roles"."role_id"
where "model_has_roles"."model_id" = 1 and "model_has_roles"."model_type" = 'App\Models\User'
-- 3. Direct permissions via pivot
select "permissions".*, "model_has_permissions"."model_id" as "pivot_model_id", ...
from "permissions" inner join "model_has_permissions" on "permissions"."id" = "model_has_permissions"."permission_id"
where "model_has_permissions"."model_id" = 1 and "model_has_permissions"."model_type" = 'App\Models\User'
-- 4. Permissions via roles
select "permissions".*, "role_has_permissions"."role_id" as "pivot_role_id", ...
from "permissions" inner join "role_has_permissions" on "permissions"."id" = "role_has_permissions"."permission_id"
where "role_has_permissions"."role_id" in (1, 2)
Dört sorgu. Yetkilendirme ile ilgili her istek. 27 izin kontrolü yapmak bunları çarpmaz; çünkü Spatie kullanıcı ilişkilerini 1 kez User::find() başına yükleyip, ardından PHP belleğinde üyelik kontrollerini çalıştırır.
Fakat ilk kez profilleme yaptığımda beni şaşırtan kısımsa: Spatie’nin izin önbelleği bu dört sorguyu hızlandırmaz. Bu önbellek, sistemde mevcut olan izin ve rol kaydını cache.permissions.cache kullanarak Laravel önbellek yüzeyi ile saklar. Kullanıcıya özgü pivot ilişkileri (model_has_roles, model_has_permissions, role_has_permissions) her seferinde User::find() ve ardından izin kontrolü çağırıldığında Eloquent tarafından yüklenir. Genel önbellek yalnızca permissions ve roles tablolarını yeniden okumaktan sizi kurtarır.
Bu, düşük trafikli uygulamalar için uygundur. Veritabanındaki darboğazı aştıktan sonra, bunu fark etmeyi durdurursunuz.
Ancak her middleware’de, her gate’de, her Blade direktifinde, her API uç noktasında yetkilendirme yapmaya başladığınızda bu rahatsız edici hale gelir.
Neden “Spatie + daha iyi önbellek” yerine Redis öncelikli tercih ettim
Neden “Spatie + daha iyi önbellek” yerine Redis öncelikli tercih ettim
Doğal bir sonraki adım, kullanıcının çözülmüş izinlerini önbelleğe almaktır, sadece genel kaydı değil. Bunu Spatie ile, her kullanıcı için getAllPermissions()‘ı önbelleğe alan özel bir dekoratör yazarak yapabilirsiniz – insanlar bunu yaptı. Ancak iki sorun ortaya çıkıyor:
Geçersiz kılma zor bir hal alıyor. Bir kullanıcıya izin verdiğinizde, o kullanıcının önbelleğini temizlemeniz gerekir. Bir rolün izinlerini değiştirdiğinizde, o rolü taşıyan her kullanıcının önbelleğini temizlemeniz gerekir. Bir izni sildiğinizde, daha geniş bir temizleme işlemi gerekir. Spatie’nin API’si, her atama değişikliğinin, aktif her kullanıcının bir sonraki isteği yolda 4 sorguya geri dönmesini zorunda bıraktığı
forgetCachedPermissions()ile tümünü siler – bu kabul edilebilir ama kullanıcıları sürekli olarak yeniden doldurmak sevinç verici değil.Veri yapısı, operasyonu desteklemiyor. Her ne kadar izinleri JSON dizisi olarak kullanıcı bazında önbelleğe alsanız da,
hasPermissionTo('posts.edit')kontrolüin_array()ile dizinin tekrar çözümlenmesi sırasında bir tarama işlemidir. Redis SET’leri bunu O(1) ileSISMEMBERaracılığıyla gerçekleştirir. Veriyi Redis’te tutuyorsanız, doğru işlemi de kullanmalısınız.
Bu nedenle, bir Spatie dekoratörü yerine ayrı bir trait yazdım. Redis’teki veri yapısı şöyle:
SET permissions:user:1:permissions {"posts.view", "posts.edit", "users.view", ...}
SET permissions:user:1:roles {"admin", "editor"}
HASH permissions:role:editor {permissions: ["..."], ...}
hasPermissionTo($perm), SISMEMBER permissions:user:{id}:permissions {$perm} haline gelir. hasRole($role) ise :roles için aynıdır. Wildcard kontroller (posts.*) SMEMBERS + fnmatch() kullanılarak PHP’de yapılır ve hâlâ sabit sorgulardır.
Önbellek ısınması, girişte (veya açıkça AuthorizationCacheManager::warmUser($userId) ile) gerçekleşir. Sonrasında, her yetkilendirme kontrolü bir Redis gidiş-dönüşü olur – kullanıcı sorgusu dışında hiçbir veritabanı sorgusu yoktur.
Sayılar, metodoloji ile birlikte
Sayılar, metodoloji ile birlikte
İşte bench sağlaması tek bir ekranda (kaynak):
class BenchmarkRunner
{
public function execute(int $userId, int $iterations, int $warmUpRuns = 3, int $measurementRuns = 10): array
{
foreach ($this->strategies as $strategy) {
// Flush Spatie cache once so both strategies start from the same baseline
app(PermissionRegistrar::class)->forgetCachedPermissions();
for ($w = 0; $w $warmUpRuns; $w++) {
$strategy->run($userId, self::PERMISSIONS, self::ROLES, $iterations);
}
$times = [];
for ($m = 0; $m $measurementRuns; $m++) {
DB::flushQueryLog();
gc_collect_cycles();
$start = microtime(true);
$strategy->run($userId, self::PERMISSIONS, self::ROLES, $iterations);
$times[] = (microtime(true) - $start) * 1000;
}
// Aggregate into p50, p95, p99, mean, stddev
}
}
}
Her “iterasyon” 27 hasPermissionTo() kontrolü + 4 hasRole() kontrolü + getAllPermissions() + getRoleNames() çalıştırır. Testlerde iterations=1, iterations=10, ve iterations=50 ölçülmüştür ve yetkilendirme çağrılarının bir istekte birden fazla kez yapıldığında maliyetlerin nasıl değiştiği gözlemlenmiştir (tipik olarak çoklu bölümleri kapsayan görünümler için).
Kendi testlerinizi çalıştırın:
git clone https://github.com/scabarcas17/laravel-permissions-redis-benchmark
cd laravel-permissions-redis-benchmark
composer install
php artisan migrate:fresh --seed --seeder=BenchmarkSeeder
php artisan bench:markdown --warm=5 --runs=30
Tam yüzdelik dağılım (aynı zamanda bench README’sinde):
### 10 İterasyon (27 izin kontrolü + 4 rol kontrolü + 2 koleksiyon çağrısı)
| Metric | Spatie | Redis | Delta |
|-------------------|----------------|---------------|------------------|
| DB Queries | 40 | 10 | 75% daha az |
| Median (p50) | 138.87 ms | 13.01 ms | 10.68x daha hızlı|
| p95 | 140.13 ms | 13.78 ms | — |
| p99 | 159.27 ms | 13.87 ms | — |
| Mean ± StdDev | 139.42 ± 3.86 | 13.11 ± 0.45 | — |
Bu sayılarla ilgili bazı dürüst notlar:
- Sabit durum, soğuk başlangıç değil. Isınma çalışmaları uygulama başlatmasını, Spatie’nin küresel kayıt önbelleği yüklemesini, Redis bağlantı kurulumu maliyetlerini amorti eder. Üretimde görülenleri ölçüyoruz, bir dağıtım sonrası ilk istek değil.
- SQLite + yerel Redis. Mutlak ms değerleri MySQL veya Postgres veya uzak Redis’te farklı olacaktır. Oran (~10x medyan) genel olarak geçerli olacaktır; sabit sayılar farklılık gösterebilir. Bu test, donanımınızda 30 saniye içerisinde yeniden üretilebilir.
- Tek kullanıcı. Eşzamanlı yük yok. Test, Redis bağlantıları için rekabet eden yüzlerce eşzamanlı isteği simüle etmemektedir — bu ayrı bir egzersizdir (k6, wrk).
- Henüz v4’e özgü özellikleri kapsamaz. Wildcard izinleri, grup geçersiz kılmaları, toplu rol kontrol API’si (
userHasAnyRole,userHasAllRoles) şu an için testte yok. Bunlar farkı daha fazla genişletir, ancak mevcut sayılar basit API ile sınırlıdır.
5 satırlık kurulum
5 satırlık kurulum
composer require scabarcas/laravel-permissions-redis
php artisan vendor:publish --tag=permissions-redis-config
php artisan migrate
Ardından kullanıcı modelinizde:
use Scabarcas\LaravelPermissionsRedis\Traits\HasRedisPermissions;
class User extends Authenticatable
{
use HasRedisPermissions;
}
Opsiyonel: AuthServiceProvider‘da veya bir dinleyicide giriş sırasında ısıtma:
Event::listen(Login::class, function (Login $event) {
app(AuthorizationCacheManager::class)->warmUser($event->user->id);
});
Bu kadar. Aynı hasRole, hasPermissionTo, assignRole, givePermissionTo, @role, @can, role: middleware’ları kullanabilirsiniz. Göç, Spatie’nin kullandığı permissions, roles, model_has_permissions, model_has_roles, role_has_permissions tablolarını ekler; dolayısıyla Spatie’den bir geçiş, esasen “trait’i değiştir ve warmAll‘ı çalıştır” demektir.
Bu durumu NE ZAMAN KULLANMAMALISINIZ
Bu durumu NE ZAMAN KULLANMAMALISINIZ
Tüm teklif birkaç durumda geçersiz hale gelir. Kendinize dürüst olun:
- Redis kullanmıyorsanız. Sadece izinler için Redis’i eklemek, 10 günlük kullanıcıya sahip tek sunucu SQLite kurulumu yapıyorsanız gereksizdir. Spatie kullanın.
- Yetkilendirmeniz yüzeysel ise. Eğer
@can('admin')sayfada iki kez yapıyor ve günde 1000 istek gönderiyorsanız, mutlak ms tasarrufu çok azdır ve geçiş maliyeti buna değmez. Spatie kullanın. - Önbellek yazar-şartlarını bağımsız bir soyutlama istiyorsanız. Bu paket sizi Redis ile bağlar. Eğer “Redis’i Memcached / DynamoDB / veritabanı önbelleği ile değiştirebiliriz” planındaysanız, bu doğru seçenek değildir – Spatie + özel bir dekoratör kullanın.
- Laravel 10’dasınız. Bu paket Laravel 11/12/13 ve PHP 8.3+’ya yöneliktir. Spatie daha eski sürümleri destekler.
Paket, tüm bunları README’sinde açıkça belirtmektedir – hedef “Spatie’yi değiştirmek” değil, yetkilendirme akışının önemli olduğu durumlarda net bir seçenek sunmaktır.
Nerede Bulabilirsiniz
Nerede Bulabilirsiniz
Geribildirim bekleniyor — özellikle zaten özel Spatie önbellek dekoratörleri yazmış olanlardan veya bunu farklı bir yük ile kıyaslayanlardan.
Kaynak: Orijinal Makale


