System Design

Cache invalidation strategiyalari

Cache’ning eng qiyin muammosi: qachon yangilash yoki o’chirish?

E-Tag (Entity Tag)

E-Tag — contentning “fingerprint”i (hash).

Client-side caching

GET /api/users/123

Response:
200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=3600
{ "name": "Ali", "email": "..." }

Keyingi request:

GET /api/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Response:
304 Not Modified (content o'zgarmagan)

Bandwidth tejash
Server’ga request kerak (latency)

Implementation

const crypto = require('crypto');

app.get('/api/users/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);
  const etag = crypto
    .createHash('md5')
    .update(JSON.stringify(user))
    .digest('hex');
  
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }
  
  res.setHeader('ETag', etag);
  res.json(user);
});

TTL (Time-To-Live)

TTL — cache qancha vaqt yashaydi.

Adaptive TTL

// Ko'p o'zgaradigan data - qisqa TTL
cache.set('trending', data, { ttl: 60 }); // 1 min

// O'zgarmas data - uzoq TTL
cache.set('user:profile', data, { ttl: 3600 }); // 1 hour

// Static assets - juda uzoq
cache.set('logo.png', data, { ttl: 86400 * 365 }); // 1 year

Probabilistic Early Expiration

async function get(key, ttl) {
  const { value, expiry } = await cache.get(key);
  
  if (!value) {
    // Cache miss
    return await fetchFromDB();
  }
  
  // Probabilistic refresh
  const timeLeft = expiry - Date.now();
  const delta = Math.random() * timeLeft;
  
  if (delta < 1000) { // 1 sekund qolganda
    // Background refresh
    refreshInBackground(key);
  }
  
  return value;
}

Foyda: Cache stampede oldini olish.

TTL Jitter

Muammo: Thundering herd

1000 users request → Cache miss (same time)
→ 1000 DB queries! 

Yechim: Random TTL

// Bad: Barcha bir vaqtda expire
cache.set('popular-post', data, { ttl: 3600 });

// Good: Random jitter
const baseTime = 3600;
const jitter = Math.random() * 600; // ±5 min
cache.set('popular-post', data, { ttl: baseTime + jitter });

Cache Stampede Prevention

1. Cache Locking

const locks = new Map();

async function get(key) {
  const cached = await cache.get(key);
  if (cached) return cached;
  
  // Check if someone is already fetching
  if (locks.has(key)) {
    await locks.get(key); // Wait
    return cache.get(key);
  }
  
  // Acquire lock
  const promise = fetchFromDB(key);
  locks.set(key, promise);
  
  try {
    const data = await promise;
    await cache.set(key, data);
    return data;
  } finally {
    locks.delete(key);
  }
}

2. Stale-While-Revalidate

// Serve stale, refresh background
async function get(key) {
  const { value, isStale } = await cache.get(key);
  
  if (value) {
    if (isStale) {
      // Return stale, refresh async
      refreshInBackground(key);
    }
    return value;
  }
  
  return await fetchFromDB(key);
}

Cache Warming

Cold cache — birinchi requestlar sekin.

// Startup'da cache'ni to'ldirish
async function warmCache() {
  const popularPosts = await db.query('SELECT * FROM posts ORDER BY views DESC LIMIT 100');
  
  for (const post of popularPosts) {
    await cache.set(`post:${post.id}`, post);
  }
  
  console.log('Cache warmed');
}

// Server start
await warmCache();
app.listen(3000);

Event-driven Invalidation

Database o’zgarganda cache invalidate qilish:

// Database trigger yoki message queue
db.on('user.updated', async (userId) => {
  await cache.del(`user:${userId}`);
  await cache.del(`user:${userId}:posts`);
});

// Update function
async function updateUser(id, data) {
  await db.update(id, data);
  await cache.del(`user:${id}`); // Invalidate
}

Cache Tagging

// Tag-based invalidation
await cache.set('post:1', data, { tags: ['user:123', 'category:tech'] });
await cache.set('post:2', data, { tags: ['user:123', 'category:design'] });

// Invalidate by tag
await cache.invalidateByTag('user:123'); // Ikkala post ham o'chiriladi

Multi-layer Cache

// L1: Memory (fast, small)
// L2: Redis (medium, large)
// L3: Database (slow, huge)

async function get(key) {
  // L1
  if (memoryCache.has(key)) return memoryCache.get(key);
  
  // L2
  const redisData = await redis.get(key);
  if (redisData) {
    memoryCache.set(key, redisData);
    return redisData;
  }
  
  // L3
  const dbData = await db.query(key);
  redis.set(key, dbData, { ttl: 3600 });
  memoryCache.set(key, dbData);
  return dbData;
}

Best Practices

1. Monitor staleness

const age = Date.now() - cache.getTimestamp(key);
if (age > 60000) {
  console.warn('Cache older than 1 min');
}

2. Version keys

const version = 'v2';
cache.set(`${version}:user:${id}`, data);
// Version o'zgarsa, eski cache'lar ignore

3. Namespace by deployment

const namespace = process.env.DEPLOYMENT_ID;
cache.set(`${namespace}:user:${id}`, data);
// Yangi deploy → yangi namespace

4. Graceful degradation

async function getUser(id) {
  try {
    return await cache.get(`user:${id}`);
  } catch (err) {
    console.error('Cache error, falling back to DB');
    return await db.getUser(id);
  }
}

Xulosa

E-Tag:

TTL:

Invalidation:

Keyingi dars: Distributed Cache (Redis, Memcached).