当前位置:网站首页>php解决redis的缓存雪崩,缓存穿透,缓存击穿的问题
php解决redis的缓存雪崩,缓存穿透,缓存击穿的问题
2022-07-05 09:48:00 【Emma'】
一:前言
设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。
二:缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。请求的数据大量的没有获取到缓存,导致走数据库,有可能搞垮数据库,使整个服务瘫痪。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
比如文章表,一般我们的主键ID都是无符号的自增类型,有些人想要搞垮你的数据库,每次请求都用负数ID,而ID为负数的记录在数据库根本就没有。
解决方案
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
- 对于像ID为负数的非法请求直接过滤掉,采用布隆过滤器(Bloom Filter)。 布隆过滤器:
<?php
class Bloom {
// 哈希函数的数量
protected $hashNum = 3;
// 位数组的大小
protected $bitArrayCount = 1024*10;
// 位数组
protected $bitArray = [];
public function __construct()
{
// 构建默认的位数组,全部置为 false
$this->bitArray = array_pad([], $this->bitArrayCount, false);
}
/** * 获取 hash 函数;也就是在位数组中,需要改为 true 的索引 * @param string $key 元素 * @return array */
protected function getIndexes($key)
{
$indexes = [];
for ($i = 0; $i < $this->hashNum; $i ++) {
$index = sprintf('%u', crc32($key . $i)); // 使用 crc32 散列
$index = $index % $this->bitArrayCount; // 获取 在位数组中的 位置
$indexes[] = $index;
}
return $indexes;
}
/** * 向过滤器中添加元素 * @param string $key 要添加的元素 */
public function addItem($key)
{
$indexes = $this->getIndexes($key);
// 将 hash 结果对应的位修改为 true
foreach ($indexes as $index) {
$this->bitArray[$index] = true;
}
}
/** * 过滤器中是否存在这个元素; true 表示很可能存在,false 表示一定不存在 * @param string $key 元素 * @return array */
public function mightExist($key)
{
$indexes = $this->getIndexes($key);
foreach ($indexes as $index) {
if (! $this->bitArray[$index]) {
return false;
}
}
return true;
}
}
class Test
{
public function run()
{
$bloom = new Bloom();
// 向过滤器中添加 1000 个元素
for ($i = 0; $i < 100; $i ++) {
$bloom->addItem($i);
}
// 测试 过滤器判断结果
for ($i = 90; $i < 110; $i ++) {
$mightExist = $bloom->mightExist($i);
if ($mightExist) {
echo "might exist ", $i, PHP_EOL;
} else {
echo "not exist ", $i, PHP_EOL;
}
}
}
}
(new Test())->run();
- 针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间。
//设置文章ID为-10000的缓存为空
$id = -10000;
$redis->set('article_content_' . $id, '', 60);
var_dump($redis->get('article_content_' . $id));
三:缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
使缓存集中失效的原因:
- redis服务器挂掉了。
- 对缓存数据设置了相同的过期时间,导致某时间段内缓存集中失效。
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
如何解决缓存集中失效:
- 针对原因1,可以实现redis的高可用,Redis Cluster 或者 Redis Sentinel(哨兵) 等方案。
- 针对原因2,设置缓存过期时间时加上一个随机值,避免缓存在同一时间过期。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379, 60);
$redis->auth('');
//设置过期时间加上一个随机值
$redis->set('article_content_1', '文章内容', 60 + mt_rand(1, 60));
$redis->set('article_content_2', '文章内容', 60 + mt_rand(1, 60));
- 使用双缓存策略,设置两个缓存,原始缓存和备用缓存,原始缓存失效时,访问备用缓存,备用缓存失效时间设置长点。
//原始缓存
$redis->set('article_content_2', '文章内容', 60);
//设置备用缓存,失效时间设置长点
$redis->set('article_content_backup_2', '文章内容', 1800);
四:缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存击穿与缓存雪崩的区别是这里针对的是某一热门key缓存,而雪崩针对的是大量缓存的集中失效。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
1、让该热门key的缓存永不过期。
这里的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
2、使用互斥锁,通过redis的setnx实现互斥锁。
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
<?php
function getRedis()
{
$redis = new Redis();
$redis->connect('127.0.0.1', 6379, 60);
return $redis;
}
//加锁
function lock($key, $random)
{
$redis = getRedis();
//设置锁的超时时间,避免释放锁失败,del()操作失败,产生死锁。
$ret = $redis->set($key, $random, ['nx', 'ex' => 3 * 60]);
return $ret;
}
//解锁
function unLock($key, $random)
{
$redis = getRedis();
//这里的随机数作用是,防止更新缓存操作时间过长,超过了锁的有效时间,导致其他请求拿到了锁。
//但上一个请求更新缓存完毕后,如果不加判断直接删除锁,就会误删其他请求创建的锁。
if ($redis->get($key) == $random) {
$redis->del($key);
}
}
//从缓存中获取文章数据
function getArticleInCache($id)
{
$redis = getRedis();
$key = 'article_content_' . $id;
$ret = $redis->get($key);
if ($ret === false) {
//生成锁的key
$lockKey = $key . '_lock';
//生成随机数,用于设置锁的值,后面释放锁时会用到
$random = mt_rand();
//拿到互斥锁
if (lock($lockKey, $random)) {
//这里是伪代码,表示从数据库中获取文章数据
$value = $db->getArticle($id);
//更新缓存,过期时间可以根据情况自已调整
$redis->set($key, $value, 2 * 60);
//释放锁
unLock($lockKey, $random);
} else {
//等待200毫秒,然后重新获取缓存值,让其他获取到锁的进程取得数据并设置缓存
usleep(200);
getArticleInCache($id);
}
} else {
return $ret;
}
}
3、"提前"使用互斥锁(mutex key):
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
4、资源保护:
采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
四种解决方案:没有最佳只有最合适
八:总结
针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
边栏推荐
- The comparison of every() and some() in JS uses a power storage plan
- Constraintlayout officially provides rounded imagefilterview
- Interview: is bitmap pixel memory allocated in heap memory or native
- Unity particle special effects series - the poison spray preform is ready, and the unitypackage package is directly used - on
- ThreadLocal source code learning
- 《剑来》语句摘录(七)
- Kotlin compose and native nesting
- ConstraintLayout的流式布局Flow
- 一种用于干式脑电图的高密度256通道电极帽
- Fluent generates icon prompt logo widget
猜你喜欢
Getting started with Apache dolphin scheduler (one article is enough)
Have you learned to make money in Dingding, enterprise micro and Feishu?
> Could not create task ‘:app:MyTest. main()‘. > SourceSet with name ‘main‘ not found. Problem repair
[system design] index monitoring and alarm system
一个程序员的职业生涯到底该怎么规划?
QT realizes signal transmission and reception between two windows
Wechat applet - simple diet recommendation (4)
ArcGIS Pro 创建要素
Kotlin Compose 多个条目滚动
Design and exploration of Baidu comment Center
随机推荐
Constrained layout flow
Jupiter notebook shortcut key
Wechat applet - simple diet recommendation (4)
Cut off 20% of Imagenet data volume, and the performance of the model will not decline! Meta Stanford et al. Proposed a new method, using knowledge distillation to slim down the data set
Personal website construction tutorial | local website environment construction | website production tutorial
《微信小程序-基础篇》小程序中的事件与冒泡
Energy momentum: how to achieve carbon neutralization in the power industry?
What is the origin of the domain knowledge network that drives the new idea of manufacturing industry upgrading?
Implementation of smart home project
善用兵者,藏于无形,90 分钟深度讲解最佳推广价值作品
【系统设计】指标监控和告警系统
NCP1342芯片替代料PN8213 65W氮化镓充电器方案
Optimize database queries using the cursor object of SQLite
Comparison of batch merge between Oracle and MySQL
Node red series (29): use slider and chart nodes to realize double broken line time series diagram
RMS TO EAP通过MQTT简单实现
Detailed explanation of the use of staticlayout
驱动制造业产业升级新思路的领域知识网络,什么来头?
. Net delay queue
MySQL character type learning notes