目录
- 概述
- 缓存穿透
- 缓存击穿
- 缓存雪崩
- php7进阶到架构师相关阅读
概述
这是关于php进阶到架构之 Redis进阶 学习的系列课程:Redis缓存穿透,缓存击穿,缓存雪崩的解决方法以及代码实现
学习目标 :
解决工作中遇到Redis出现缓存穿透,缓存击穿,缓存雪崩等问题
缓存穿透
缓存穿透表示查询一个一定不存在的数据,由于没有获取到缓存,所以没写入缓存,导致这个不存在的数据每次都需要去数据库查询,失去了缓存的意义。
比如根据文章ID获取文章,如下代码:
$db = new Db();
$redis = new \Redis("127.0.0.1",6379);
$articleId = -100;
$articleKey = 'article_'.$articleId;
$article = $redis->get($articleKey);
if(empty($article)){//缓存不存在,读取数据库
$article = $db->getArticle($articleId);
$redis->set($articleKey,json_encode($article),600);
}
上述代码存在什么问题呢?
当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了 缓存穿透 。请求的数据大量的没有获取到缓存,导致走数据库,有可能搞垮数据库,使整个服务瘫痪。
解决方案 :
方法1:
采用布隆过滤器(Bloom Filter),对于像ID为负数的非法请求直接过滤掉
方法2:
针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间。
业务流程如下:
代码示例:
$db = new Db();
$redis = new \Redis("127.0.0.1",6379);
$articleId = 100;
$articleKey = 'article_'.$articleId;
//这里是伪代码,表示从数据库中获取文章数据
$article = $redis->get($articleKey);
if(empty($article)){
$article = $db->getArticle($articleId);
if(empty($article)){
$article['error'] = 1;//表示数据库不存在
}
$redis->set($articleKey,json_encode($article),100);
}
缓存击穿
缓存击穿表示某个key的缓存非常热门,有很高的并发一直在访问,如果该缓存失效,那同时会走数据库,压垮数据库。
解决方案:
1、让该热门key的缓存永不过期
2、使用互斥锁,通过redis的set实现互斥锁
<?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 = time().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;
}
}
缓存雪崩
在某一时间段,缓存集中失效,导致请求全部走数据库,有可能搞垮数据库,使整个服务瘫痪。缓存击穿与缓存雪崩的区别:缓存击穿是某一热门key缓存,而雪崩针对的是大量缓存的集中失效
使缓存集中失效的原因:
1、redis服务器挂掉了
2、对缓存数据设置了相同的过期时间,导致某时间段内缓存集中失效。
如何解决缓存集中失效:
1、针对原因1,可以实现redis的高可用,Redis Cluster 或者 Redis Sentinel(哨兵) 等方案
2、针对原因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, 6));