Redis总结

Redis

redis 简介

简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

为什么要用 redis / 为什么要用缓存

主要从 “高性能” 和“高并发”这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

img

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

img

为什么要用 redis 而不用 map/guava 做缓存?

下面的内容来自 segmentfault 一位网友的提问,地址:https://segmentfault.com/q/1010000009106416

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached 服务的高可用,整个程序架构上较为复杂。

拓展:

  1. Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了
  2. Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了
  3. Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里
  4. Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象
  5. Redis 缓存有过期机制,Map 本身无此功能
  6. Redis 有丰富的 API,Map 就简单太多了

redis 的线程模型

参考地址:https://www.javazhiyin.com/22943.html

redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

Redis通过多路复用模型监听多个Socket,是阻塞的;当监听到有网络请求时,将相应的事件添加进队列,文件时间处理器每次从中取一个事件(也是单线程的),由于是内存操作,所以响应极快,单线程保证了线程安全,若采用多线程会产生锁同步的开销、上下文的切换

redis 和 memcached 的区别

对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!

  1. redis 支持更丰富的数据类型(支持更复杂的应用场景):Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。memcache 支持简单的数据类型,String。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用, 而 Memecache 把数据全部存在内存之中(不落地)
  3. 集群模式:memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
  4. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。

Memcache:代码层次类似Hash

  • 支持简单数据类型
  • 不支持持久化存储
  • 不支持主从
  • 不支持分片(数据分片存储)

Redis

  • 数据类型丰富
  • 支持持久化存储
  • 支持主从
  • 支持分片(数据分片存储)

来自网络上的一张图,这里分享给大家!

img

redis 常见数据结构以及使用场景分析

1.String

常用命令: set,get,decr,incr,mget 等。

String 数据结构是简单的 key-value 类型,value 其实不仅可以是 String,也可以是数字。 常规 key-value 缓存应用; 常规计数:微博数,粉丝数等。

2.Hash

常用命令: hget,hset,hgetall 等。

hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:

1
2
3
4
5
6
7
key=JavaUser293847
value={
“id”: 1,
name”: “SnailClimb”,
“age”: 22,
location”: “Wuhan, Hubei”
}

3.List

常用命令: lpush,rpush,lpop,rpop,lrange 等

list 就是链表,Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

4.Set

常用命令: sadd,spop,smembers,sunion 等

set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。

当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:

1
sinterstore key1 key2 key3     将交集存在key1内

5.Sorted Set

常用命令: zadd,zrange,zrem,zcard 等

和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

redis 设置过期时间

Redis 中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。

如果假设你设置了一批 key 只能存活 1 个小时,那么接下来 1 小时后,redis 是怎么对这批 key 进行删除的?

定期删除 + 惰性删除。

通过名字大概就能猜出这两个删除方式的意思了。

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

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

redis 内存淘汰机制 (MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?)

redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看: http://download.redis.io/redis-stable/redis.conf

redis 提供 6 种数据淘汰策略:

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最久使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu:从已设置过期时间的数据集 (server.db[i].expires) 中挑选最不经常使用的数据淘汰
  2. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

每个算法都有自己的应用场景以及优缺点。各种缓存算法的核心区别在于它的淘汰机制。而这个淘汰机制主要参考这两个维度:最后被访问的时间和最近被访问的频率次数。

LRU(Least Recently Used ):淘汰最后被访问时间最久的元素。(时间上最久未使用)

缺点:可能会由于一次冷数据的批量查询而误导大量热点的数据。(丢失热点数据)

LFU(Least Frequently Used):淘汰最近访问频率最小的元素。(频率上最少使用)

缺点:1. 最新加入的数据常常会被踢除,因为其起始方法次数少。 2. 如果频率时间度量是1小时,则平均一天每个小时内的访问频率1000的热点数据可能会被2个小时的一段时间内的访问频率是1001的数据剔(丢失新增数据)

redis 持久化机制 (怎么保证 redis 挂掉之后再重启数据可以进行恢复)

很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。

Redis 不同于 Memcached 的很重一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。

快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存里面的全量数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:(会覆盖旧的RDB文件)

1
2
3
4
5
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

手动触发RDB持久化的方式

  • SAVE:阻塞Redis的服务器进程(此线程为主线程,处理请求),直到RDB文件被创建完毕
  • BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程;创建成功后返回指令,主线程接收
    • lastsave:可以查询上一次存储的时间戳,据此,可以将某个时间点的数据文件进行备份

BGSAVE原理

1584780562269

  • 系统调用fork:创建进程,实现了Copy-on-Write(COW)

Copy-on-Write:如果有多个调用者同时要求相同资源(内存/磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真真复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变

缺点:

  • 内存数据的全量同步,影响I/O性能
  • 可能因为Redis挂掉而丢失从当前至最近一次快照期间的数据

自动化触发RDB持久化的方式

  • 根据redis.conf配置里的save m n定时触发(BGSAVE)
  • 主从复制时,主节点自动触发
  • 执行Debug Reload
  • 执行Shutdown且没有开启AOF持久化

AOF(append-only file)持久化

与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

1
appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

1
2
3
#appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
#appendfsync no #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

Redis之AOF重写及其实现原理

补充内容:AOF 重写

AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小(命令进行了重写,将冗余的命令合并成一条

AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。

在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新旧AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致以及保证数据的安全性(旧文件也更新)。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作

1584783317904

Redis 4.0 对于持久化机制的优化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

  • BGSAVE做镜像全量持久化,AOF做增量持久化(开启通道记录内存缓冲区新增的数据);这样就将RDB与AOF文件保存在一起了

redis 事务

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务 (transaction) 功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。

补充内容:

redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。(来自 issue: 关于 Redis 事务不是原子性问题

缓存雪崩和缓存穿透问题解决方案

缓存雪崩

什么是缓存雪崩?

简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

有哪些解决办法?

(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):

  • 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
  • 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免 MySQL 崩掉
  • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存

img

断路器Hystrix全面解析

缓存穿透

什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。下面用图片展示一下 (这两张图片不是我画的,为了省事直接在网上找的,这里说明一下):

正常缓存处理流程:

img

缓存穿透情况处理流程:

img

一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 show variables like '%max_connections%';命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。

有哪些解决办法?

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:SET key value EX 10086。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

另外,这里多说一嘴,一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值

如果用 Java 代码展示的话,差不多是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object getObjectInclNullById(Integer id) {

Object cacheValue = cache.get(id);

if (cacheValue == null) {

Object storageValue = storage.get(key);

cache.set(key, storageValue);

if (storageValue == null) {

cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
}

2)布隆过滤器:布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个 “人”。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图 (这张图片不是我画的,为了省事直接在网上找的):

img

更多关于布隆过滤器的内容可以看我的这篇原创:《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。

如何在redis中从海量Key里查询到固定前缀的数据(最好问到有多少数据量)?

KEYS pattern

优点:返回所有数据

缺点:一次遍历,会造成卡顿

卡顿的问题会造成线上业务的阻塞,所以采用以下指令

SCAN cursor [MATCH pattern] [COUNT count]

优点:根据游标迭代器,每次返回少量符合的数据,同时返回游标的下一次迭代开始(游标不一定有序),直到返回游标为0时结束

缺点:不能实时返回所有数据,count可以指定返回数据,但是知识大概率会返回

实现:由于游标不一定有序,可能会造成取出重复的key,因此可以将key存放在一个HashSet中去重;

分布式锁

作用:分布式系统访问共享资源时,需要加分布式锁

redis单线程为什么需要加锁

1.解决的问题

互斥性(同一时刻只有一个客户端获取资源)

安全性(删除锁只能由拥有者删除)

死锁(由于宕机等因素未释放锁)

容错(宕机时,客户端仍能释放锁)

2.实现

  • 方法一
    • setnk key value(原子操作)
      • 若key不存在,则赋值,且返回1;
      • 若key存在,则返回-1,不可再次设置同一个key,直到被释放
    • EXPIRE key seconds(设置key过期时间)(原子操作)
      • key过期了便可释放锁
      • 缺点:原子性得不到满足
1
2
3
4
5
int status = redisService.setnx(key,value);
if(status == 1){ //虽然两个操作是原子操作,但是分开来就不是原子操作了,可能导致expire未设置,当前线程断掉了,就会造成锁无法释放
redisService.expire(key,expire);
doSomething();
}
  • 方法二
    • SET key value[EX seconds] [PX time] [NX|XX](原子操作)
      • EX:秒
      • PX:毫秒
      • NX:键不存在时,才对建进行设置操作
      • XX:键存在时,才对键进行设置操作
      • set操作成功完成时,返回OK,否则返回nil】

异步队列

什么时消息队列(MQ)

消息队列三大特性:

  • 解耦
  • 异步
  • 削峰

1.pub/sub:主题订阅者模式

1584777417762

订阅端

1
subscribe Topic

发布端

1
publish Topic message

Redis的集群原理

根据一致性哈希算法,将hash(key)存储在hash环中,后面引入虚拟节点,期望节点可以均匀分布在少量节点的情况

Redis哨兵、复制、集群的设计原理与区别

《吊打面试官》—-Redis基础

Redis

集群:将数据分片,横向拓展,高并发(一致性哈希算法)

哨兵:检测主从节点

  • 一个哨兵监听一个

主从:备份数据,高可用性(可在单机上运行多个实例,更改端口即可,充分利用CPU)

  • 主服务器(Master):可读写
  • 从服务器(Slave):仅读
  • 主从备份:在主服务器上根据RDB持久化,将数据持久化,并将其备份到从节点;再将备份期间缓冲区增量的指令发送给从节点,从节点自动运行,实现主从备份
0%