Databases Storage And Caching

Redis Overview

Redis is a networked data-structure server commonly used by PHP applications for caches, sessions, queues, rate limits, counters, coordination, and short-lived locks. It stores active data in memory for low-latency access and offers configurable persistence and replication options.

Redis is not automatically "just a cache," nor is it automatically a durable system of record. Its role, data type, expiry, persistence, memory policy, and failure behavior must be designed for each use case.

Choose Redis For A Specific Responsibility

Common PHP uses include:

  • caching database or API results;
  • sharing sessions across several PHP servers;
  • storing queue or stream work;
  • implementing rate-limit counters;
  • maintaining leaderboards or priority sets;
  • coordinating short critical sections;
  • publishing transient notifications;
  • storing short-lived idempotency markers.

The impact of failure differs. A missing cache entry can often be rebuilt. Lost session state logs users out. Lost queue work may drop customer actions. An unavailable lock can permit duplicate processing if code fails open.

Write the role and failure contract before choosing configuration.

Connect Through A Maintained PHP Client

PHP applications commonly use a framework abstraction, Predis, or the phpredis extension. Redis's current PHP client documentation provides a Predis guide while noting that Predis is a third-party client.

Keep connection configuration outside source code:

PHP example
<?php

declare(strict_types=1);

function redisConnectionConfig(array $environment): array
{
    return [
        'scheme' => $environment['REDIS_SCHEME'] ?? 'tcp',
        'host' => $environment['REDIS_HOST'] ?? '127.0.0.1',
        'port' => (int) ($environment['REDIS_PORT'] ?? 6379),
        'database' => (int) ($environment['REDIS_DATABASE'] ?? 0),
    ];
}

Use authentication and TLS according to the deployment. Do not expose Redis directly to the public internet. Apply network restrictions, least-privilege access where supported, secret management, and client timeouts.

Official reference: Redis PHP client guide.

Redis Offers Data Structures, Not Only Strings

Redis supports data types including strings, hashes, lists, sets, sorted sets, streams, geospatial indexes, and specialized probabilistic structures. Select the type whose commands match the operation.

Official reference: Redis data types.

Strings suit serialized cache values, counters, flags, and tokens. Commands such as increment operations can update numeric string values atomically.

Hashes group fields under one key, such as a small session or profile projection. They can avoid rewriting one large serialized document for each field change, though field-level expiry behavior requires checking current Redis capabilities and client support.

Lists preserve insertion order and can support simple queue-like operations. Production job systems need acknowledgement, retry, visibility, and failure handling beyond pushing and popping values.

Sets store unique unordered members, useful for membership and tags.

Sorted sets associate unique members with scores, useful for rankings, scheduled work, and priority ordering.

Streams provide an append-oriented event structure with consumer-group capabilities. They are more suitable than Pub/Sub when work needs tracking and acknowledgement.

Design Predictable Key Names

A key should identify ownership, entity, purpose, and schema version:

PHP example
<?php

declare(strict_types=1);

function userProfileCacheKey(string $environment, int $userId): string
{
    return "$environment:cache:user-profile:$userId:v2";
}

echo userProfileCacheKey('prod', 42), PHP_EOL;

// Prints:
// prod:cache:user-profile:42:v2

Include tenant ID for tenant-specific data. A missing tenant dimension can leak one customer's cached value to another.

Avoid embedding secrets or personal data in key names. Keys appear in diagnostics, monitoring, memory analysis, and administrative tools.

Key versioning helps deployments change serialized shapes. Old keys expire naturally instead of being decoded by incompatible code.

Use Expiry Deliberately

Most cache, idempotency, rate-limit, and lock keys need a TTL. Permanent keys consume memory until explicitly removed.

A cache-aside plan is:

  1. Read the key.
  2. On hit, validate and return the value.
  3. On miss, load authoritative data.
  4. Store it with a bounded TTL.
  5. Return the source value.
PHP example
<?php

declare(strict_types=1);

function cachePlan(bool $redisAvailable, bool $cacheHit): string
{
    if (!$redisAvailable) {
        return 'load authoritative source without Redis';
    }

    return $cacheHit
        ? 'return validated cached value'
        : 'load source, cache with TTL, return value';
}

TTL is an upper bound on ordinary staleness, not a guarantee that the key remains present. Redis may evict keys under its configured memory policy, operators may flush data, and failover can affect availability.

Add jitter to large groups of cache keys so they do not expire simultaneously and overload the source.

Distinguish Missing, False, And Invalid Values

Client return values differ. A cache miss may be represented by null, false, or another client-specific result. The cached value itself might validly encode false or zero.

Wrap the client behind an interface that returns an explicit hit indicator or uses a reliable sentinel. Validate decoded JSON and schema version. Corrupt or incompatible cache data should be treated as a miss and removed, not trusted because the key exists.

Negative caching can store "not found" briefly to protect a source from repeated misses. Use short TTLs and invalidate after creating the previously missing record.

Atomic Commands Prevent Read-Modify-Write Races

Do not fetch a counter into PHP, increment it, and write it back. Use an atomic Redis command:

INCR rate:user:42:minute:202606101230
EXPIRE rate:user:42:minute:202606101230 120

Several commands may need one atomic decision. Redis transactions with MULTI/EXEC, optimistic watching, server-side scripts, or functions can help, depending on the requirement. Pipelining reduces network round trips but does not by itself make commands atomic.

For rate limiting, command ordering matters. A crash between increment and expiry can leave a permanent key. Use an atomic script or an existing well-reviewed algorithm when correctness matters.

Redis Transactions Differ From SQL Transactions

Redis transactions queue commands and execute them in order, but they do not provide SQL-style rollback of commands that already execute. Command errors and optimistic conflicts have Redis-specific semantics.

Do not apply ACID database assumptions blindly. Read the current command and client documentation, and test the complete operation.

If business records require relational constraints, joins, and durable multi-row transactions, keep them in the database. Redis can coordinate or accelerate around that source.

Caching Requires Invalidation

Redis does not know when a MySQL or PostgreSQL row changes. Application writes must delete or replace dependent keys after the source transaction commits.

A short TTL limits stale exposure if deletion fails. Versioned keys, dependency tags implemented through sets, or an outbox-backed invalidation worker can help complex systems.

Do not clear all Redis data for one product update. Broad flushes destroy unrelated caches, sessions, queues, and rate-limit state when workloads share an instance.

Separate workloads through dedicated deployments or strong ownership boundaries when their risk profiles differ. Numbered Redis logical databases are not a complete security or resource-isolation mechanism.

Memory Is Finite

Redis memory use includes keys, values, data-structure overhead, replication buffers, client buffers, and fragmentation. Configure and monitor a memory limit and eviction policy suitable for the workload.

A cache may tolerate eviction. Sessions or queues may not. Mixing them under an all-keys eviction policy can remove critical state to make room for disposable cache entries.

Monitor memory usage, evictions, key count, large keys, command latency, blocked clients, and connection count. Avoid unbounded lists, sets, or hashes. Cap history, trim streams, expire temporary values, and partition oversized records.

Large keys make deletion, transfer, and failover more expensive. Model collections in bounded pieces rather than one enormous serialized value.

Persistence Has Explicit Tradeoffs

Redis supports configurable persistence approaches, including point-in-time snapshots and append-only logging, and can combine them. Each has recovery, disk, latency, and operational tradeoffs.

Official reference: Redis persistence.

Persistence does not automatically make every use safe as an authoritative store. Consider acknowledgement timing, replication, failover, backup, restore testing, and application semantics.

If Redis contains only rebuildable cache data, persistence may be unnecessary. If it stores sessions or queue state, data-loss tolerance and recovery need explicit decisions. Managed Redis services still require configuration review.

Replication And Cluster Change Client Behavior

Replication can improve availability and read capacity, while asynchronous replication can lose recent writes during failover. Redis Cluster partitions keys across nodes and imposes rules on multi-key operations: keys used together may need to share a hash slot.

Do not add cluster key tags casually without understanding distribution. A tag that places every key in one slot defeats sharding. Group only keys that genuinely require atomic multi-key operations.

Use a client and framework integration that understands the deployed topology. Test failover, reconnect, timeout, and retry behavior. A retry after an ambiguous write can duplicate non-idempotent work.

Sessions Need Security And Availability

Redis-backed sessions let multiple PHP servers share login state. Protect Redis from public access, encrypt network traffic where required, and restrict credentials.

Session values are sensitive. Keep them minimal, expire them, rotate session IDs during authentication changes, and avoid exposing them in diagnostics.

Decide what happens during Redis outage. Failing closed may log users out or reject requests. Falling back to local files can split session state across servers and create confusing security behavior. Do not improvise a fallback during an incident.

Queues Need Delivery Semantics

A plain list pop can lose a job if a worker removes it and crashes before completion. Reliable queue designs track reserved work, acknowledgements, retries, timeouts, dead-letter handling, and idempotency.

Use a maintained queue library or framework rather than inventing a protocol from a few Redis commands. Monitor queue depth, oldest-job age, processing duration, retries, failed jobs, and worker health.

Pub/Sub is transient: subscribers that are disconnected do not receive past publications. Use Streams or another durable messaging design when consumers must catch up.

Locks Need Tokens And Expiry

A basic lock acquisition uses a unique random token and an atomic set-if-absent with expiry. Release must delete the key only if the stored token still matches the owner.

Why? A worker may pause past the TTL. Another worker acquires the expired lock. The first worker resumes and must not delete the second worker's lock.

The critical operation may also need a fencing token checked by the protected resource. A Redis lock alone cannot always stop an expired owner from continuing work.

Official reference: Redis distributed locks.

Use a well-reviewed implementation and understand its failure assumptions. Never proceed without a lock when duplicate execution would corrupt data merely because Redis is unavailable.

Protect Redis Operationally

Apply:

  • private network placement;
  • authentication and scoped access controls;
  • TLS where required;
  • secret rotation;
  • command restrictions appropriate to the environment;
  • patching and supported versions;
  • resource limits;
  • backups where data requires them;
  • monitoring and alerting;
  • tested failover and restore procedures.

Avoid expensive key scans in production. Use incremental scan operations and observability tools instead of commands that block while enumerating an enormous keyspace.

Do not place untrusted user input directly into command names or unrestricted key patterns. Client libraries handle protocol encoding, but application authorization and key scoping remain necessary.

Test Failure Behavior

Tests should verify more than a cache hit:

  • cache miss and corrupt value;
  • Redis timeout or connection failure;
  • TTL expiry;
  • explicit invalidation after source commit;
  • duplicate queue delivery;
  • worker crash before acknowledgement;
  • session-store outage;
  • rate-limit operation atomicity;
  • lock expiry and token-safe release;
  • failover reconnect and ambiguous writes;
  • memory eviction assumptions.

Use a real Redis integration environment for command semantics. An in-memory PHP fake may not model expiry, scripts, transactions, cluster slots, or failover.

Review Checklist

Before using Redis, answer:

  • What exact responsibility does Redis have?
  • Is the data authoritative, rebuildable, or transient?
  • Which data type and commands match the operation?
  • What are the key namespace and tenant dimensions?
  • Which keys expire, and why?
  • What memory and eviction policy applies?
  • What persistence and replication guarantee is required?
  • What happens on timeout, failover, or complete outage?
  • Are queues acknowledged and idempotent?
  • Are locks token-protected and failure-safe?
  • Can the workload be monitored and restored?

What You Should Be Able To Do

After this lesson, you should be able to choose Redis for a specific role, select an appropriate data type, design namespaced and versioned keys, use expiry, and distinguish pipelines from atomic operations.

You should also understand memory eviction, persistence, replication, sessions, queue acknowledgements, and token-safe locks. Most importantly, you should be able to state what happens when Redis is missing, stale, full, slow, or unavailable instead of treating low latency as the complete design.

Practice

Practice: Design Redis Cache Behaviour

Write a small PHP example that models how a user profile cache should behave with Redis.

Requirements

  • Build a predictable Redis key for a user profile.
  • Include a TTL value.
  • Model cache hit and cache miss decisions.
  • Identify the database as the source of truth.
  • Include a note about what happens if Redis is unavailable.
Show solution

This solution models the behaviour without requiring a Redis server.

PHP example
<?php

declare(strict_types=1);

function userProfileCacheKey(int $userId): string
{
    return 'cache:user_profile:' . $userId;
}

function userProfileCachePlan(int $userId, bool $cacheHit, bool $redisAvailable): array
{
    if (!$redisAvailable) {
        return [
            'key' => userProfileCacheKey($userId),
            'source' => 'database',
            'action' => 'skip Redis and load from the database',
            'ttl_seconds' => null,
        ];
    }

    if ($cacheHit) {
        return [
            'key' => userProfileCacheKey($userId),
            'source' => 'redis',
            'action' => 'return cached profile',
            'ttl_seconds' => 300,
        ];
    }

    return [
        'key' => userProfileCacheKey($userId),
        'source' => 'database',
        'action' => 'load profile, store in Redis, then return it',
        'ttl_seconds' => 300,
    ];
}

$examples = [
    userProfileCachePlan(42, true, true),
    userProfileCachePlan(42, false, true),
    userProfileCachePlan(42, false, false),
];

foreach ($examples as $example) {
    echo json_encode($example, JSON_THROW_ON_ERROR) . PHP_EOL;
}

// Prints:
// {"key":"cache:user_profile:42","source":"redis","action":"return cached profile","ttl_seconds":300}
// {"key":"cache:user_profile:42","source":"database","action":"load profile, store in Redis, then return it","ttl_seconds":300}
// {"key":"cache:user_profile:42","source":"database","action":"skip Redis and load from the database","ttl_seconds":null}

For a cache, falling back to the database can be acceptable. For Redis-backed sessions, queues, or locks, Redis being unavailable may need to be treated as an outage instead.

Choose Redis Data Types

Choose a Redis data type for a page cache, unique active-feature flags, a leaderboard, an acknowledged event log, and a rate-limit counter.

For each, name the important commands or behavior, expiry policy, maximum-growth control, and authoritative source.

Show solution

Use a string with TTL for the page cache; a set for unique flags; a sorted set for leaderboard members and scores; a Stream with consumer acknowledgements and trimming for the event log; and an atomic numeric string operation with expiry for the counter.

The database or application configuration remains authoritative where applicable. Cap or trim growing structures, expire temporary keys, and use an atomic operation or script when increment and first-time expiry must be one decision.

Design Redis Failure Policies

Define separate failure behavior for Redis used as a product cache, session store, queue, and distributed lock. Include timeout handling, fallback, monitoring, and which cases must fail closed rather than continue.

Show solution

The product cache can bypass Redis and use the database with load protection. The session store normally treats outage as authentication/session unavailability; silently switching to local files would split state. The queue stops accepting or records durable alternative work according to its contract rather than dropping jobs. Lock-dependent work must fail closed when duplicate execution can corrupt state.

All paths use short timeouts, structured error metrics, and alerts. Recovery tests verify reconnect and failover behavior, and critical writes use stable IDs so ambiguous retries do not duplicate work.