一.缓存
1.缓存的含义:
把⼀些常⽤的数据放到触⼿可及(访问速度更快)的地⽅, ⽅便随时读取
对于计算机硬件来说,往往访问速度越快的设备,成本越高,存储空间越小
缓存是更快, 但是空间上往往是不⾜的. 因此⼤部分的时候, 缓存只放⼀些 热点数据 (访问频繁的数据),就⾮常有⽤了.
2.使用Redis作为缓存:
在⼀个⽹站中, 我们经常会使⽤关系型数据库 (⽐如 MySQL) 来存储数据.
关系型数据库虽然功能强⼤, 但是有⼀个很⼤的缺陷, 就是性能不⾼. (换⽽⾔之, 进⾏⼀次查询操作消耗的系统资源较多)
因此, 如果访问数据库的并发量⽐较⾼, 对于数据库的压⼒是很⼤的, 很容易就会使数据库服务器宕机
如何让数据库承担更大的并发量,核心思路有两个:
- 开源:引⼊更多的机器, 部署更多的数据库实例, 构成数据库集群. (主从复制, 分库分表等...)
- **节流:**引⼊缓存, 使⽤其他的⽅式保存经常访问的热点数据, 从⽽降低直接访问数据库的请求数量
Redis 就是⼀个⽤来作为数据库缓存的常⻅⽅案.
Redis访问速度比MySQL快很多,或者说处理同一个访问请求,Redis消耗的系统资源比MySQl少很多,因此Redis能支持的并发量更大
- Redis的数据存储在内存中,访问内存比硬盘快得多
- Redis只是支持简单的key-value存储,不涉及复杂查询那么多限制规则

注:缓存是⽤来加快 "读操作" 的速度的. 如果是 "写操作", 还是要⽼⽼实实写数据库, 缓存并不能
提⾼性能.
3.缓存的更新策略
(1)定期生成:
每隔一定的周期,对于数据的访问频次进行统计,挑选出访问频次最高的前N%
(2)实时生成:
先给缓存容量设置上限,接下来把用户的每次查询:
- 如果在Redis查到了,就直接返回
- 如果Redis没查到,就去MySQL数据库查询,MySQL返回数据的同时将数据放进Redis中
如果缓存已经满了(达到上限), 就触发缓存淘汰策略, 把⼀些 "相对不那么热⻔" 的数据淘汰掉.
按照上述过程, 持续⼀段时间之后 Redis 内部的数据⾃然就是 "热⻔数据" 了
FIFO:先进先出,把缓存中存在时间最长(最先来的数据)淘汰掉
LRU:最近最少使用,当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
LFU:最少频次使用,统计每个key的访问频率,值越小的越先淘汰
Random:随即淘汰,从所有key中挑选幸运儿淘汰
Redis 内置的淘汰策略如下:
- volatile-lru: 当内存不⾜以容纳新写⼊数据时,从设置了过期时间的key中使⽤LRU(最近最
少使⽤)算法进⾏淘汰 - allkeys-lru:当内存不⾜以容纳新写⼊数据时,从所有key中使⽤LRU(最近最少使⽤)算法进
⾏淘汰 - volatile-lfu :4.0版本新增,当内存不⾜以容纳新写⼊数据时,在过期的key中,使⽤LFU算法
进⾏删除key - allkeys-lfu: 4.0版本新增,当内存不⾜以容纳新写⼊数据时,从所有key中使⽤LFU算法进⾏
淘汰 - volatile-random: 当内存不⾜以容纳新写⼊数据时,从设置了过期时间的key中,随机淘汰数
据 - allkeys-random: 当内存不⾜以容纳新写⼊数据时,从所有key中随机淘汰数据
- noeviction:默认策略,当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错
4.缓存穿透
含义:查一个不存在的数据,Redis和MySQL都查不到,也不会写入缓存,就会导致每次请求都在访问数据库,给数据库带来巨大压力
产生原因:
- 业务设计不合理. ⽐如缺少必要的参数校验环节, 导致⾮法的 key 也被进⾏查询了
- 开发/运维误操作. 不⼩⼼把部分数据从数据库上误删了
- ⿊客恶意攻击
解决方法:
- 针对要查询的参数进⾏严格的合法性校验. ⽐如要查询的 key 是⽤⼾的⼿机号, 那么就需要校验当前key 是否满⾜⼀个合法的⼿机号的格式
- 针对数据库上也不存在的 key , 也存储到 Redis 中, ⽐如 value 就随便设成⼀个 "". 避免后续频繁访问数据库
- 使用布隆过滤器判断key是否存在,再真正进行查询
5.缓存击穿
含义:给某个key设置了过期时间,当key过期了,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能瞬间把DB压垮
解决方法:
- 基于统计的方式设置热点key,并设置永不过期(高可用,性能优)
- 进行必要的服务升级,添加互斥锁,例如访问数据库的时候使⽤分布式锁, 限制同时请求数据库的并发数(强一致,性能差)
6.缓存雪崩
含义:在同一时间段大量的缓存key同时失效或redis服务宕机,导致大量的请求到达数据库,带来巨大的压力
解决方法:
- 给不同的key的TTL设置随机值
- 利用redis的集群提高服务的可用性(哨兵,集群)
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
二.分布式锁
1.含义:
在⼀个分布式的系统中, 也会涉及到多个节点访问同⼀个公共资源的情况. 此时就需要通过 锁 来做互斥控制, 避免出现类似于 "线程安全" 的问题
2.分布式锁的基础实现:
本质上就是通过**⼀个键值对**来标识锁的状态
举个例⼦: 考虑买票的场景, 现在⻋站提供了若⼲个⻋次, 每个⻋次的票数都是固定的.
现在存在多个服务器节点, 都可能需要处理这个买票的逻辑: 先查询指定⻋次的余票, 如果余票 > 0, 则设置余票值 -= 1,如下图所示:

很显然,上诉过程是存在"线程安全"问题的,所以就需要锁来控制

此时, 如果 买票服务器1 尝试买票, 就需要先访问 Redis, 在 Redis 上设置⼀个键值对. ⽐如 key 就是⻋次, value 随便设置个值
如果这个操作设置成功, 就视为当前没有节点对该 001 ⻋次加锁, 就可以进⾏数据库的读写操作. 操作完成之后, 再把 Redis 上刚才的这个键值对给删除掉.
如果在 买票服务器1 操作数据库的过程中, 买票服务器2 也想买票, 也会尝试给 Redis 上写⼀个键值对,key 同样是⻋次. 但是此时设置的时候发现该⻋次的 key 已经存在了, 则认为已经有其他服务器正在持有锁, 此时 服务器2 就需要等待或者暂时放弃
注:Redis 中提供了 setnx 操作, 正好适合这个场景. 即: key 不存在就设置, 存在则直接失败.
3.引入过期时间:
当 服务器1 加锁之后, 开始处理买票的过程中, 如果 服务器1 意外宕机了, 就会导致解锁操作 (删除该key) 不能执⾏. 就可能引起其他服务器始终⽆法获取到锁的情况.
为了解决这个问题, 可以在设置 key 的同时引⼊过期时间. 即这个锁最多持有多久, 就应该被释放.
注:可以使⽤ set ex nx 的⽅式, 在设置锁的同时把过期时间设置进去
4.引入校验id:
对于 Redis 中写⼊的加锁键值对, 其他的节点也是可以删除的
⽐如可以把设置的键值对的值, 不再是简单的设为⼀个 1, ⽽是设成服务器的编号. 形如 "001": "服务器1".这样就可以在删除 key (解锁)的时候, 先校验当前删除 key 的服务器是否是当初加锁的服务器, 如果是,才能真正删除; 不是, 则不能删除
5.引入lua:
为了使解锁操作原⼦, 可以使⽤ Redis 的 Lua 脚本功能
Lua 的语法类似于 JS, 是⼀个动态弱类型的语⾔. Lua 的解释器⼀般使⽤ C 语⾔实现. Lua 语法
简单精炼, 执⾏速度快, 解释器也⽐较轻量(Lua 解释器的可执⾏程序体积只有 200KB 左右).
因此 Lua 经常作为其他程序内部嵌⼊的脚本语⾔. Redis 本⾝就⽀持 Lua 作为内嵌脚本.
6.引入看门狗(Watch dog):
上述⽅案仍然存在⼀个重要问题. 当我们设置了 key 过期时间之后 (⽐如 10s), 仍然存在⼀定的可能性,当任务还没执⾏完, key 就先过期了. 这就导致锁提前失效
所以看门狗的作用就是对锁的过期时间进行"续约"
举个例子:
初始情况下设置过期时间为 10s. 同时设定看⻔狗线程每隔 3s 检测⼀次.
那么当 3s 时间到的时候, 看⻔狗就会判定当前任务是否完成
- 如果任务已经完成, 则直接通过 lua 脚本的⽅式, 释放锁(删除 key)
- 如果任务未完成, 则把过期时间重写设置为 10s. (即 "续约")
这样就不担⼼锁提前失效的问题了. ⽽且另⼀⽅⾯, 如果该服务器挂了, 看⻔狗线程也就随之挂了, 此时⽆⼈续约, 这个 key ⾃然就可以迅速过期, 让其他服务器能够获取到锁了
注:看门狗是业务服务器上的线程,不是Redis服务器上的线程
7.引入Redlock算法:
引⼊⼀组 Redis 节点. 其中每⼀组 Redis 节点都包含⼀个主节点和若⼲从节点. 并且组和组之间存
储的数据都是⼀致的, 相互之间是 "备份" 关系(⽽并⾮是数据集合的⼀部分, 这点有别于 Redis cluster).
加锁的时候, 按照⼀定的顺序, 写多个 master 节点. 在写锁的时候需要设定操作的 "超时时间". ⽐如
50ms. 即如果 setnx 操作超过了 50ms 还没有成功, 就视为加锁失败

如果给某个节点加锁失败, 就⽴即再尝试下⼀个节点.
当加锁成功的节点数超过总节点数的⼀半, 才视为加锁成功
(如上图,一共五个节点,三个加锁成功,两个失败,视为加锁成功)
这样的话, 即使有某些节点挂了, 也不影响锁的正确性
同理, 释放锁的时候, 也需要把所有节点都进⾏解锁操作. (即使是之前超时的节点, 也要尝试解锁, 尽量保证逻辑严密)
Redlock 算法的核⼼就是, 加锁操作不能只写给⼀个 Redis 节点, ⽽要写给多个!!
分布式系统中任何⼀个节点都是不可靠的. 最终的加锁成功结论是 "少数服从多数的 ".
由于⼀个分布式系统不⾄于⼤部分节点都同时出现故障, 因此这样的可靠性要⽐单个节点来说靠谱不少.