💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
>[info] redis做缓存场景 1. 开销大的复杂计算:以MySQL为例子,一些复杂的操作或者计算(例如大量联表操作、一些分组计算),如果不加缓存,不但无法满足高并发量,同时也会给MySQL带来巨大的负担。 2. String类型:计数器,缓存用户对象,分布式锁,session共享 3. List类型:不频繁更新排行榜,消息队列 4. Hash类型:购物车,缓存对象 5. Zset:实时排行榜,抢票加速包 6. Set类型:分库分表之后减少复杂查询,黑白名单,帅选功能等 ***** 图左侧为客户端直接调用存储层的架构,右侧为比较典型的缓存层+存储层架构。 ![](https://img.kancloud.cn/91/4f/914fab8e9b2ffe68c2ad80eb138bd625_1165x616.png) ***** >[info] 缓存维度划分 **一张用户表中有整整100个字段,那么我们在redis中该怎么去缓存这个用户数据呢?** 1. 缓存全部数据 ``` Set(key,select * from user wgere id= xxx) ``` 2. 缓存部分数据 ``` Set(key,select 字段1,字段2,…字段n from user where id=xxx) ``` | 数据类型 | 通用性 | 空间占比 | 代码维护 | | --- | --- | --- | --- | | 全部数据 | 高 | 大 | 简单 | | 部分数据 | 低 | 小 | 复杂 | >[info] 缓存击穿 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。 ![](https://img.kancloud.cn/f3/26/f3269679c52cdaae1648c937c6fe991f_1190x531.png) ***** **为什么会出现缓存击穿?** 一般在使用缓存去缓存数据时,会使用“缓存+过期时间”的策略既可以加速数据读写,又可以保证数据的定期更新,这个模式在一般情况的业务都是可以满足的。 ***** **但是这样也会出现两个致命问题:** 1. 当前key是一个热点数据(比如:某个热门的文章,新闻),并发量非常大。 2. 重建缓存不能在短时间完成,可能是一个复杂的计算,例如复杂的sql,多次IO,多个依赖等。 ***** **在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能让应用崩溃,如果要解决这个问题至少是遵循如下:** 1. 减少重建缓存的次数 2. 数据尽可能一致 3. 较少的潜在威胁 ***** >[info] 缓存击穿问题解决 **1. 互斥锁** ![](https://img.kancloud.cn/c4/20/c42076c4705fa0251293dc06991cd801_1690x898.png) ``` <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\DB; class TestController extends Controller { public function index(Request $request) { $userId = $request->input("user_id"); dd(json_decode($this->getArticle(1))); } public function lock($key,$random) { $lock = Redis::set($key,$random,"nx","ex",10); return $lock; } public function unlock($key,$random) { if (Redis::get($key) == $random){ Redis::del($key); } } public function getArticle($id) { $key = "article_content_".$id; $ret = Redis::get($key); if ($ret === null) { //生成锁的key $lockKey = $key . '_lock'; //生成随机数,用于设置锁的值,后面释放锁时会用到 $random = mt_rand(); //拿到互斥锁 if ($this->lock($lockKey, $random)) { //这里是伪代码,表示从数据库中获取文章数据 $ret = json_encode(DB::table("article")->where("id",$id)->first()); //更新缓存,过期时间可以根据情况自已调整 Redis::set($key, json_encode($ret)); //释放锁 $this->unLock($lockKey, $random); } else { //等待200毫秒,然后重新获取缓存值,让其他获取到锁的进程取得数据并设置缓存 usleep(200); return $this->getArticle($id); } }else{ $ret = json_decode($ret); } return $ret; } } ``` **代码解释:** ~~~ 1. 从redis获取数据,如果值不为空,则直接返回值,否则执行下面的步骤: 2. 如果set(nx和ex)结果为true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑 2. 如果set(nx和ex)结果为false,说明此时已有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间后,重新执行函数,直到获取到数据。 ~~~ ***** **2. key设置永不过期** 设置永不过期这里包含两层意思: ``` 1. 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是物理过期。 2. 从功能层面看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。 ``` ***** 从实战方面来说,设置热点数据永不过期有效杜绝了热点key产生的问题,但是不足的点在于重构缓存期间,会出现数据不一致性的情况,这种情况在应用中是坚决不允许出现的。 ![](https://img.kancloud.cn/3f/95/3f95f333a740339c9f81ec6d1157e041_1191x660.png) 解决数据不一致的问题可以是每次查询的时候都去判断下是否超时,超时就立刻更新缓存数据。