什么是redis
redis是一个开源的内存数据结构存储,用作数据库 缓存 消息代理和流引擎
可以对这些数据结构进行原子操作
redis使用内存中的数据集来到达最高性能,可以定期将数据集转储到磁盘或者将每个命令附加到基于磁盘的日志
redis数据结构
使用场景
- string:缓存对象 常规计数 分布式锁 共享session
- list:消息队列(生产者需要自行实现全局唯一id)
- hash:缓存对象 购物车等
- set:聚合计算(并集 交集 差集)场景,比如点赞 共同关注 抽奖活动等
- zset:排序场景,比如排行榜 电话和姓名排序等
怎么实现的
string:主要是sds(简单动态字符串),sds和我们认识的c字符串不太一样,相比于c字符串多了:
- sds不仅可以保存文本数据,还可以保存二进制,比如音频 视频
- 获取字符长度时间复杂度o1
- redis的sds api是安全的,拼接字符串不会造成缓冲区溢出
list:双向链表或者压缩链表实现的
- 如果列表的元素个数小于512个且每个元素值都小于64字节,redis会使用压缩链表
- 否则双向链表
hash:压缩链表或哈希表,规则同上
set:哈希表或整数集合
zset:压缩链表或跳表
- 元素个数小于128个且每个元素的值小于64字节时,使用压缩列表
- 否则跳表
redis线程模型
redis单线程指什么
接收客户端请求 解析请求 进行数据读写等操作 发送数据给客户端 这个过程是由一个线程(主线程完成的)
但并非是只有单线程,会启动后台线程进行关闭文件 aof刷盘 异步释放redis内存
基于io多路复用的单线程模型
为什么使用单线程
- 都在内存里面操作,所以瓶颈不在cpu,而是内存和带宽
- 避免多线程之间的竞争
- 采用io多路复用机制
持久化
redis读写都是在内存里面,当机器重启或者宕机后,内存数据全部丢失,所以需要一个持久化方式
有三个持久化方式
- AOF日志:每执行一条写命令就把该命令追加到一个文件中
- RDB快照:将某一时刻的内存数据以二进制方式写入磁盘
- 混合持久化方式:redis新增的方式,集成了AOF和RDB的优点
redis使用的过期删除策略是什么
redis对已经过期的键值对进行删除的策略
我们对redis key设置了ttl后会把该key带有的过期时间存储到一个过期字典,也就是说过期字典存储了所有key的过期时间
当我们查询一个key的时候redis会首先去字典中看key是否存在该过期字典中:
如果不在就就正常读取键值
如果在就比对过期时间,根据结果判定是否过期
redis使用的过期删除策略是惰性删除+定期删除
惰性删除的做法是,不主动删除过期键,每次数据库访问key的时候都检测key是否过期,如果过期就删除该key
优点:每次需要访问该资源的时候才会去检测,只会使用很小的系统资源,于是对cpu最友好
缺点:对key删除不及时,可能造成不必要的内存浪费,于是对内存不友好
定期删除的做法是每隔一段时间就随机从数据库中取出一定数量的key进行检查,并删除其中的过期key,是一个循环过程,如果过期key比例超过一定值就重复一次,有超时时间
优点:通过限制删除操作的执行时长与频率来减少删除操作对cpu的影响,同时页能删除一部分过期的数据减少了过期键对内存的无效占用
缺点:难以确定时长与频率,如果太频繁就会对cpu影响不好,太少又对内存不好
redis的内存淘汰
当redis内存达到某个阈值就会触发内存淘汰机制,这个阈值时我们设置的最大运行内存
内存淘汰策略有哪些
不进行数据淘汰的策略
达到阈值就不提供服务,直接返回错误
进行数据淘汰的策略
- volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早的过期的键值
- volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值
redis缓存设计
缓存雪崩
问题描述
大量缓存在同一时间过期,如果此时有很多用户请求,都无法在redis中处理,就会全部打进数据库,从而在极短时间内造成数据库压力
解决方案
缓存失效时间打散,增加一个随机时间值
设置缓存不过期(敢这样写明天准备收拾工位了)
缓存击穿
问题描述
热点数据过期,大量请求打进数据库
解决方案
互斥锁方案,保证同一时间只有一个请求打进数据库,然后回填缓存
缓存穿透
问题描述
访问的数据既不在缓存也不在数据库,请求永远无法拿到缓存,造成数据库压力倍增。一般要么是业务误操作,把数据库里面的值删掉了,要么是黑客恶意攻击,故意大量访问某些不存在的数据库业务
解决方案
- api网关处就开始检验请求是否合法,参数是否正常,拦截同一ip短时间大量请求
- 回填缓存设置空值
- 布隆过滤器
如何设置一个策略可以做到动态缓存热点数据
组合:多级缓存+自动淘汰+热点识别
多级缓存
- 本地缓存(进程内):go的map+lru
- 分布式缓存:redis
访问路径:请求 本地缓存 redis db
效果:热点数据直接命中本地,延迟最低
自动过期+lru淘汰
- 设置ttl
- redis使用lru或者lfu
关键点:热点数据访问频繁,自然可以存活更久。冷数据自动淘汰
热点识别(核心)
根据访问频率来判断热点
方法A:简单计数
redis记录key的访问次数,超过阈值就标记为热点加入本地缓存
方法B:滑动窗口
记录最近N喵访问次数,使用sorted set或者ring buffer
热点数据提升策略
一旦识别为热点:
- 放进本地缓存(内存级)
- 延长ttl
- 主动预热
防缓存击穿(必须)
热点key过期会瞬间打爆DB:
- 互斥锁
防缓存穿透
- 空值缓存
- 布隆过滤器
动态效果总结
访问频率变大 自动进入本地缓存 ttl变长
访问频率下降 ttl到期自动淘汰
热点自动浮现,无需人工干预
缓存更新策略
旁路缓存
写策略
先更新数据库中的数据,再删除缓存中的数据
读策略
- 如果读取的数据命中了缓存,则直接返回数据
- 如果读取的数据没有命中缓存就从数据库中读数据然后回填
注意:不能先删缓存再更新数据库,原因是在读+写并发的时候会出现缓存和数据库数据不一致的情况
why:
考虑这样一个场景:请求A先删缓存,紧接着请求B读缓存,未命中则取数据库旧值回填缓存,B操作完后A去更新数据库,这样数据库就是新值而缓存是旧值
但是实际上旁路缓存也会有一个问题:请求A去读取缓存未命中,读到数据库旧值,然后B去更新数据库,删除缓存,B操作完后A就去回填缓存,这时候的缓存是旧值,也会出现数据不一致的情况,但是这种情况实际上是很小可能发生的,因为回填缓存速度极快,很难在中间插入一段更新数据库和删除缓存的操作
读穿/写穿策略
读穿
查询有没有缓存,有就直接返回,没有就由缓存组件去数据库拉数据然后写入缓存组件,返回结果
写穿
当有数据更新的时候就
如果缓存里面有就用先更新缓存,缓存组件同步更新数据库,缓存组件告知更新完成
如果缓存没有,就直接更新数据库然后返回结果
实际上这种方式用的不多,因为无论是redis还是memcached都不提供写入数据库与和自动载入数据库的功能。只能在本地缓存的时候用用
写回策略
写回就是更新数据的时候只更新缓存,同时把缓存数据设置为脏数据,然后立即返回,并不会更新数据库。对于数据库的更新是异步批量进行
实际上redis也没有异步更新数据库的功能,所以我们平时开发的时候也一般不用这个,但是诸如cpu缓存 操作系统的文件缓存都采用了写回策略
写回策略特别适合写多的场景,但是数据不是强一致性的,而且有数据丢失的风险
redis实战
延迟队列
场景:
- 淘宝等购物平台里面下单,超过一定时间没有支付就会取消订单
- 打车没有司机接单就会取消订单并且提醒暂时没有车主接单
- 外卖商家不接单就会自动取消订单
redis里面可以通过zset实现,用score存储延迟执行的时间
生产者往zset里面add元素,消费者通过zrangebyscore来查询符合条件的所有待处理任务
大key处理
什么是redis里的大key
大key指key的value很大
一般来说下面两种情况可以叫大key:
- string类型的值大于10kb
- hash list set zset类型元素超过5000个
大key带来的问题是什么
- 客户端超时阻塞:redis是单线程,所以处理大key比较耗时的时候,在客户端看来就是长时间没有响应
- 引发网络阻塞:每次获取大key的时候产生的网络流量很大
- 阻塞工作线程:删除一个大key的时候,回阻塞工作线程,这样就没办法处理后续命令
怎么找到redis里面的大key
redis-cli –bigkeys查找大key
使用scan命令查找大key
使用rdbTools工具查找大key
如何删除大key
删除实际上是在主线程进行,本质上是释放键值对占用的内存空间,然后把空闲的内存块插入到一个空闲内存块的链表,这个过程本身需要一定时间而且回阻塞当前释放内存的应用程序
所以一下子释放大量内存就会导致阻塞,其他请求也会阻塞甚至超时
删除大key实际上是一个危险的动作,做法可以这样做:
分批次删除
就是一个大key每次删一点,分批次删完
异步删除
unlink命令来代替del删除
这样redis就会把这个key放进一个异步线程中机型删除,这样不会阻塞主线程
redis管道
pipeline实际上是客户端提供的一种批处理技术,用于一次执行多个redis命令,从而提高整个交互的性能
使用管道可以减少网络时延,但是也要注意防止一次性发太多命令或者命令过大导致网络阻塞