pull/1/head
595208882@qq.com 3 years ago
parent fc72f2324e
commit 572185a9cc

@ -3462,26 +3462,44 @@ FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存
## 缓存策略
## 更新策略
缓存更新的策略主要分为三种:
- **Cache-Aside**:通常会先更新数据库,然后再删除缓存,为了兜底通常还会将数据设置缓存时间
- **Read/Write through**:一般是由一个 Cache Provider 对外提供读写操作,应用程序不用感知操作的是缓存还是数据库
- **Write-Behind**即延迟写入Cache Provider 每隔一段时间会批量输入数据库,优点是应用程序写入速度非常快
- **Cache Aside Pattern旁路缓存**
- **Read/Write Through Pattern读写穿透**
- **Write Behind Caching Pattern异步写入**
### Cache-Aside
`Cache-Aside旁路缓存`的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
**缓存使用场景**
**分布式系统中要么通过2PC、3PC或Paxos协议保证强一致性要么就是拼命的降低并发时脏数据的概率**。缓存系统适用的场景就是非强一致性的场景所以它属于CAP中的AP只能做到BASE理论中说的**最终一致性**。异构数据库本来就没办法强一致,我们只是**尽可能减少不一致的时间窗口,达到最终一致性**。同时结合设置过期时间的兜底方案。
**缓存场景分析**
- 对于读多写少的数据,请使用缓存
- 为了保持数据库和缓存的一致性,会导致系统吞吐量的下降
- 为了保持数据库和缓存的一致性,会导致业务代码逻辑复杂
- 缓存做不到绝对一致性,但可以做到最终一致性
- 对于需要保证缓存数据库数据一致的情况,请尽量考虑对一致性到底有多高要求,选定合适的方案,避免过度设计
### Cache Aside(旁路缓存)
`Cache Aside旁路缓存` 是最广泛使用的缓存模式之一,如果能正确使用 `Cache Aside` 的话,能极大的提升应用性能,`Cache Aside`可用来读或写操作。`Cache Aside`的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
#### Read Cache Aside
`Cache-Aside` 的读请求流程如下:
`Cache Aside` 的读请求流程如下:
![Cache-Aside读请求](images/Solution/Cache-Aside读请求.jpg)
- 读的时候,先读缓存,缓存命中的话,直接返回数据
- 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应
- **读的时候,先读缓存,缓存命中的话,直接返回数据**
- **缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应**
@ -3491,54 +3509,142 @@ FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存
![Cache-Aside写请求](images/Solution/Cache-Aside写请求.jpg)
- 更新的时候,先**更新数据库,然后再删除缓存**
- **更新的时候,先更新数据库,然后再删除缓存**
### Read/Write-Through
### Read/Write Through(读写穿透)
`Read/Write-Through读写穿透` 模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过**抽象缓存层**完成的。
`Read/Write Through读写穿透` 模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过**抽象缓存层**完成的。
#### Read-Through
#### Read Through
`Read-Through`的简要流程如下
`Read Through` 和 `Cache Aside` 很相似,不同点在于程序不需要再去管理从哪去读数据(缓存还是数据库)。相反它会直接从缓存中读数据,该场景下是缓存去决定从哪查询数据。当我们比较两者的时候这是一个优势因为它会让程序代码变得更简洁。`Read Through`的简要流程如下
![Read-Through简要流程](images/Solution/Read-Through简要流程.png)
- 从缓存读取数据,读到直接返回
- 如果读取不到的话,从数据库加载,写入缓存后,再返回响应
- **从缓存读取数据,读到直接返回**
- **如果读取不到的话,从数据库加载,写入缓存后,再返回响应**
这个简要流程是不是跟`Cache-Aside`很像呢?其实`Read-Through`就是多了一层`Cache-Provider`而已,流程如下:
![Read-Through流程](images/Solution/Read-Through流程.png)
`Read-Through`实际只是在`Cache-Aside`之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。
该模式只在 `Cache Aside` 之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。流程如下:
![Read-Through流程](images/Solution/Read-Through流程.png)
#### Write-Through
#### Write Through
`Write-Through`模式下,当发生写请求时,也是由**缓存抽象层**完成数据源和缓存数据的更新,流程如下:
`Write Through` 模式下的所有写操作都经过缓存,每次向缓存中写数据时,缓存会把数据持久化到对应的数据库中去,且这两个操作都在一个事务中完成。因此,只有两次都写成功才是最终写成功。用写延迟保证了数据一致性。当发生写请求时,也是由**缓存抽象层**完成数据源和缓存数据的更新,流程如下:
![Write-Through](images/Solution/Write-Through.png)
- **向缓存中写数据,并向数据库写数据**
- **使用事务保证一致性,两者都写成功才成功**
当使用 `Write Through `的时候一般都配合使用 `Read Through`。`Write Through `适用情况有:
- **需要频繁读取相同数据**
- **不能忍受数据丢失(相对 `Write Behind` 而言)和数据不一致**
**`Write Through` 的潜在使用例子是银行系统。**
### Write-Behind
`Write-Behind`在一些地方也被成为`Write back` 简单理解就是:应用程序更新数据时只更新缓存, `Cache Provider`每隔一段时间将数据刷新到数据库中。说白了就是`延迟写入`。
`Write-Behind异步写入``Read/Write-Through` 有相似的地方,都是由`Cache Provider`来负责缓存和数据库的读写。它们又有个很大的不同:`Read/Write-Through`是同步更新缓存和数据的,`Write-Behind`则是只更新缓存,不直接更新数据库,通过**批量异步**的方式来更新数据库。
### Write Behind(异步写入)
`Write Behind异步写入又叫Write Back``Read/Write Through` 相似,都是由 `Cache Provider` 来负责缓存和数据库的读写。它们又有个很大的不同:`Read/Write Through` 是同步更新缓存和数据的,`Write Behind` 则是只更新缓存,不直接更新数据库,通过**批量异步**的方式来更新数据库。
![WriteBehind流程](images/Solution/WriteBehind流程.png)
这种方式下,缓存和数据库的一致性不强,**对一致性要求高的系统要谨慎使用**。但是它适合频繁写的场景MySQL的**InnoDB Buffer Pool机制**就使用到这种模式。如上图应用程序更新两个数据Cache Provider 会立即写入缓存中,但是隔一段时间才会批量写入数据库中。优缺点如下:
这种方式下,缓存和数据库的一致性不强,**对一致性要求高的系统要谨慎使用**。但是它适合频繁写的场景MySQL的**InnoDB Buffer Pool 机制**就使用到这种模式。如上图应用程序更新两个数据Cache Provider 会立即写入缓存中,但是隔一段时间才会批量写入数据库中。优缺点如下:
- **优点**:是数据写入速度非常快,适用于频繁写的场景
- **缺点**:是缓存和数据库不是强一致性,对一致性要求高的系统慎用
## 数据一致性
![缓存双写一致性](images/Solution/缓存双写一致性.png)
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
- **强一致性**:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
- **弱一致性**:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
- **最终一致性**:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
### 业务延时双删
先删除缓存,再更新数据库中如何避免脏数据?采用延时双删策略。
![延时双删流程](images/Solution/延时双删流程.png)
- **先删除缓存**
- **再写数据库**
- **休眠1秒后再次删除缓存**这1秒=业务可能最大耗时,主要是等待正在加载脏数据的请求完成)
**① 读写分离架构**
读写架构中,先删除缓存,再更新数据库中如何避免脏数据?采用延时双删策略。
- **先淘汰缓存**
- **再写数据库**
- **休眠1秒后再次淘汰缓存**这1秒=主从同步可能最大耗时+业务可能最大耗时,主要是等待正在加载脏数据的请求完成)
**② 延时双删导致吞吐量降低**
延时双删的方式同步淘汰策略导致了吞吐量降低如何解决?
- **将第二次删除作为异步**
### MQ重试机制
不管是**延时双删**还是**Cache-Aside的先操作数据库再删除缓存**,都可能会存在第二步的删除缓存失败,导致的数据不一致问题,可以引入**删除缓存重试机制**来解决。
![删缓存失败-解决方案一](images/Solution/删缓存失败-解决方案一.png)
![缓存一致性-基于MQ的解决方案](images/Solution/缓存一致性-基于MQ的解决方案.jpg)
流程如下:
- **更新数据库数据**
- **删除缓存中的数据,可此时缓存服务出现不可用情况,造成无法删除缓存数据**
- **当删除缓存数据失败时,将需要删除缓存的 Key 发送到消息队列 (MQ) 中**
- **应用自己消费需要删除缓存 Key 的消息**
- **应用接收到消息后,删除缓存,如果删除缓存确认 MQ 消息被消费,如果删除缓存失败,则让消息重新入队列,进行多次尝试删除缓存操作**
### biglog异步删除
重试删除缓存机制会造成好多**业务代码入侵**。其实还可以这样优化:通过数据库的**binlog来异步淘汰key**。
![删缓存失败-解决方案二](images/Solution/删缓存失败-解决方案二.png)
![缓存一致性-基于Canal的解决方案](images/Solution/缓存一致性-基于Canal的解决方案.jpg)
流程如下:
- **更新数据库数据**
- **MySQL将数据更新日志写入binlog中**
- **Canal订阅&消费MySQL binlog并提取出被更新数据的表名及ID**
- **调用应用删除缓存接口**
- **删除缓存数据**
- **Redis 不可用时,将更新数据的表名及 ID 发送到 MQ 中**
- **应用接收到消息后,删除缓存,如果删除缓存确认 MQ 消息被消费,如果删除缓存失败,则让消息重新入队列,进行多次尝试删除缓存操作,直到缓存删除成功为止**
## 策略选择
### 删除or更新
@ -3593,6 +3699,75 @@ A先把数据库更新为 123由于网络问题更新缓存的动作慢了
### 缓存更新
除了缓存服务器自带的缓存失效策略之外Redis默认的有6中策略可供选择还可以根据具体的业务需求进行自定义的缓存淘汰。常见的更新策略如下
- **LRU/LFU/FIFO**:都是属于当**缓存不够用**时采用的更新算法
适合内存空间有限,数据长期不变动,基本不存在数据一不致性业务。比如一些一经确定就不允许变更的信息。
- **超时剔除**:给缓存数据设置一个过期时间
适合于能够容忍一定时间内数据不一致性的业务,比如促销活动的描述文案。
- **主动更新**:如果数据源的数据有更新,则主动更新缓存
对于数据的一致性要求很高,比如交易系统,优惠劵的总张数。
常见数据更新方式有两大类,其余基本都是这两类的变种:
**方式一:先删缓存,再更新数据库**
![缓存更新-先删缓存再更新数据库](images/Solution/缓存更新-先删缓存再更新数据库.png)
这种做法是遇到数据更新我们先去删除缓存然后再去更新DB如左图。让我们来看一下整个操作的流程
- A请求需要更新数据先删除对应的缓存还未更新DB
- B请求来读取数据
- B请求看到缓存里没有就去读取DB并将旧数据写入缓存脏数据
- A请求更新DB
可以看到B请求将脏数据写入了缓存如果这是一个读多写少的数据可能脏数据会存在比较长的时间要么有后续更新要么等待缓存过期这是业务上不能接受的。
**方式二:先更新数据库,再删除缓存**
![缓存更新-先更新数据库再删除缓存](images/Solution/缓存更新-先更新数据库再删除缓存.png)
上图的右侧部分可以看到在A更新DB和删除缓存之间B请求会读取到老数据因为此时A操作还没有完成并且这种读到老数据的时间是非常短的可以满足数据最终一致性要求。
**删除缓存而非更新缓存原因**
上图可以看到我们用的是删除缓存,而不是更新缓存,原因如下图:
![缓存更新-删除缓存原因](images/Solution/缓存更新-删除缓存原因.png)
上图我用操作代替了删除或更新当我们做删除操作时A先删还是B先删没有关系因为后续读取请求都会从DB加载出最新数据但是当我们对缓存做的是更新操作时就会对A先更新缓存还是B先更新缓存敏感了如果A后更新那么缓存里就又存在脏数据了所以 go-zero 只使用删除缓存的方式。
**缓存更新请求处理流程**
我们来一起看看完整的请求处理流程:
![缓存更新-请求处理流程](images/Solution/缓存更新-请求处理流程.png)
**注意**:不同颜色代表不同请求。
- 请求1更新DB
- 请求2查询同一个数据返回了老的数据这个短时间内返回旧数据是可以接受的满足最终一致性
- 请求1删除缓存
- 请求3再来请求时缓存里没有就会查询数据库并回写缓存再返回结果
- 后续的请求就会直接读取缓存了
## 缓存问题
### 缓存雪崩
@ -3677,171 +3852,101 @@ A先把数据库更新为 123由于网络问题更新缓存的动作慢了
### 缓存预热
缓存预热是指系统上线后提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候先查询数据库然后再将数据缓存的问题用户直接查询事先被预热的缓存数据。如果不进行预热那么Redis初始状态数据为空系统上线初期对于高并发的流量都会访问到数据库中 对数据库造成流量的压力。
**缓存预热思路:**
- **数据量不大的时候**:工程启动的时候进行加载缓存动作
- **数据量大的时候**:设置一个定时任务脚本,进行缓存的刷新
- **数据量太大的时候**:优先保证热点数据进行提前加载到缓存
**缓存预热解决方案:**
- 直接写个缓存刷新页面,上线时手工操作下
- 数据量不大,可以在项目启动的时候自动进行加载
- 定时刷新缓存
### 缓存降级
缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
**降级的最终目的是保证核心服务可用,即使是有损的**。而且有些服务是无法降级的(如加入购物车、结算)。
**分级降级预案:**
- **一般**:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级
- **警告**有些服务在一段时间内成功率有波动如在95~100%之间),可以自动降级或人工降级,并发送告警
- **错误**比如可用率低于90%,或数据库连接池被打爆,或访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
- **严重错误**:比如因为特殊原因数据错误了,此时需要紧急人工降级
### 缓存更新
除了缓存服务器自带的缓存失效策略之外Redis默认的有6中策略可供选择还可以根据具体的业务需求进行自定义的缓存淘汰。常见的更新策略如下
- **LRU/LFU/FIFO**:都是属于当**缓存不够用**时采用的更新算法
适合内存空间有限,数据长期不变动,基本不存在数据一不致性业务。比如一些一经确定就不允许变更的信息。
- **超时剔除**:给缓存数据设置一个过期时间
适合于能够容忍一定时间内数据不一致性的业务,比如促销活动的描述文案。
- **主动更新**:如果数据源的数据有更新,则主动更新缓存
对于数据的一致性要求很高,比如交易系统,优惠劵的总张数。
常见数据更新方式有两大类,其余基本都是这两类的变种:
**方式一:先删缓存,再更新数据库**
![缓存更新-先删缓存再更新数据库](images/Solution/缓存更新-先删缓存再更新数据库.png)
这种做法是遇到数据更新我们先去删除缓存然后再去更新DB如左图。让我们来看一下整个操作的流程
- A请求需要更新数据先删除对应的缓存还未更新DB
- B请求来读取数据
- B请求看到缓存里没有就去读取DB并将旧数据写入缓存脏数据
- A请求更新DB
可以看到B请求将脏数据写入了缓存如果这是一个读多写少的数据可能脏数据会存在比较长的时间要么有后续更新要么等待缓存过期这是业务上不能接受的。
## Hot Key
**方式二:先更新数据库,再删除缓存**
### 产生原因
![缓存更新-先更新数据库再删除缓存](images/Solution/缓存更新-先更新数据库再删除缓存.png)
- **用户消费的数据远大于生产的数据**(热卖商品、热点新闻、热点评论、明星直播)
上图的右侧部分可以看到在A更新DB和删除缓存之间B请求会读取到老数据因为此时A操作还没有完成并且这种读到老数据的时间是非常短的可以满足数据最终一致性要求
在日常工作生活中一些突发的的事件,例如:双十一期间某些热门商品的降价促销,当这其中的某一件商品被数万次点击浏览或者购买时,会形成一个较大的需求量,这种情况下就会造成热点问题。同理,被大量刊发、浏览的热点新闻、热点评论、明星直播等,这些典型的读多写少的场景也会产生热点问题。
- **请求分片集中,超过单 Server 的性能极限**
在服务端读数据进行访问时,往往会对数据进行分片切分,此过程中会在某一主机 Server 上对相应的 Key 进行访问,当访问超过 Server 极限时,就会导致热点 Key 问题的产生。
**删除缓存而非更新缓存原因**
上图可以看到我们用的是删除缓存,而不是更新缓存,原因如下图:
![缓存更新-删除缓存原因](images/Solution/缓存更新-删除缓存原因.png)
### 问题危害
上图我用操作代替了删除或更新当我们做删除操作时A先删还是B先删没有关系因为后续读取请求都会从DB加载出最新数据但是当我们对缓存做的是更新操作时就会对A先更新缓存还是B先更新缓存敏感了如果A后更新那么缓存里就又存在脏数据了所以 go-zero 只使用删除缓存的方式
当某一热点 Key 的请求在某一主机上超过该主机网卡上限时,由于流量的过度集中,会导致服务器中其它服务无法进行。如果热点过于集中,热点 Key 的缓存过多,超过目前的缓存容量时,就会导致缓存分片服务被打垮现象的产生。当缓存服务崩溃后,此时再有请求产生,会缓存到后台 DB 上由于DB 本身性能较弱,在面临大请求时很容易发生请求穿透现象,会进一步导致雪崩现象,严重影响设备的性能。
- **流量集中,达到物理网卡上限**
- **请求过多,缓存分片服务被打垮**
- **DB 击穿,引起业务雪崩**
**缓存更新请求处理流程**
我们来一起看看完整的请求处理流程:
![缓存更新-请求处理流程](images/Solution/缓存更新-请求处理流程.png)
### 发现热key
**注意**:不同颜色代表不同请求。
- 预估热key如秒杀商品火爆新闻
- 在客户端进行统计
- 可用Proxy如Codis可以在Proxy端收集
- 利用redis自带命令monitorhotkeys。**执行缓慢,不推荐使用**
- 利用流式计算引擎统计访问次数如Storm、Spark Streaming、Flink
- 请求1更新DB
- 请求2查询同一个数据返回了老的数据这个短时间内返回旧数据是可以接受的满足最终一致性
- 请求1删除缓存
- 请求3再来请求时缓存里没有就会查询数据库并回写缓存再返回结果
- 后续的请求就会直接读取缓存了
### 解决方案
### 缓存Hot Key
通常的解决方案主要集中在对客户端和 Server 端进行相应的改造。
对于突发事件,大量用户同时去访问热点信息,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至 Crash我们称之为缓存热点。这个在新浪微博经常遇到某大V明星出轨、结婚、离婚瞬间引发数百千万的吃瓜群众围观访问同一个key流量集中打在一个缓存节点机器很容易打爆网卡、带宽、CPU的上限最终导致缓存不可用。
#### 读写分离
一般使用 **缓存+过期时间** 的策略来加速读写,又保证数据的定期更新,这种模式基本能满足绝大部分需求。但是如果有两个问题同时出现,可能会对应用造成致命的伤害:
![读写分离方案解决热读](images/Solution/读写分离方案解决热读.jpg)
- **当前key是一个hot key**。比如热点娱乐新闻,并发量非常大
- **重建缓存不能在短时间完成,可能是一个复杂计算**。例如复杂的SQL、多次IO、多个依赖等
架构中各节点的作用如下:
当缓存失效的瞬间,将会有大量线程来重建缓存,造成后端负载加大,甚至让应该崩溃。
- **SLB 层做负载均衡**
- **Proxy 层做读写分离自动路由**
- **Master 负责写请求**
- **ReadOnly 节点负责读请求**
- **Slave 节点和 Master 节点做高可用**
实际过程中 Client 将请求传到 SLBSLB 又将其分发至多个 Proxy 内,通过 Proxy 对请求的识别,将其进行分类发送。例如,将同为 Write 的请求发送到 Master 模块内,而将 Read 的请求发送至 ReadOnly 模块。而模块中的只读节点可以进一步扩充从而有效解决热点读的问题。读写分离同时具有可以灵活扩容读热点能力、可以存储大量热点Key、对客户端友好等优点。
**如何发现热key**
- 预估热key如秒杀商品火爆新闻
- 在客户端进行统计
- 可用Proxy如Codis可以在Proxy端收集
- 利用redis自带命令monitorhotkeys。**执行缓慢,不推荐使用**
- 利用流式计算引擎统计访问次数如Storm、Spark Streaming、Flink
#### 热点数据
![热点数据解决方案](images/Solution/热点数据解决方案.jpg)
该方案通过主动发现热点并对其进行存储来解决热点 Key 的问题。首先 Client 也会访问 SLB并且通过 SLB 将各种请求分发至 Proxy 中Proxy 会按照基于路由的方式将请求转发至后端的 Redis 中。在热点 key 的解决上是采用在服务端增加缓存的方式进行。具体来说就是在 Proxy 上增加本地缓存,本地缓存采用 LRU 算法来缓存热点数据,后端 DB 节点增加热点数据计算模块来返回热点数据。Proxy 架构的主要有以下优点:
解决方案主要包括以下几种:
- **Proxy 本地缓存热点,读能力可水平扩展**
- **DB 节点定时计算热点数据集合**
- **DB 反馈 Proxy 热点数据**
- **对客户端完全透明,不需做任何兼容**
- 首先能先找到这个`热key`来,比如通过`Spark`实时流分析及时发现新的热点key
- 将集中化流量打散避免一个缓存节点过载。由于只有一个key我们可以在key的后面拼上`有序编号`,比如`key#01`、`key#02`......`key#10`多个副本这些加工后的key位于多个缓存节点上。每次请求时客户端随机访问一个即可
- **本地缓存**
变更分布式缓存为本地缓存,减少网络开销,提高吞吐量。
### 热点key处理
- **互斥锁**
#### 热点数据的读取
具体做法是只允许一个线程重建缓存,其它线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
![热点数据的读取](images/Solution/热点数据的读取.jpg)
- **方案风险**:重建的时间太长或者并发量太大,将会大量的线程阻塞,同样会加大系统负载
在热点 Key 的处理上主要分为写入跟读取两种形式,在数据写入过程当 SLB 收到数据 K1 并将其通过某一个 Proxy 写入一个 Redis完成数据的写入。假若经过后端热点模块计算发现 K1 成为热点 key 后, Proxy 会将该热点进行缓存当下次客户端再进行访问K1时可以不经Redis。最后由于Proxy是可以水平扩充的因此可以任意增强热点数据的访问能力。
- **优化方案**:除了重建线程之外,其它线程拿旧值直接返回
比如Google的Guava Cache的refreshAfterWrite采用的就是这种方案避免雪崩效应。
- **永不过期**
#### 热点数据的发现
这种就是缓存更新操作是独立的,可以通过跑定时任务来定期更新,或者变更数据时主动更新。
![热点数据的发现](images/Solution/热点数据的发现.jpg)
- **限流熔断**
对于 db 上热点数据的发现,首先会在一个周期内对 Key 进行请求统计,在达到请求量级后会对热点 Key 进行热点定位,并将所有的热点 Key 放入一个小的 LRU 链表内,在通过 Proxy 请求进行访问时,若 Redis 发现待访点是一个热点就会进入一个反馈阶段同时对该数据进行标记。DB 计算热点时,主要运用的方法和优势有:
以上两种方案都是建立在我们事先知道hot key的情况下如果事先知道哪些是hot key其实问题都不是很大。问题是我们不知道的情况既然hot key的危害是因为有大量的重建请求落到了后端如果后端自己做了限流只有部分请求落到了后端, 其它的都打回去了。一个hot key只要有一个重建请求处理成功了,后面的请求都是直接走缓存了,问题就解决了。
- **基于统计阀值的热点统计**
- **基于统计周期的热点统计**
- **基于版本号实现的无需重置初值统计方法**
- **DB 计算同时具有对性能影响极其微小、内存占用极其微小等优点**
### 缓存Big Key
## Big Key
Big Key指数据量大的key由于其数据大小远大于其它key导致经过分片之后某个具体存储这个Big Key的实例内存使用量远大于其他实例造成内存不足拖累整个集群的使用。
**常见场景**
### 常见场景
- 热门话题下的讨论
- 大V的粉丝列表
@ -3850,7 +3955,7 @@ Big Key指数据量大的key由于其数据大小远大于其它key导致
**大Key影响**
### 大Key影响
- 大key会大量占用内存在Redis集群中无法均衡
- Reids性能下降影响主从复制
@ -3858,14 +3963,14 @@ Big Key指数据量大的key由于其数据大小远大于其它key导致
**如何发现大key**
### 如何发现大Key
- redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。但如果Redis的key比较多执行该命令会比较慢
- 获取生产Redis的rdb文件通过rdbtools分析rdb生成csv文件再导入MySQL或其他数据库中进行分析统计根据size_in_bytes统计bigkey
**解决方案**
### 解决方案
优化big key的原则就是string减少字符串长度而list、hash、set、zset等则减少成员数
@ -3878,78 +3983,51 @@ Big Key指数据量大的key由于其数据大小远大于其它key导致
## 数据一致性
![缓存双写一致性](images/Solution/缓存双写一致性.png)
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
- **强一致性**:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
- **弱一致性**:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
- **最终一致性**:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
## 其它问题
数据库和缓存数据如何保持强一致?实际上,没办法做到数据库与缓存**绝对的一致性**。
- 加锁可以吗?并发写期间加锁,任何读操作不写入缓存?
- 缓存及数据库封装CAS乐观锁更新缓存时通过lua脚本
- 分布式事务3PCTCC
其实,这是由**CAP理论**决定的。缓存系统适用的场景就是非强一致性的场景它属于CAP中的AP。**个人觉得,追求绝对一致性的业务场景,不适合引入缓存**。
> CAP理论指的是在一个分布式系统中 Consistency一致性、 Availability可用性、Partition tolerance分区容错性三者不可得兼。
但是,通过一些方案优化处理,是可以**保证弱一致性,最终一致性**的。有3种方案保证数据库与缓存的一致性**缓存延时双删、删除缓存重试机制和读取biglog异步删除缓存**。
### 缓存预热
缓存预热是指系统上线后提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候先查询数据库然后再将数据缓存的问题用户直接查询事先被预热的缓存数据。如果不进行预热那么Redis初始状态数据为空系统上线初期对于高并发的流量都会访问到数据库中 对数据库造成流量的压力。
**缓存预热思路**
### 缓存延时双删
- **数据量不大的时候**:工程启动的时候进行加载缓存动作
- **数据量大的时候**:设置一个定时任务脚本,进行缓存的刷新
- **数据量太大的时候**:优先保证热点数据进行提前加载到缓存
有人可能会说,并不一定要先操作数据库呀,采用**缓存延时双删**策略,就可以保证数据的一致性啦。什么是延时双删呢?
![延时双删流程](images/Solution/延时双删流程.png)
1. 先删除缓存
2. 再更新数据库
3. 休眠一会比如1秒再次删除缓存
**预热解决方案**
这个休眠一会一般多久呢都是1秒
- 直接写个缓存刷新页面,上线时手工操作下
- 数据量不大,可以在项目启动的时候自动进行加载
- 定时刷新缓存
> 这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒
>
> 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
这种方案还算可以只有休眠那一会比如就那1秒可能有脏数据一般业务也会接受的。但是如果**第二次删除缓存失败**呢缓存和数据库的数据还是可能不一致对吧给Key设置一个自然的expire过期时间让它自动过期怎样那业务要接受**过期时间**内,数据的不一致咯?还是有其他更佳方案呢?
**缓存加载策略**
- **使用时加载缓存**。当需要使用缓存数据时,就从数据库中查出,第一次查出后,接下来的请求都能从缓存中查询到数据
- **预加载缓存**。在项目启动的时候,预加载类似“国家信息、货币信息、用户信息,新闻信息”等不是经常变更的数据
### 删除缓存重试机制
不管是**延时双删**还是**Cache-Aside的先操作数据库再删除缓存**,都可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了呀~ 所以可以引入**删除缓存重试机制**
![删除缓存重试流程](images/Solution/删除缓存重试流程.png)
### 缓存降级
1. 写请求更新数据库
2. 缓存因为某些原因,删除失败
3. 把删除失败的key放到消息队列
4. 消费消息队列的消息获取要删除的key
5. 重试删除缓存操作
缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
**降级的最终目的是保证核心服务可用,即使是有损的**。而且有些服务是无法降级的(如加入购物车、结算)。
### 读取biglog异步删除缓存
重试删除缓存机制还可以吧,就是会造成好多**业务代码入侵**。其实,还可以这样优化:通过数据库的**binlog来异步淘汰key**。
**分级降级预案:**
![读取biglog异步删除缓存](images/Solution/读取biglog异步删除缓存.png)
- **一般**:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级
- **警告**有些服务在一段时间内成功率有波动如在95~100%之间),可以自动降级或人工降级,并发送告警
- **错误**比如可用率低于90%,或数据库连接池被打爆,或访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
- **严重错误**:比如因为特殊原因数据错误了,此时需要紧急人工降级
以mysql为例
- 可以使用阿里的canal将binlog日志采集发送到MQ队列里面
- 然后通过ACK机制确认处理这条更新消息删除缓存保证数据缓存一致性

@ -1,141 +0,0 @@
<div style="color:#16b0ff;font-size:50px;font-weight: 900;text-shadow: 5px 5px 10px var(--theme-color);font-family: 'Comic Sans MS';">Subject</div>
<span style="color:#16b0ff;font-size:20px;font-weight: 900;font-family: 'Comic Sans MS';">Introduction</span>:收纳专题相关的知识总结!
[TOC]
# Redis
## 线程模型
Redis内部使用文件事件处理器File Event Handler这个文件事件处理器是单线程的所以Redis才叫做单线程的模型。它采用I/O多路复用机制同时监听多个Socket将产生事件的Socket压入到内存队列中事件分派器根据Socket上的事件类型来选择对应的事件处理器来进行处理。文件事件处理器包含5个部分
- 多个Socket
- I/O多路复用程序
- Scocket队列
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
客户端与redis的一次通信过程
![Redis请求过程](images/Subject/Redis请求过程.png)
- **请求类型1**`客户端发起建立连接的请求`
- 服务端会产生一个AE_READABLE事件IO多路复用程序接收到server socket事件后将该socket压入队列中
- 文件事件分派器从队列中获取socket交给连接应答处理器创建一个可以和客户端交流的socket01
- 将socket01的AE_READABLE事件与命令请求处理器关联
- **请求类型2**`客户端发起set key value请求`
- socket01产生AE_READABLE事件socket01压入队列
- 将获取到的socket01与命令请求处理器关联
- 命令请求处理器读取socket01中的key value并在内存中完成对应的设置
- 将socket01的AE_WRITABLE事件与命令回复处理器关联
- **请求类型3**`服务端返回结果`
- Redis中的socket01会产生一个AE_WRITABLE事件压入到队列中
- 将获取到的socket01与命令回复处理器关联
- 回复处理器对socket01输入操作结果如ok。之后解除socket01的AE_WRITABLE事件与命令回复处理器的关联
### 效率高
- `纯内存操作`数据存放在内存中内存的响应时间大约是100纳秒这是Redis每秒万亿级别访问的重要基础
- `非阻塞的I/O多路复用机制`Redis采用epoll做为I/O多路复用技术的实现再加上Redis自身的事件处理模型将epoll中的连接读写关闭都转换为了时间不在I/O上浪费过多的时间
- `C语言实现`:距离操作系统更近,执行速度会更快
- `单线程避免切换开销`:单线程避免了多线程上下文切换的时间开销,预防了多线程可能产生的竞争问题
## 数据结构
## 热点数据
## 双写一致性
如何保证缓存和数据库的双写一致性?
## 缓存问题
### 缓存雪崩
### 缓存击穿
### 缓存穿透
# Netty
## 线程模型
## 单机支持百万连接
## 长连接心跳保活机制
## 时间轮
# MySQL
## 数据结构
## 内存设计
## 索引设计
# RocketMQ
# Thread Pool
# Register
## 多数据中心方案
## 百万服务访问
# 问题定位
## CPU飙高
## 内存飙高
## 频繁GC
## 死锁
# 服务治理
## 限流
### 固定时间窗口
### 滑动时间窗口
### 漏桶算法
### 令牌桶算法
### 分布式限流
## 熔断
## 降级

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Loading…
Cancel
Save