随着前端技术的普及和前后端分离的盛行,前端对缓存的需求也越来越高。利用 Redis 缓存可以显著提高网站的性能,降低服务器负荷,但是也需要注意它所带来的一些问题,以及如何做出优化。
问题
缓存雪崩
缓存雪崩是指在 Redis 缓存中,由于缓存键的过期时间相同或接近,导致缓存同时失效,请求都落在了数据库上,造成数据库短时间内瞬时并发访问压力太大,从而导致数据库宕机的现象。
// www.javascriptcn.com code example
// 示例代码
// 设置缓存过期时间是1小时
redis.set('user', { name: 'Tom', age: 20 }, 'EX', 3600);
// 多处读取 user 缓存
async function getUser() {
let user = await redis.get('user');
if (!user) {
// 缓存失效,从数据库获取
user = await getUserFromDB();
// 写入缓存
redis.set('user', user, 'EX', 3600);
}
return user;
}以上示例代码中,由于缓存过期时间一致,如果同时失效,则在从数据库获取数据时会同时发起大量请求,导致数据库可能会宕机,从而引发缓存雪崩。
缓存穿透
缓存穿透是指在 Redis 缓存中,由于恶意攻击或者业务逻辑bug,在缓存和数据库中均不存在某个键,这时大量请求都会落在数据库上,造成数据库短时间内瞬时并发访问压力太大,从而导致数据库宕机的现象。
// www.javascriptcn.com code example
// 示例代码
// 防止缓存穿透
async function getUserById(id) {
let user = await redis.get('user:' + id);
if (!user) {
user = await getUserFromDBById(id);
if (!user) {
// 写入空缓存
redis.set('user:' + id, '', 'EX', 3600);
return null;
}
redis.set('user:' + id, user, 'EX', 3600);
}
return user;
}为了防止缓存穿透,通常会在 Redis 缓存中把不存在的键写入一个空值,这样下次请求过来时就只会落在 Redis 缓存中。但是如果攻击者频繁的请求数量太大,仍然会对数据库造成压力。
缓存击穿
缓存击穿是指在 Redis 缓存中,由于一个热点数据缓存过期或者业务逻辑高频访问,而导致这个键的大量请求都会落在数据库上,造成数据库短时间内瞬时并发访问压力太大,从而导致数据库宕机的现象。
// www.javascriptcn.com code example
// 示例代码
// 防止缓存击穿
async function getUserById(id) {
let user = await redis.get('user:' + id);
if (!user) {
// 设置缓存过期时间是1小时,但是为了防止缓存击穿,加上了互斥锁
if (await redis.set('mutex:user:' + id, '1', 'NX', 'EX', 10)) {
user = await getUserFromDBById(id);
if (!user) {
// 写入空缓存
redis.set('user:' + id, '', 'EX', 3600);
return null;
}
redis.set('user:' + id, user, 'EX', 3600);
// 释放锁
redis.del('mutex:user:' + id);
} else {
// 如果没有获取到锁,说明其他线程正在从数据库中获取数据,等待一段时间再次尝试即可
await wait(50);
return await getUserById(id);
}
}
return user;
}为了防止缓存击穿,通常会在 Redis 缓存中加入互斥锁。当一个请求到来时,如果获取到了锁,那么它就可以从数据库中获取数据,然后写入缓存,并释放锁。如果一个请求到来时没有获取到锁,则等待一段时间(比如50ms),再次尝试获取缓存即可。
优化方案
缓存雪崩优化
为了避免缓存雪崩的发生,可以采取以下几种优化方案:
- 不同缓存键设置不同的过期时间,避免同时失效;
- 缓存失效时不是立即从数据库中获取数据,在某个随机的时间点再去获取数据;
- 对于实时性要求不高的数据,可以在高峰期对其进行预热,提前将其写入缓存中。
// www.javascriptcn.com code example
// 示例代码
// 不同缓存键设置不同的过期时间
redis.set('user:1001', { name: 'Tom', age: 20 }, 'EX', 3600);
redis.set('user:1002', { name: 'Jerry', age: 22 }, 'EX', 1800);
redis.set('user:1003', { name: 'Jenny', age: 25 }, 'EX', 7200);
// 缓存失效后再获取数据,增加随机时间等待
async function getUser() {
let user = await redis.get('user');
if (!user) {
// 等待 1~5 秒再次尝试获取缓存
const delay = Math.floor(Math.random() * (5 - 1)) + 1;
await wait(delay * 1000);
user = await getUserFromDB();
// 写入缓存
redis.set('user', user, 'EX', 3600);
}
return user;
}
// 预热数据
async function preloadUsers() {
const users = await getUsersFromDB();
for (let user of users) {
redis.set('user:' + user.id, user, 'EX', 3600);
}
}缓存穿透优化
为了避免缓存穿透的发生,可以采取以下几种优化方案:
- 对于不存在的键,写入一个空值或一个特殊标识符,表示该键不存在,这样下次请求过来时就只会落在 Redis 缓存中,避免请求数据库;
- 过滤掉异常或者不存在的请求;
- 使用布隆过滤器,在 Redis 缓存和数据库之间做一层过滤,过滤掉一些不存在于数据库中的请求。
// www.javascriptcn.com code example
// 示例代码
// 写入空值缓存
async function getUserById(id) {
let user = await redis.get('user:' + id);
if (!user) {
// 写入空缓存
redis.set('user:' + id, '', 'EX', 3600);
return null;
}
return user;
}
// 过滤掉异常请求
async function getUserById(id) {
if (!isValidId(id)) {
return null;
}
let user = await redis.get('user:' + id);
if (!user) {
user = await getUserFromDBById(id);
if (!user) {
// 写入空缓存
redis.set('user:' + id, '', 'EX', 3600);
return null;
}
redis.set('user:' + id, user, 'EX', 3600);
}
return user;
}
// 使用布隆过滤器,过滤掉不存在于数据库中的请求
const RedisBloom = require('redis-bloom');
const redisBloom = new RedisBloom({ host: 'localhost', port: 6379 });
async function getUserByIdWithBloomFilter(id) {
if (await redisBloom.exists('user:id:bloom', id)) {
return null;
}
let user = await redis.get('user:' + id);
if (!user) {
user = await getUserFromDBById(id);
if (!user) {
await redisBloom.add('user:id:bloom', id);
return null;
}
redis.set('user:' + id, user, 'EX', 3600);
}
return user;
}缓存击穿优化
为了避免缓存击穿的发生,可以采取以下几种优化方案:
- 加入互斥锁,避免缓存并发访问;
- 利用 Redis 的自带机制,通过批量获取数据来优化查询。
// www.javascriptcn.com code example
// 示例代码
// 加入互斥锁
async function getUserById(id) {
let user = await redis.get('user:' + id);
if (!user) {
// 设置缓存过期时间是1小时,但是为了防止缓存击穿,加上了互斥锁
if (await redis.set('mutex:user:' + id, '1', 'NX', 'EX', 10)) {
user = await getUserFromDBById(id);
if (!user) {
// 写入空缓存
redis.set('user:' + id, '', 'EX', 3600);
return null;
}
redis.set('user:' + id, user, 'EX', 3600);
// 释放锁
redis.del('mutex:user:' + id);
} else {
// 如果没有获取到锁,说明其他线程正在从数据库中获取数据,等待一段时间再次尝试即可
await wait(50);
return await getUserById(id);
}
}
return user;
}
// 批量获取数据
async function getUsersByIds(ids) {
// 批量从缓存中获取数据
const cacheResult = await redis.mget(...ids.map(id => 'user:' + id));
// 过滤出不存在的键
const missedIds = [];
const missedResults = [];
cacheResult.forEach((result, index) => {
if (!result) {
missedIds.push(ids[index]);
} else {
missedResults.push({
id: ids[index],
result: JSON.parse(result),
});
}
});
// 过滤出真正缺失的键
const missedUsers = await getUsersFromDBByIds(missedIds);
// 写入缓存
missedUsers.forEach(user => {
redis.set('user:' + user.id, JSON.stringify(user), 'EX', 3600);
missedResults.push({ id: user.id, result: user });
});
// 合并结果
const results = [];
ids.forEach(id => {
const item = missedResults.find(item => item.id === id);
if (item) {
results.push(item.result);
} else {
results.push(null);
}
});
return results;
}综上所述,利用 Redis 缓存可以大幅提升网站性能,但也带来了一些问题。通过采取优化方案可以有效避免这些问题,让网站运行更加平稳稳定。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d806e0a941bf7134e534d0