聊聊缓存Redis的那些事

1.缓存

1.1 为什么用缓存

对于交互的要求,曾经听过一句话:“在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。另外,超过一弹指的耗时操作要有进度提示,并且可以随时中止或取消,这样才能给用户最好的体验。”
那么瞬间、刹那、一弹指具体是多少时间呢?根据《摩诃僧祗律》记载:
一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。
那么,经过周密的计算,一瞬间为0.36 秒,一刹那有 0.018 秒.一弹指长达 7.2 秒。
为了达到这个响应时间要求,所有的数据都走服务器获取显然是满足不了要求的。前端对于静态资源和图片的缓存,cdn和网关对于服务器地址的缓存,服务器对于数据的缓存,nosql对于数据库的缓存对于现如今大流量高并发的业务场景具有很好的支撑作用

1.2 缓存应用场景

  • 热点数据(经常会被查询,但是不经常被修改或者删除的数据)
  • 排行榜,使用有续集,sorted set
  • 最新列表,LPUSH命令构建List,一个个顺序都塞进去就可以啦
  • 秒杀系统,基于redis是单线程特征,防止出现数据库“**”
  • 全局增量ID生成,类似“秒杀”(当然最好的是使用全局序列id生成服务)
  • 分布式锁,比如http防重复请求校验

2.Redis

2.1 Redis简介

Redis是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器

2.2 Redis优点

  • 支持数据类型丰富,不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
  • Redis操作速度快,基于内存,单线程
  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
  • Redis支持数据的备份,即master-slave模式的数据备份
  • 支持事务

2.3 Redis数据类型

聊聊缓存Redis的那些事

2.4 I/O多路复用

I/O多路复用机制是一种非阻塞的io模型,在netty和reactor模型中都有广泛的应用。我们的redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有一段I/O多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。
需要说明的是,这个I/O多路复用机制,redis还提供了select、epoll、evport、kqueue等多路复用函数库,也就是不同的selector模型。具体的后续会重新开一篇文章重点介绍I/O多路复用模型。

2.5 Redis过期策略以及内存淘汰机制

2.5.1 Redis过期策略

  • 分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?
  • 回答:redis采用的是定期删除+惰性删除策略

2.5.2 为什么不用定时删除策略?

定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

2.5.3 该策略底层的工作原理

  • 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
  • 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

2.5.4 为什么还需要内存淘汰机制

定期删除+惰性删除也是有问题的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。这就是为什么我们需要内存淘汰机制。
在redis.conf中有一行配置maxmemory-policy volatile-lru,该配置就是配内存淘汰策略的,常见的类型有以下几种:

  • 1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
  • 2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
  • 3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
  • 4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
  • 5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
  • 6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐
    ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

3.缓存问题及解决方案

3.1 缓存雪崩&解决方案

3.1.1 概念

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩

3.1.2 解决方案

  • 可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效;
  • 采用限流算法,限制流量;
  • 采用分布式锁,加锁访问。

3.2 缓存穿透&解决方案

3.2.1 概念

访问一个一定不存在的数据,这将导致这个不存在的数据每次请求都要查询数据库,在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。比如这条数据在数据库本来就不存在,我们查询不到也就不会放到缓存中,这种就是有风险的。

3.2.2 解决方案

  • 采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;
  • 访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。

3.3 缓存击穿&解决方案

3.3.1 概念

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果缓存失效了,超高并发的访问也会同时落到数据库,导致DB瞬时压力过重。这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key

3.3.2 解决方案

我们的目标是:尽量少的线程构建缓存(甚至是一个) + 数据一致性 + 较少的潜在危险。

  • 使用互斥锁(mutex key): 这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了;
  • 初始化加载并且永远不过期