前端工程Technical Deep Dive

数据库与缓存:Redis 实战与数据一致性

发布时间2026/03/29
分类前端工程
预计阅读10 分钟
作者吴长龙
*

数据库与缓存:Redis 实战与数据一致性

01.内容

# 数据库与缓存:Redis 实战与数据一致性

缓存是提升应用性能的核心手段,但引入缓存也带来了数据一致性的挑战。本文从 Redis 基础到缓存策略,再到分布式锁和延迟队列,系统介绍数据库与缓存的协同工作方式。

02.一、Redis 基础

1.1 数据结构

javascript snippetjavascript
const redis = require('ioredis');
const client = new redis({
  host: 'localhost',
  port: 6379,
  password: process.env.REDIS_PASSWORD
});

// String
await client.set('user:123', JSON.stringify({ name: '张三' }));
await client.get('user:123');
await client.setex('token:abc', 3600, 'valid_token');  // 1小时过期

// Hash(适合对象)
await client.hset('user:123', 'name', '张三');
await client.hset('user:123', 'email', 'zhangsan@example.com');
await client.hgetall('user:123');

// List(队列)
await client.lpush('queue:tasks', JSON.stringify({ type: 'email', to: 'test@example.com' }));
await client.brpop('queue:tasks', 10);

// Set(去重、标签)
await client.sadd('user:123:tags', 'vip', 'premium', 'active');
await client.smembers('user:123:tags');

// Sorted Set(排行榜)
await client.zadd('leaderboard', 100, 'user:1');
await client.zadd('leaderboard', 200, 'user:2');
await client.zrevrange('leaderboard', 0, 9);

1.2 过期策略

javascript snippetjavascript
// 设置过期时间
await client.expire('token:abc', 3600);
await client.setex('key', 3600, 'value');  // 一步到位

// 查看剩余时间
const ttl = await client.ttl('token:abc');  // -1 永不过期,-2 不存在

1.3 事务与管道

javascript snippetjavascript
// 管道(批量发送,减少 RTT)
const pipeline = client.pipeline();
pipeline.set('key1', 'value1');
pipeline.get('key1');
pipeline.hset('hash1', 'f1', 'v1');
await pipeline.exec();

// Lua 脚本(原子操作)
const script = `
  local current = redis.call('GET', KEYS[1])
  if current == false then current = 0 end
  local new = tonumber(current) + tonumber(ARGV[1])
  redis.call('SET', KEYS[1], new)
  return new
`;
const result = await client.eval(script, 1, 'counter', 10);

03.二、缓存策略

2.1 Cache-Aside(旁路缓存)

javascript snippetjavascript
// 读流程
async function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  
  const user = await db.users.find(id);
  if (user) {
    await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
  }
  return user;
}

// 写流程:先数据库,后删除缓存
async function updateUser(id, data) {
  await db.users.update(id, data);
  await redis.del(`user:${id}`);  // 删除而非更新
}

2.2 缓存问题处理

javascript snippetjavascript
// 1. 缓存穿透:缓存空值
if (!product) {
  await redis.setex(cacheKey, 60, '');  // 短时间缓存空值
}

// 2. 缓存击穿:分布式锁
async function getProductWithLock(id) {
  const cached = await redis.get(`product:${id}`);
  if (cached) return JSON.parse(cached);
  
  const lock = await client.set(`lock:product:${id}`, '1', 'EX', 10, 'NX');
  if (lock) {
    try {
      const product = await db.products.find(id);
      if (product) await redis.setex(`product:${id}`, 3600, JSON.stringify(product));
      return product;
    } finally {
      await client.del(`lock:product:${id}`);
    }
  }
  await new Promise(r => setTimeout(r, 50));
  return getProductWithLock(id);
}

// 3. 缓存雪崩:随机过期时间
const randomExpiry = Math.floor(Math.random() * 3600) + 3600;
await redis.setex(cacheKey, randomExpiry, value);

04.三、分布式锁

javascript snippetjavascript
// 基础分布式锁
async function acquireLock(key, ttl = 10) {
  return await client.set(`lock:${key}`, process.pid, 'EX', ttl, 'NX');
}

async function releaseLock(key) {
  const script = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    end
    return 0
  `;
  return await client.eval(script, 1, `lock:${key}`, process.pid);
}

05.四、延迟队列

javascript snippetjavascript
// 生产者:加入延迟队列
async function scheduleTask(task, delay) {
  const executeAt = Date.now() + delay;
  await client.zadd('delay:queue', executeAt, JSON.stringify(task));
}

// 消费者
async function startConsumer() {
  while (true) {
    const now = Date.now();
    const tasks = await client.zrangebyscore('delay:queue', 0, now, 'LIMIT', 0, 10);
    
    if (tasks.length > 0) {
      await client.zrem('delay:queue', ...tasks);
      for (const task of tasks) {
        await processTask(JSON.parse(task));
      }
    }
    await new Promise(r => setTimeout(r, 1000));
  }
}

06.五、数据一致性方案

模式适用场景优点缺点
Cache-Aside读多写少简单,一致性好控制首次查询慢
Write-Through写多读多读缓存必命中写延迟增加
Write-Behind高并发写入写入性能高可能丢数据

最佳实践:Cache-Aside + 延迟删除是最稳妥的方案,配合重试机制保证最终一致性。

---

*下一篇文章将介绍容器化与部署实战。*