前端工程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 + 延迟删除是最稳妥的方案,配合重试机制保证最终一致性。
---
*下一篇文章将介绍容器化与部署实战。*