语法
一、页面缓存
某些动态生成的网页(如文章详情页、商品页面)可能生成成本较高,但在一定时间内访问内容不变。使用 Redis
缓存页面 HTML
数据,减少后台渲染压力。具体实现为: 在服务启动时,将定义的热点数据从数据库加载到 Redis
缓存中, 提前预加载热点数据到缓存。数据读取时,优先从缓存中读取,如果未命中则从数据库查询,并缓存到 Redis
。设置 随机过期时间(TTL
) 避免缓存集中过期,造成缓存雪崩。查询无效数据时,将无效结果(如 null
)缓存到 Redis
,设置短 TTL
避免频繁查询数据库, 造成缓存穿透。在高并发场景下,当缓存未命中时使用分布式锁限制同一时间只有一个请求可以加载数据并更新缓存,防止高并发场景下多个请求同时访问数据库,导致缓存击穿。在更新数据时,优先更新数据库,再使缓存失效。使用延迟双删策略确保并发场景下数据一致性。
# 缓存渲染后的页面数据
SETEX page:article:456 300 '<html>...article content...</html>'
GET page:article:456
# 定期更新或自动失效(配合定时任务)。
二、热点数据缓存
网站或应用中存在一些频繁访问的数据,例如用户信息、商品详情等。将这些数据缓存到 Redis
中,减少对后端数据库的访问压力,提升系统响应速度。具体实现为: 在服务启动时,将定义的热点数据从数据库加载到 Redis
缓存中, 提前预加载热点数据到缓存。数据读取时,优先从缓存中读取,如果未命中则从数据库查询,并缓存到 Redis
。设置 随机过期时间(TTL
) 避免缓存集中过期,造成缓存雪崩。查询无效数据时,将无效结果(如 null
)缓存到 Redis
,设置短 TTL
避免频繁查询数据库, 造成缓存穿透。在高并发场景下,当缓存未命中时使用分布式锁限制同一时间只有一个请求可以加载数据并更新缓存,防止高并发场景下多个请求同时访问数据库,导致缓存击穿。在更新数据时,优先更新数据库,再使缓存失效。使用延迟双删策略确保并发场景下数据一致性。
其中,分布式锁增加指数退避重试机制,指数退避(Exponential Backoff
)是指: 每次重试的间隔时间按照指数增长(如 2^n
),逐步增加, 增加一个随机抖动(jitter
),防止大量请求在相同时间再次争抢锁。计算公式为: Math.min(baseDelay * Math.pow(2, attempts), maxDelay) + Math.random() * jitterRange
。baseDelay
为基础延迟时间, attempts
为当前重试的次数,使用 baseDelay * 2^attempts
动态调整重试间隔时间。maxDelay
为最大延迟时间,限制最大间隔时间为 maxDelay
,防止过长的延迟。randomJitter
为随机抖动时间, 每次重试增加随机抖动 randomJitter
,避免并发请求同时重试导致锁竞争。通过 引入指数退避策略,可以显著降低高并发场景下锁竞争的冲突概率,同时最大限度提高获取锁的成功率,增强系统的稳定性和可靠性。
const redis = require('redis');
const mysql = require('mysql2/promise');
const express = require('express');
const app = express();
app.use(express.json());
// 配置 Redis 客户端
const redisClient = redis.createClient();
redisClient.connect().catch(console.error);
// 配置 MySQL 数据库
const db = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test_db',
});
// 设置缓存(避免缓存雪崩)
const setCacheWithRandomTTL = async (key, data) => {
const ttl = 3600 + Math.floor(Math.random() * 300); // 1 小时 ± 5 分钟
await redisClient.setEx(key, ttl, JSON.stringify(data));
};
// 使用分布式锁(防止缓存击穿,增加指数退避机制)
const setWithLock = async (key, fetchData, maxRetries = 5, baseDelay = 100) => {
const lockKey = `lock:${key}`;
const requestId = `${Date.now()}:${Math.random()}`; // 唯一请求标识
let attempts = 0;
while (attempts < maxRetries) {
try {
// 尝试获取锁
const lock = await redisClient.set(lockKey, requestId, { NX: true, EX: 5 });
if (lock) {
// 成功获取锁
console.log(`Lock acquired for key: ${key}`);
const data = await fetchData();
if (data) {
await setCacheWithRandomTTL(key, data); // 使用随机 TTL 设置缓存
}
return data;
}
} catch (err) {
console.error('Error while trying to acquire lock:', err);
}
// 计算指数退避时间
attempts++;
const delay = Math.min(baseDelay * 2 ** attempts, 5000); // 限制最大退避时间为 5 秒
const jitter = Math.random() * 100; // 随机抖动
console.log(`Retrying to acquire lock... Attempt ${attempts}/${maxRetries}. Delay: ${delay + jitter}ms`);
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
console.warn(`Failed to acquire lock for key: ${key} after ${maxRetries} retries.`);
return null; // 超过最大重试次数,返回 null
};
// 获取数据(解决缓存穿透和缓存击穿)
const getData = async (key) => {
try {
const cachedData = await redisClient.get(key);
if (cachedData) {
console.log('Cache Hit');
return JSON.parse(cachedData);
}
console.log('Cache Miss');
// 使用分布式锁加载数据(防止缓存击穿)
const data = await setWithLock(key, async () => {
const [rows] = await db.query('SELECT * FROM items WHERE id = ?', [key]);
return rows.length > 0 ? rows[0] : null;
});
if (data === null) {
// 避免缓存穿透:缓存 null 数据并设置短 TTL
await redisClient.setEx(key, 60, JSON.stringify(null));
}
return data;
} catch (error) {
console.error('Error in getData:', error);
return null;
}
};
// 更新数据(解决缓存与数据库一致性)
const updateData = async (key, newData) => {
try {
await db.query('UPDATE items SET value = ? WHERE id = ?', [newData.value, key]);
await redisClient.del(key); // 立即删除缓存
setTimeout(async () => {
await redisClient.del(key); // 延迟双删
}, 500);
} catch (error) {
console.error('Error in updateData:', error);
}
};
// 提前预热热点数据
const preloadHotKeys = async () => {
console.log('Preloading hotspot keys...');
const hotKeys = [1, 2, 3]; // 假设 1, 2, 3 是热点数据的 ID
for (const key of hotKeys) {
const [rows] = await db.query('SELECT * FROM items WHERE id = ?', [key]);
if (rows.length > 0) {
await setCacheWithRandomTTL(key.toString(), rows[0]);
console.log(`Preloaded key ${key} into cache.`);
}
}
};
// 路由配置
app.get('/items/:id', async (req, res) => {
const id = req.params.id;
const data = await getData(id);
res.json(data || { message: 'No data found' });
});
app.post('/items/:id', async (req, res) => {
const id = req.params.id;
const newData = req.body;
await updateData(id, newData);
res.json({ message: 'Data updated successfully' });
});
// 启动服务器时预热热点数据
app.listen(3000, async () => {
console.log('Server running on http://localhost:3000');
await preloadHotKeys(); // 热点数据预热
});