缓存一致性问题
- 产生原因:在高并发环境下,多个请求可能同时对缓存数据进行读写操作。当数据发生变化时,如果没有合适的同步机制,不同请求读取到的缓存数据可能不一致。例如,一个请求更新了数据库中的数据,但还未来得及更新缓存,其他请求仍然从缓存中读取到旧数据。
- 优化方案:
- 读写锁机制:
- 实现思路:在Node.js中可以使用第三方库如
rwlock
来实现读写锁。当有写操作时,获取写锁,此时其他读写操作都被阻塞,确保写操作完成后,缓存数据与数据库数据一致。读操作时获取读锁,允许多个读操作同时进行,但有写操作时,读操作会被阻塞。示例代码如下:
const RWLock = require('rwlock');
const rwlock = new RWLock();
// 写操作
function writeData(newData) {
return new Promise((resolve, reject) => {
rwlock.writeLock((release) => {
// 更新数据库和缓存
// 假设这里有更新数据库和缓存的逻辑
release();
resolve();
});
});
}
// 读操作
function readData() {
return new Promise((resolve, reject) => {
rwlock.readLock((release) => {
// 从缓存读取数据
// 假设这里有从缓存读取数据的逻辑
release();
resolve();
});
});
}
- **使用发布 - 订阅模式**:
- **实现思路**:当数据更新时,发布一个事件,所有订阅了该事件的模块收到通知后更新缓存。在Node.js中可以使用内置的`EventEmitter`模块来实现。示例代码如下:
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 订阅事件
emitter.on('dataUpdate', () => {
// 更新缓存的逻辑
});
// 数据更新时发布事件
function updateData(newData) {
// 更新数据库
// 发布事件通知缓存更新
emitter.emit('dataUpdate');
}
缓存击穿问题
- 产生原因:缓存击穿指的是在高并发情况下,一个热点缓存数据过期的瞬间,大量请求同时绕过缓存直接访问数据库,给数据库带来巨大压力。例如,一个商品详情页的缓存设置了较短的过期时间,过期时恰好大量用户同时访问该商品详情,这些请求都会直接去数据库查询。
- 优化方案:
- 互斥锁(Mutex):
- 实现思路:在Node.js中可以使用
redis
的SETNX
(SET if Not eXists)命令来实现互斥锁。当缓存过期时,只有一个请求能获取到锁,该请求去数据库加载数据并更新缓存,其他请求等待锁释放后从缓存获取数据。示例代码如下:
const redis = require('redis');
const client = redis.createClient();
async function getData(key) {
let data = await new Promise((resolve, reject) => {
client.get(key, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
if (!data) {
const lockKey = `lock:${key}`;
const acquireLock = await new Promise((resolve, reject) => {
client.setnx(lockKey, 1, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
if (acquireLock) {
try {
// 从数据库获取数据
data = await getFromDatabase(key);
// 更新缓存
await new Promise((resolve, reject) => {
client.set(key, data, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
} finally {
// 释放锁
await new Promise((resolve, reject) => {
client.del(lockKey, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
}
} else {
// 等待一段时间后重试获取数据
await new Promise((resolve) => setTimeout(resolve, 100));
return getData(key);
}
}
return data;
}
function getFromDatabase(key) {
// 从数据库获取数据的逻辑
return Promise.resolve('data from database');
}
- **热点数据永不过期**:
- **实现思路**:对热点数据设置一个很长的过期时间,或者不设置过期时间。同时在系统中另外安排一个定时任务,定期更新这些热点数据的缓存,确保数据的实时性。在Node.js中可以使用`setInterval`来实现定时任务。示例代码如下:
const redis = require('redis');
const client = redis.createClient();
// 加载热点数据到缓存
async function loadHotData() {
const data = await getFromDatabase();
await new Promise((resolve, reject) => {
client.set('hotDataKey', data, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
}
// 定时更新热点数据
setInterval(async () => {
await loadHotData();
}, 60 * 1000); // 每60秒更新一次
function getFromDatabase() {
// 从数据库获取数据的逻辑
return Promise.resolve('data from database');
}