Skip to content

服务端缓存之redis入门 #5

Description

@coderzzp

content

  1. 为什么使用服务端缓存
  2. 什么是redis
  3. redis相对memcache的优势
  4. 过期策略
  5. 内存淘汰机制
  6. 缓存更新策略
  7. 缓存穿透 & 缓存雪崩
  8. 是否需要redis

引言

起因:我们在做配置平台的时候,需要用node提供下发服务,而redis是我们在项目中使用的一项缓存技术,今天将知识点整理了分享给大家

1.为什么使用服务端缓存

更快

在日常对数据库的访问中,读操作的次数远超写操作,所以需要读的可能性是比写的可能大得多的。当我们使用SQL语句去数据库进行读写操作时,数据库就会去磁盘把对应的数据索引取回来,这是一个相对较慢的过程。

而服务端缓存因为通常都是放在内存中的,相比放在硬盘中的sql数据,它的速度要快的多,如果让请求去访问缓存中的数据,接口就会更快

image

更高并发

如图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。而缓存能够承受的请求量是远远大于直接访问数据库的,如果让缓存承担绝大部分的请求,系统就能承受更大的并发量

image

2.什么是redis

redis即为我们刚才所说的服务端缓存的一种

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API

有什么特点?

内存数据库

完全基于内存,绝大部分请求是纯粹的内存操作,非常快速

单线程

单线程就意味着它不需要处理线程间的通信问题,不需要频繁的切换上下文
传统意义下的多进程并发处理:如果一个I/O流进来,我们就开启一个进程处理这个I/O流。但是我们刚才说了:redis是单线程的, 那么他是如何处理多并发的场景呢?

采用io多路复用模型

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,系统首先将需要进行IO操作的socket添加到select中,然后select会帮我们监听每个注册的io流的状态。当有数据到达时,对应的socket被激活,用户线程读取数据并且继续执行

以上三点决定了redis是非常快的,官方数据redis的读速度达到了11w/s,写速度达到8w/s

3.redis相对于memcache的优势

支持快照和持久化

memcache是市面上另一个比较主流的key-value缓存数据库,memcache也是将数据存在内存当中的,但是它有一个问题,一旦它的服务器重启或者宕机,所有的数据都会消失,因为内存中的数据无法持久化;而redis与之相比的优势之一就是,它支持数据的快照和持久化。也就是说即使他的服务器在重启之后,也可以通过在硬盘中的rdb快照文件,实现数据恢复

支持master-slave主从复制

第二点,memcache如果说他的服务挂掉了,所有请求会瞬间涌向数据库,可能会导致数据库的宕机,而redis因为支持master-slave主从复制,当主节点出现问题时,可以有子节点提供服务,实现快速的故障恢复。并且redis的主从模式可以实现读写分离,由主节点提供写服务,子节点提供读服务,可以大大提高redis服务器的并发量
image

支持更加丰富的数据类型

  1. string:它是最常见的一种数据类型,redis可以完全实现memcache的功能,并且它还提供了额外的一些操作,例如:获取字符串长度,往字符串append内容 等等一些操作
  2. Hash:在memcaceh中,因为只支持string存储,所以对于结构化的信息例如用户信息,即使需要更改某一个属性的数据,也需要在存入的时候对整个用户信息执行一次序列化操作,而取出的时候进行一次反序列化操作,相当于js中的Json.stringfy和json.parse,这样对于性能来说是非常不友好的。而redis天然支持hash结构,即我们每次可以只更新hash中的某一个属性
  3. list: List就是链表,因为它是有序的,所以我们可以用来实现一些任务队列的功能
  4. set:无序的非重复集合,类似于在微博中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能
  5. Sorted set:有序的非重复集合,我们可以依靠它非常快速的实现一个类似于斗鱼 用户排行榜 的应用

4. 过期策略

为什么要有过期策略?

  1. 我们前面了解到因为redis基于内存的,而内存的空间大小是非常昂贵的,所以redis提供了一种过期策略,会按照他的策略定期删除一些数据以保证内存空间的大小
  2. 保证数据的最终一致性,简单来说在缓存使用当中,数据库中的数据有极小的可能是和缓存中的数据不一致的,设置过期策略能够让缓存在一定时间内失效,而不是一直处于一个错误状态

redis的过期删除策略: 定期删除+惰性删除

定期删除:Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 Key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 Redis 存了几十万个 Key ,每隔 100ms 就遍历所有的设置过期时间的 Key 的话,就会给 CPU 带来很大的负载
定期删除可能会导致很多过期 Key 到了时间并没有被删除掉。所以就有了惰性删除。什么是惰性删除呢,假如你的过期 Key,靠定期删除没有被删除掉,还停留在内存里,当你的系统去查一下那个 Key,才会被 Redis 给删除掉。这就是所谓的惰性删除

但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 Key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?
如果大量过期 Key 堆积在内存里,导致 Redis 内存块耗尽了。怎么解决这个问题呢?

5. 内存淘汰机制

maxmemory-policy:可以设置当内存不足以容纳新写入数据时,采取什么策略来移除数据。

  • noeviction:新写入操作会报错。
  • allkeys-lru:在键空间中,移除最近最少使用的key。(推荐)
  • allkeys-random:在键空间中,随机移除某个key
  • volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。

看起来6个选择是很难抉择的,但其实是有一个选择原则的,就是尽量让你在缓存中保留的数据都是热点数据,这样能够让大量的请求落在缓存上,最大程度的发挥缓存的作用,

6.缓存更新策略

缓存”读“的过程

数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象放进缓存,那么下次请求时就能直接从缓存中取数据
image

但是如果要更新数据就涉及到一个问题:先更新数据库还是先删除缓存,看似没有差别,但在一些具体场景下会导致一些问题

方式一:先删除缓存,再更新数据库

同时有两个请求,一个更新一个查询,在如下场景下,发生了数据库和缓存中的数据不一致
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
image

解决方式:延时双删

// 一个写操作
async update(data) {
    // 先删除缓存
    await redis.delKey(key);
    // 更新数据库
    await db.updateData(data);
    // 休眠一秒,等B将旧值写入缓存
    await Thread.sleep(1000);
    // 再次删除,将B的脏数据删掉
    await redis.delKey(key);
}

image

方式二:先更新数据库,再删除缓存

思考如下场景:
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存

image
但是这种情况发生的可能性是很小的:发生上述情况有一个先天性条件,就是步骤(2)的写数据库操作比步骤(1)的读数据库操作耗时更短,才有可能使得步骤(3)先于步骤(4),而数据库的读操作的速度是远快于写操作的,所以我们推荐**先更新数据库,再删除缓存 **

7.1 缓存穿透

什么是缓存穿透?

image
如果数据库查询对象为空,则不放进缓存。这就会导致一个问题,如果一个用户他一直请求不存在的数据,那么每次请求都会穿过缓存,去请求数据库,这就是所谓的缓存穿透。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库

解决方式一:

我们可以将所有可能存在的key放到这个布隆过滤器当中,当请求进来时,先用布隆过滤器判断是否在所有可能的合法的key当中,如果不存在,即为非法请求,直接返回错误
image

解决方式二:

第二种方式比较粗暴,设置空缓存:当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取这个空对象了。但可能会遇到这样一种情况,你使用了写入空缓存的策略,而黑客不断的去创造不存在的key去请求你的接口,这个时候你的缓存中就会存在大量的空数据,会占用你的缓存空间。所以说如果要使用第二种方式,建议将空缓存的过期时间设置得短些,让这些空缓存在一定的时间后被删除掉
image

7.2 缓存雪崩

什么是缓存雪崩?

如果缓存数据设置的过期时间是相同的,并且在同一时间设置了大量的redis数据,那么这些数据会在同一时间同时失效,这就会导致在这段时间内,全部请求到数DB中,DB瞬间压力过大导致雪崩。举个例子:比方说双十二的时候,12点会迎来一波抢购的高峰期,我们在同一时间比较集中的放了一批商品的信息到缓存里,假设缓存一个小时,那么到了凌晨一点的时候,这批缓存数据就会同时失效,对于这批商品的访问查询,都会落到数据库上,数据库的压力有可能会瞬间增大导致宕机

解决方式:

在缓存的时候给过期时间加上一个随机值,尽可能分散缓存过期时间,这样就会大幅度的减少缓存在同一时间过期。

8.是否需要redis

  1. 数据访问频率高:业务数据常用么,数据访问频率大么?试想如果数据的使用频率非常高,例如热搜排行榜等数据,那么缓存能替我们挡下大量的sql执行,提升整个系统的并发能力。
  2. 读写比例大:数据的读写比例怎么样?如果读大于写,例如用户信息,再比如我们的配置数据,读的次数是远大于写的,这就会让缓存长时间的存在于缓存中,而不会因为频繁的写操作频繁导致缓存失效。
  3. 对数据一致性的要求不高: ,我们之前也了解到数据库中的数据和缓存中的数据可能会存在不一致的问题,尽管概率很小,但如果是例如 金额交易等敏感数据,建议还是不要使用缓存。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions