深入浅出redis缓存应用
0.1、索引
https://blog.waterflow.link/articles/1663169309611
1、只读缓存
只读缓存的流程是这样的:
当查询请求过来时,先从redis中查询数据,如果有的话就直接返回。如果没有的话,就从数据库查询,并写入到缓存中。
当删改请求过来时,会直接从数据库中删除修改数据,并把redis中保存的数据删除。
这样做的好处是,所有最新的数据都在数据库中,而数据库是有数据可靠性保障的。
2、读写缓存
读写缓存的流程是这样的:
- 当查询请求过来时,先从redis中查询数据,如果有的话就直接返回。如果没有的话,就从数据库查询,并写入到缓存中。
- 当增删改请求过来时,得益于Redis的高性能访问特性,数据的增删改操作可以在缓存中快速完成,处理结果也会快速返回给业务应用,这就可以提升业务应用的响应速度。
- 但是和只读缓存不同的是,最新的数据都是在redis中,一旦出现掉电宕机,由于redis的持久化机制,最新的数据有可能会丢失,就会给业务带来风险。
所以,根据业务应用对数据可靠性和缓存性能的不同要求,我们会有同步直写和异步写回两种策略。其中,同步直写策略优先保证数据可
靠性,而异步写回策略优先提供快速响应。
2.1、同步直写
当增删改请求过来时,请求到redis的同时,也会请求mysql,等到redis和mysql都写完数据才会返回数据。
这样,即使缓存宕机或发生故障,最新的数据仍然保存在数据库中,这就提供了数据可靠性保证。
但是也会降低缓存的使用性能,因为写缓存很快,但是写数据库就要慢很多,整个的响应时间就会增加。
2.2、异步写回
异步写回优先考虑了响应速度,写到缓存会立即响应客户端。等到数据要从redis中淘汰时,再同步到mysql。
但是如果发生掉电,数据还是没有写到mysql,还是有丢失的风险。
3、如何选择
- 如果需要对写请求进行加速,我们选择读写缓存;
- 如果写请求很少,或者是只需要提升读请求的响应速度的话,我们选择只读缓存。
4、关于一致性
- 对于读写缓存的异步写回,由于是只写redis,淘汰时才会写入mysql,如果发生宕机不能保证一致性
- 对于读写缓存的同步写回,由于redis和mysql是同时写,需要加入事物机制,要么都执行要么都不执行,可以保证一致性。(问题:如何保证原子性?当有并发写过来时即使都执行了也可能会不一致,这是就要引入锁保证互斥性)
- 对于只读缓存,如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。由于redis和mysql是同时操作,需要加入事物机制,要么都执行要么都不执行,可以保证一致性。(问题:如何保证原子性?)
4.1、对于只读缓存的一致性问题
先删除缓存,再更新数据库
- 如果缓存删除成功,但是数据库更新失败,那么,应用再访问数据时,缓存中没有数据,就会发生缓存缺失。然后,应用再访问数库,但是数据库中的值为旧值,应用就访问到旧值了。
- 如果线程A都成功了,但是同时另一个线程B在线程A的这俩个请求中间过来。这个时候缓存已经删除,但是数据库还是旧值,线程B发现没有缓存,就从数据库读读取了旧值更新到redis中,然后线程A把新值更新到数据库。此时redis中是旧值,mysql中是新值。
先更新数据库,再删除缓存中的值
- 如果应用先完成了数据库的更新,但是,在删除缓存时失败了,那么,数据库中的值是新值,而缓存中的是旧值,这肯定是不一致的。这个时候,如果有其他的并发请求来访问数据,按照正常的缓存访问流程,就会先在缓存中查询,但此时,就会读到旧值了。
- 如果线程A删除了数据库中的值,但还没来得及删除缓存值,线程B就开始读取数据了,那么此时,线程B查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程A一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。(可以理解为最终一致性,读到旧数据只是暂时的,最终都会读到新数据)
所以一般项目中使用只读缓存,先更新数据库,再删除缓存。这样的代价是最小的,而且尽量保证了一致性。
5、缓存异常
5.1、缓存雪崩
缓存雪崩是指,大量的请求无法在redis中处理(redis没拦住),直接打到了mysql,导致数据库压力激增,甚至服务崩溃。
redis无法处理的原因有两种:
缓存中大量数据同时过期
解决方案:
- 给过期时间增加一个较小的随机数,过期的数据通过时间去分摊
- 服务降级,直接返回错误信息
Redis缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层
解决方案:
- 服务熔断或者请求限流,redis客户端直接返回,不会请求到redis服务,但是影响范围比较大
- 构建redis集群,提高可用性
5.2、缓存击穿
缓存击穿是指,访问某个热点数据,无法在缓存中处理,大量请求打到mysql,导致数据库压力激增,甚至服务崩溃。
解决方案:
- 对于频繁访问的热点数据不设置过期时间
5.3、缓存穿透
缓存穿透是指,要访问的数据既不在redis中,也不在mysql中。请求redis发现数据不存在,继续访问mysql发现数据还是不存在,然后也无法写回缓存,下次继续请求的时候还是会打到mysql。
解决方案:
- 缓存空值或者缺省值
- 使用布隆过滤器
布隆过滤器
布隆过滤器由一个初值都为0的bit数组和N个哈希函数组成,可以用来快速判断某个数据是否存在(准确说是判断不存在,如果布隆过滤器不存在数据库中一定不存在,如果布隆过滤器判断存在,数据库不一定存在,这是布隆过滤器的机制决定的)。当我们想标记某个数据存在时(例如,数据已被写入数据库),布隆过滤器会通过三个操作完成标记:
- 首先,使用N个哈希函数,分别计算这个数据的哈希值,得到N个哈希值。
- 然后,我们把这N个哈希值对bit数组的长度取模,得到每个哈希值在数组中的对应位置。
- 最后,我们把对应位置的bit位设置为1,这就完成了在布隆过滤器中标记数据的操作。
如果数据不存在(例如,数据库里没有写入数据),我们也就没有用布隆过滤器标记过数据,那么,bit数组对应bit位的值仍然为0。
所以当我们写入数据库时,使用布隆过滤器做个标记。当缓存缺失后,应用查询数据库时,可以通过查询布隆过滤器快速判断数据是否存在。如果不存在,就不用再去数据库中查询了。
6、应用场景
我们看下go-zero中是如何使用缓存的,go-zero中使用的只读缓存,当数据有更新删除操作的时候,redis中的对应Primary记录和查询条件记录会同步删除。go-zero中对某行的缓存,会缓存主键到行记录的缓存,和查询条件(唯一索引)到主键的缓存
我们看下查询的逻辑(针对的是单行的记录):
- 通过查询条件查询某条记录时,如果没有查询条件到主键的缓存
- 通过查询条件到mysql查询行记录,然后把主键到行记录的缓存,和查询条件(唯一索引)到主键的缓存更新到redis(前者的过期时间会多余后者几秒时间)
- 继续回到1,如果有查询条件到主键的缓存,如果没有主键到记录的缓存,通过主键到mysql查询并写入redis
下面看下go-zero源码:
// v - 需要读取的数据对象
// key - 缓存key
// query - 用来从DB读取完整数据的方法
// cacheVal - 用来写缓存的方法
func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) error,
cacheVal func(v interface{}) error) error {
// singleflight一批请求过来,只允许一个去真正访问数据,防止缓存击穿
val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
// 从cache里读取数据
if err := c.doGetCache(key, v); err != nil {
// 如果是预先放进来的placeholder(用来防止缓存穿透)的,那么就返回预设的errNotFound
// 如果是未知错误,那么就直接返回,因为我们不能放弃缓存出错而直接把所有请求去请求DB,
// 这样在高并发的场景下会把DB打挂掉的
if err == errPlaceholder {
return nil, c.errNotFound
} else if err != c.errNotFound {
// why we just return the error instead of query from db,
// because we don't allow the disaster pass to the DBs.
// fail fast, in case we bring down the dbs.
return nil, err
}
// 请求DB
// 如果返回的error是errNotFound,那么我们就需要在缓存里设置placeholder,防止缓存穿透
if err = query(v); err == c.errNotFound {
if err = c.setCacheWithNotFound(key); err != nil {
logx.Error(err)
}
return nil, c.errNotFound
} else if err != nil {
// 统计DB失败
c.stat.IncrementDbFails()
return nil, err
}
// 把数据写入缓存
if err = cacheVal(v); err != nil {
logx.Error(err)
}
}
// 返回json序列化的数据
return jsonx.Marshal(v)
})
if err != nil {
return err
}
if fresh {
return nil
}
// got the result from previous ongoing query
c.stat.IncrementTotal()
c.stat.IncrementHit()
// 把数据写入到传入的v对象里
return jsonx.Unmarshal(val.([]byte), v)
}
从上面代码我们可以看到:
- 使用sigleflight防止缓存击穿
- 缓存穿透,使用了占位符,即在redis中保存一个空值
深入浅出redis缓存应用的更多相关文章
- 10.27-Redis-mz 深入浅出Redis
深入浅出Redis 1.Redis的发展史 Redis[Remote Directory Server]:远程服务器字典 2.下载安装Redis 1>Linux下安装Reids ...
- 缓存工厂之Redis缓存
这几天没有按照计划分享技术博文,主要是去医院了,这里一想到在医院经历的种种,我真的有话要说:医院里的医务人员曾经被吹捧为美丽+和蔼+可亲的天使,在经受5天左右相互接触后不得不让感慨:遇见的有些人员在挂 ...
- Windows下Redis缓存服务器的使用 .NET StackExchange.Redis Redis Desktop Manager
Redis缓存服务器是一款key/value数据库,读110000次/s,写81000次/s,因为是内存操作所以速度飞快,常见用法是存用户token.短信验证码等 官网显示Redis本身并没有Wind ...
- 总结:如何使用redis缓存加索引处理数据库百万级并发
前言:事先说明:在实际应用中这种做法设计需要各位读者自己设计,本文只提供一种思想.准备工作:安装后本地数redis服务器,使用mysql数据库,事先插入1000万条数据,可以参考我之前的文章插入数据, ...
- .NET基于Redis缓存实现单点登录SSO的解决方案[转]
一.基本概念 最近公司的多个业务系统要统一整合使用同一个登录,这就是我们耳熟能详的单点登录,现在就NET基于Redis缓存实现单点登录做一个简单的分享. 单点登录(Single Sign On),简称 ...
- Redis缓存连接池管理
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.util.Assert;import ...
- ssm+redis 如何更简洁的利用自定义注解+AOP实现redis缓存
基于 ssm + maven + redis 使用自定义注解 利用aop基于AspectJ方式 实现redis缓存 如何能更简洁的利用aop实现redis缓存,话不多说,上demo 需求: 数据查询时 ...
- Windows Azure Redis 缓存服务
8月20日,Windows Azure (中国版)开始提供Redis缓存服务,比较国际版的Microsoft Azure晚了差不多一年的时间.说实话,微软真不应该将这个重要的功能delay这么长时间, ...
- .NET基于Redis缓存实现单点登录SSO的解决方案
一.基本概念 最近公司的多个业务系统要统一整合使用同一个登录,这就是我们耳熟能详的单点登录,现在就NET基于Redis缓存实现单点登录做一个简单的分享. 单点登录(Single Sign On),简称 ...
随机推荐
- React生命周期和响应式原理(Fiber架构)
注意:只有类组件才有生命周期钩子函数,函数组件没有生命周期钩子函数. 生命周期 装载阶段:constructor() render() componentDidMount() 更新阶段:render( ...
- 744. 寻找比目标字母大的最小字母--LeetCode
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/find-smallest-letter-greater-than-target 著作权归领扣网络所有. ...
- Word 分页符怎么使用
当一页内容输入完之后,还留有很多空白区域没有填写,一直按回车键跳转到下一页显得复杂,并且回车键经过的地方都是段落. 可以手动添加分页符,使当前页跳转到下一页. 也可以使用快捷键Ctrl + Enter ...
- Excel 统计函数(二):COUNTIF 和 COUNTIFS
COUNTIF [语法]COUNTIF(range, criteria) [作用]range 为统计的范围,criteria 是统计的条件. [题目]统计 A1 到 A10 范围内,出现"你 ...
- springboot中,子项目的boot依赖全部爆红
应仔细检查父项目的dependencyManagement是否指定了打包方式<type>为pom,<scope>为import
- [ARC119E] Pancakes (二维偏序,分类讨论)
题面 一个长为 N N N 的序列 S S S ,最多翻转序列中一个区间,最小化 ∑ i = 2 N ∣ S i − S i − 1 ∣ \sum_{i=2}^{N}|S_i-S_{i-1}| i=2 ...
- 【java】学习路径45-多线程-线程生命周期
线程分为五大状态:新建.就绪.运行.阻塞.死亡. New,Runnable,Running,Blocked,Terminated. 新建状态(New: 创建好一个系统对象,在调用start()之前,线 ...
- NetCore性能优化
NetCore性能优化2.非跟踪查询在只读方案中使用结果时,非跟踪查询十分有用,可以更快速地执行.增加AsNoTracking()表示非跟踪,如:var users = context.User.As ...
- 第九章 kubectl命令行工具使用详解
1.管理k8s核心资源的三种基础方法 陈述式管理方法:主要依赖命令行CLI工具进行管理 声明式管理方法:主要依赖统一资源配置清单(manifest)进行管理 GUI式管理方法:主要依赖图形化操作界面( ...
- Windows下使用SSH连接到旧设备
正好今天遇到一个旧设备有点问题,需要通过SSH的方式连接上去检查.Windows 10自带了SSH命令,可以直接连接而不必寻求其它工具的支持了.如果看不到图,请点我. 结果发现无法连接,显示协商错误. ...