Redis

转载自JavaGuide/redis-all.md at master · Snailclimb/JavaGuide (github.com)html

1. 简单介绍一下 Redis 呗!

简单来讲 Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不一样的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,因此读写速度很是快,所以 Redis 被普遍应用于缓存方向。java

另外,Redis 除了作缓存以外,也常常用来作分布式锁,甚至是消息队列。git

Redis 提供了多种数据类型来支持不一样的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。github

2. 分布式缓存常见的技术选型方案有哪些?

分布式缓存的话,使用的比较多的主要是 Memcached 和 Redis。不过,如今基本没有看过还有项目使用 Memcached 来作缓存,都是直接用 Redis。web

Memcached 是分布式缓存最开始兴起的那会,比较经常使用的。后来,随着 Redis 的发展,你们慢慢都转而使用更增强大的 Redis 了。面试

分布式缓存主要解决的是单机缓存的容量受服务器限制而且没法保存通用信息的问题。由于,本地缓存只在当前服务里有效,好比若是你部署了两个相同的服务,他们二者之间的缓存数据是没法共同的。redis

3. 说一下 Redis 和 Memcached 的区别和共同点

如今公司通常都是用 Redis 来实现缓存,并且 Redis 自身也愈来愈强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于咱们在作相应的技术选型的时候,可以作到有理有据!shell

共同点 :数据库

  1. 都是基于内存的数据库,通常都用来当作缓存使用。
  2. 都有过时策略。
  3. 二者的性能都很是高。

区别 :编程

  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不只仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,能够将内存中的数据保持在磁盘中,重启的时候能够再次加载进行使用,而 Memecache 把数据所有存在内存之中。
  3. Redis 有灾难恢复机制。 由于能够把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使用完以后,能够将不用的数据放到磁盘上。可是,Memcached 在服务器内存使用完以后,就会直接报异常。
  5. Memcached 没有原生的集群模式,须要依靠客户端来实现往集群中分片写入数据;可是 Redis 目前是原生支持 cluster 模式的。
  6. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
  7. Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。而且,Redis 支持更多的编程语言。
  8. Memcached 过时数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与按期删除。

相信看了上面的对比以后,咱们已经没有什么理由能够选择使用 Memcached 来做为本身项目的分布式缓存了。

4. 缓存数据的处理流程是怎样的?

做为暖男一号,我给你们画了一个草图。

正常缓存处理流程

简单来讲就是:

  1. 若是用户请求的数据在缓存中就直接返回。
  2. 缓存中不存在的话就看数据库中是否存在。
  3. 数据库中存在的话就更新缓存中的数据。
  4. 数据库中不存在的话就返回空数据。

5. 为何要用 Redis/为何要用缓存?

简单,来讲使用缓存主要是为了提高用户体验以及应对更多的用户。

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

高性能 :

对照上面 👆 我画的图。咱们设想这样的场景:

假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。可是,若是说,用户访问的数据属于高频数据而且不会常常改变的话,那么咱们就能够很放心地将该用户访问的数据存在缓存中。

这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就能够直接从缓存中获取了。操做缓存就是直接操做内存,因此速度至关快。

不过,要保持数据库和缓存中的数据的一致性。 若是数据库中的对应数据改变的以后,同步改变缓存中相应的数据便可!

高并发:

通常像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,可是使用 Redis 缓存以后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的状况,redis 集群的话会更高)。

QPS(Query Per Second):服务器每秒能够执行的查询次数;

因而可知,直接操做缓存可以承受的数据库请求数量是远远大于直接访问数据库的,因此咱们能够考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用通过数据库。进而,咱们也就提升了系统总体的并发。

6. Redis 常见数据结构以及使用场景分析

你能够本身本机安装 redis 或者经过 redis 官网提供的在线 redis 环境

try-redis

6.1. string

  1. 介绍 :string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,可是 Redis 并无使用 C 的字符串表示,而是本身构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光能够保存文本数据还能够保存二进制数据,而且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此以外,Redis 的 SDS API 是安全的,不会形成缓冲区溢出。
  2. 经常使用命令: set,get,strlen,exists,decr,incr,setex 等等。
  3. 应用场景: 通常经常使用在须要计数的场景,好比用户的访问次数、热点文章的点赞转发数量等等。

下面咱们简单看看它的使用!

普通字符串的基本操做:

127.0.0.1:6379> set key value #设置 key-value 类型的值 OK 127.0.0.1:6379> get key # 根据 key 得到对应的 value "value" 127.0.0.1:6379> exists key # 判断某个 key 是否存在 (integer) 1 127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。 (integer) 5 127.0.0.1:6379> del key # 删除某个 key 对应的值 (integer) 1 127.0.0.1:6379> get key (nil)

批量设置 :

127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值 OK 127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value 1) "value1" 2) "value2"

计数器(字符串的内容为整数的时候可使用):

127.0.0.1:6379> set number 1 OK 127.0.0.1:6379> incr number # 将 key 中储存的数字值增一 (integer) 2 127.0.0.1:6379> get number "2" 127.0.0.1:6379> decr number # 将 key 中储存的数字值减一 (integer) 1 127.0.0.1:6379> get number "1"

过时(默认为永不过时):

127.0.0.1:6379> expire key  60 # 数据在 60s 后过时 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过时 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过时 (integer) 56

6.2. list

  1. 介绍 :list 便是 链表。链表是一种很是常见的数据结构,特色是易于数据元素的插入和删除而且能够灵活调整链表长度,可是链表的随机访问困难。许多高级编程语言都内置了链表的实现好比 Java 中的 LinkedList,可是 C 语言并无实现链表,因此 Redis 实现了本身的链表数据结构。Redis 的 list 的实现为一个 双向链表,便可以支持反向查找和遍历,更方便操做,不过带来了部分额外的内存开销。
  2. 经常使用命令: rpush,lpop,lpush,rpop,lrange,llen 等。
  3. 应用场景: 发布与订阅或者说消息队列、慢查询。

下面咱们简单看看它的使用!

经过 rpush/lpop 实现队列:

127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素 (integer) 1 127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素 (integer) 3 127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出 "value1" 127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end 1) "value2" 2) "value3" 127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的全部元素,-1表示倒数第一 1) "value2" 2) "value3"

经过 rpush/rpop 实现栈:

127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出 "value3"

我专门花了一个图方便小伙伴们来理解:

redis list

经过 lrange 查看对应下标范围的列表元素:

127.0.0.1:6379> rpush myList value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end 1) "value1" 2) "value2" 127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的全部元素,-1表示倒数第一 1) "value1" 2) "value2" 3) "value3"

经过 lrange 命令,你能够基于 list 实现分页查询,性能很是高!

经过 llen 查看链表长度:

127.0.0.1:6379> llen myList
(integer) 3

6.3. hash

  1. 介绍 :hash 相似于 JDK1.8 前的 HashMap,内部实现也差很少(数组 + 链表)。不过,Redis 的 hash 作了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操做的时候,你能够直接仅仅修改这个对象中的某个字段的值。 好比咱们能够 hash 数据结构来存储用户信息,商品信息等等。
  2. 经常使用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
  3. 应用场景: 系统中对象数据的存储。

下面咱们简单看看它的使用!

127.0.0.1:6379> hmset userInfoKey name "guide" description "dev" age "24" OK 127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value中指定的字段是否存在。 (integer) 1 127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。 "guide" 127.0.0.1:6379> hget userInfoKey age "24" 127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的全部字段和值 1) "name" 2) "guide" 3) "description" 4) "dev" 5) "age" 6) "24" 127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表 1) "name" 2) "description" 3) "age" 127.0.0.1:6379> hvals userInfoKey # 获取 value 列表 1) "guide" 2) "dev" 3) "24" 127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某个字段对应的值 127.0.0.1:6379> hget userInfoKey name "GuideGeGe"

6.4. set

  1. 介绍 : set 相似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有前后顺序。当你须要存储一个列表数据,又不但愿出现重复数据时,set 是一个很好的选择,而且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。能够基于 set 轻易实现交集、并集、差集的操做。好比:你能够将一个用户全部的关注人存在一个集合中,将其全部粉丝存在一个集合。Redis 能够很是方便的实现如共同关注、共同粉丝、共同喜爱等功能。这个过程也就是求交集的过程。
  2. 经常使用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
  3. 应用场景: 须要存放的数据不能重复以及须要获取多个数据源交集和并集等场景

下面咱们简单看看它的使用!

127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去 (integer) 2 127.0.0.1:6379> sadd mySet value1 # 不容许有重复元素 (integer) 0 127.0.0.1:6379> smembers mySet # 查看 set 中全部的元素 1) "value1" 2) "value2" 127.0.0.1:6379> scard mySet # 查看 set 的长度 (integer) 2 127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素 (integer) 1 127.0.0.1:6379> sadd mySet2 value2 value3 (integer) 2 127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中 (integer) 1 127.0.0.1:6379> smembers mySet3 1) "value2"

6.5. sorted set

  1. 介绍: 和 set 相比,sorted set 增长了一个权重参数 score,使得集合中的元素可以按 score 进行有序排列,还能够经过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
  2. 经常使用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
  3. 应用场景: 须要对数据根据某个权重进行排序的场景。好比在直播系统中,实时排行信息包含直播间在线用户列表,各类礼物排行榜,弹幕消息(能够理解为按消息维度的消息排行榜)等信息。
127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重 (integer) 1 127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素 (integer) 2 127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量 (integer) 3 127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重 "3" 127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出全部元素 1) "value3" 2) "value2" 3) "value1" 127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value3" 2) "value2" 127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value1" 2) "value2"

6.6 bitmap

  1. 介绍: bitmap 存储的是连续的二进制数字(0 和 1),经过 bitmap, 只须要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素自己 。咱们知道 8 个 bit 能够组成一个 byte,因此 bitmap 自己会极大的节省储存空间。
  2. 经常使用命令: setbit 、getbit 、bitcountbitop
  3. 应用场景: 适合须要保存状态信息(好比是否签到、是否登陆...)并须要进一步对这些信息进行分析的场景。好比用户签到状况、活跃用户状况、用户行为统计(好比是否点赞过某个视频)。
# SETBIT 会返回以前位的值(默认是 0)这里会生成 7 个位 127.0.0.1:6379> setbit mykey 7 1 (integer) 0 127.0.0.1:6379> setbit mykey 7 0 (integer) 1 127.0.0.1:6379> getbit mykey 7 (integer) 0 127.0.0.1:6379> setbit mykey 6 1 (integer) 0 127.0.0.1:6379> setbit mykey 8 1 (integer) 0 # 经过 bitcount 统计被被设置为 1 的位的数量。 127.0.0.1:6379> bitcount mykey (integer) 2

针对上面提到的一些场景,这里进行进一步说明。

使用场景一:用户行为分析 不少网站为了分析你的喜爱,须要研究你点赞过的内容。

# 记录你喜欢过 001 号小姐姐 127.0.0.1:6379> setbit beauty_girl_001 uid 1

使用场景二:统计活跃用户

使用时间做为 key,而后用户 ID 为 offset,若是当日活跃过就设置为 1

那么我该如何计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只要有一天在线就称为活跃),有请下一个 redis 的命令

# 对一个或多个保存二进制位的字符串 key 进行位元操做,并将结果保存到 destkey 上。 # BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操做中的任意一种参数 BITOP operation destkey key [key ...]

初始化数据:

127.0.0.1:6379> setbit 20210308 1 1
(integer) 0
127.0.0.1:6379> setbit 20210308 2 1 (integer) 0 127.0.0.1:6379> setbit 20210309 1 1 (integer) 0

统计 20210308~20210309 总活跃用户数: 1

127.0.0.1:6379> bitop and desk1 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk1 (integer) 1

统计 20210308~20210309 在线活跃用户数: 2

127.0.0.1:6379> bitop or desk2 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk2 (integer) 2

使用场景三:用户在线状态

对于获取或者统计用户在线状态,使用 bitmap 是一个节约空间且效率又高的一种方法。

只须要一个 key,而后用户 ID 为 offset,若是在线就设置为 1,不在线就设置为 0。

7. Redis 单线程模型详解

Redis 基于 Reactor 模式来设计开发了本身的一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。因为文件事件处理器(file event handler)是单线程方式运行的,因此咱们通常都说 Redis 是单线程模型。

既然是单线程,那怎么监听大量的客户端链接呢?

Redis 经过IO 多路复用程序 来监听来自客户端的大量链接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每一个事件是否发生。

这样的好处很是明显: I/O 多路复用技术的使用让 Redis 不须要额外建立多余的线程来监听客户端的大量链接,下降了资源的消耗(和 NIO 中的 Selector 组件很像)。

另外, Redis 服务器是一个事件驱动程序,服务器须要处理两类事件:1. 文件事件; 2. 时间事件。

时间事件不须要多花时间了解,咱们接触最多的仍是 文件事件(客户端进行读取写入等操做,涉及一系列网络通讯)。

《Redis 设计与实现》有一段话是如是介绍文件事件的,我以为写得挺不错。

Redis 基于 Reactor 模式开发了本身的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不一样的事件处理器。

当被监听的套接字准备好执行链接应答(accept)、读取(read)、写入(write)、关 闭(close)等操做时,与操做相对应的文件事件就会产生,这时文件事件处理器就会调用套接字以前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但经过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通讯模型,又能够很好地与 Redis 服务器中其余一样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

能够看出,文件事件处理器(file event handler)主要是包含 4 个部分:

  • 多个 socket(客户端链接)
  • IO 多路复用程序(支持多个客户端链接的关键)
  • 文件事件分派器(将 socket 关联到相应的事件处理器)
  • 事件处理器(链接应答处理器、命令请求处理器、命令回复处理器)

《Redis设计与实现:12章》

8. Redis 没有使用多线程?为何不使用多线程?

虽说 Redis 是单线程模型,可是,实际上,Redis 在 4.0 以后的版本中就已经加入了对多线程的支持。

redis4.0 more thread

不过,Redis 4.0 增长的多线程主要是针对一些大键值对的删除操做的命令,使用这些命令就会使用主处理以外的其余线程来“异步处理”。

大致上来讲,Redis 6.0 以前主要仍是单线程处理。

那,Redis6.0 以前 为何不使用多线程?

我以为主要缘由有下面 3 个:

  1. 单线程编程容易而且更容易维护;
  2. Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  3. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

9. Redis6.0 以后为什么引入了多线程?

Redis6.0 引入多线程主要是为了提升网络 IO 读写性能,由于这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,可是 Redis 的多线程只是在网络数据的读写这类耗时操做上使用了,执行命令仍然是单线程顺序执行。所以,你也不须要担忧线程安全问题。

Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启须要修改 redis 配置文件 redis.conf :

io-threads-do-reads yes

开启多线程后,还须要设置线程数,不然是不生效的。一样须要修改 redis 配置文件 redis.conf :

io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

推荐阅读:

  1. Redis 6.0 新特性-多线程连环 13 问!
  2. 为何 Redis 选择单线程模型

10. Redis 给缓存数据设置过时时间有啥用?

通常状况下,咱们设置保存的缓存数据的时候都会设置一个过时时间。为何呢?

由于内存是有限的,若是缓存中的全部数据都是一直保存的话,分分钟直接 Out of memory。

Redis 自带了给缓存数据设置过时时间的功能,好比:

127.0.0.1:6379> exp key 60 # 数据在 60s 后过时 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过时 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过时 (integer) 56

注意:**Redis 中除了字符串类型有本身独有设置过时时间的命令 setex 外,其余方法都须要依靠 expire 命令来设置过时时间 。另外, persist 命令能够移除一个键的过时时间。 **

过时时间除了有助于缓解内存的消耗,还有什么其余用么?

不少时候,咱们的业务场景就是须要某个数据只在某一时间段内存在,好比咱们的短信验证码可能只在 1 分钟内有效,用户登陆的 token 可能只在 1 天内有效。

若是使用传统的数据库来处理的话,通常都是本身判断过时,这样更麻烦而且性能要差不少。

11. Redis 是如何判断数据是否过时的呢?

Redis 经过一个叫作过时字典(能够看做是 hash 表)来保存数据过时的时间。过时字典的键指向 Redis 数据库中的某个 key(键),过时字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过时时间(毫秒精度的 UNIX 时间戳)。

redis过时字典

过时字典是存储在 redisDb 这个结构里的:

typedef struct redisDb { ... dict *dict; //数据库键空间,保存着数据库中全部键值对 dict *expires // 过时字典,保存着键的过时时间 ... } redisDb;

12. 过时的数据的删除策略了解么?

若是假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?

经常使用的过时数据的删除策略就两个(重要!本身造缓存轮子的时候须要格外考虑的东西):

  1. 惰性删除 :只会在取出 key 的时候才对数据进行过时检查。这样对 CPU 最友好,可是可能会形成太多过时 key 没有被删除。
  2. 按期删除 : 每隔一段时间抽取一批 key 执行删除过时 key 操做。而且,Redis 底层会经过限制删除操做执行的时长和频率来减小删除操做对 CPU 时间的影响。

按期删除对内存更加友好,惰性删除对 CPU 更加友好。二者各有千秋,因此 Redis 采用的是 按期删除+惰性/懒汉式删除 。

可是,仅仅经过给 key 设置过时时间仍是有问题的。由于仍是可能存在按期删除和惰性删除漏掉了不少过时 key 的状况。这样就致使大量过时 key 堆积在内存里,而后就 Out of memory 了。

怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。

13. Redis 内存淘汰机制了解么?

相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

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

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

4.0 版本后增长如下两种:

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

14. Redis 持久化机制(怎么保证 Redis 挂掉以后再重启数据能够进行恢复)

不少时候咱们须要持久化数据也就是将内存中的数据写入到硬盘里面,大部分缘由是为了以后重用数据(好比重启机器、机器故障以后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。

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

快照(snapshotting)持久化(RDB)

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

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

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命令建立快照。

AOF(append-only file)持久化

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

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,而后再根据 appendfsync 配置来决定什么时候将其同步到硬盘中的 AOF 文件。

AOF 文件的保存位置和 RDB 文件的位置相同,都是经过 dir 参数设置的,默认的文件名是 appendonly.aof

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

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

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

相关 issue :783:Redis 的 AOF 方式

拓展:Redis 4.0 对于持久化机制的优化

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

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

官方文档地址:https://redis.io/topics/persistence

补充内容:AOF 重写

AOF 重写能够产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态同样,但体积更小。

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

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

15. Redis 事务

Redis 能够经过 MULTIEXECDISCARD 和 WATCH 等命令来实现事务(transaction)功能。

> MULTI
OK
> SET USER "Guide哥" QUEUED > GET USER QUEUED > EXEC 1) OK 2) "Guide哥"

使用 MULTI 命令后能够输入多个命令。Redis 不会当即执行这些命令,而是将它们放到队列,当调用了 EXEC 命令将执行全部命令。

这个过程是这样的:

  1. 开始事务(MULTI)。
  2. 命令入队(批量操做 Redis 的命令,先进先出(FIFO)的顺序执行)。
  3. 执行事务(EXEC)。

你也能够经过 DISCARD 命令取消一个事务,它会清空事务队列中保存的全部命令。

> MULTI
OK
> SET USER "Guide哥" QUEUED > GET USER QUEUED > DISCARD OK

WATCH 命令用于监听指定的键,当调用 EXEC 命令执行事务时,若是一个被 WATCH 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。

> WATCH USER
OK
> MULTI > SET USER "Guide哥" OK > GET USER Guide哥 > EXEC ERR EXEC without MULTI

Redis 官网相关介绍 https://redis.io/topics/transactions 以下:

redis事务

可是,Redis 的事务和咱们平时理解的关系型数据库的事务不一样。咱们知道事务具备四大特性: 1. 原子性,2. 隔离性,3. 持久性,4. 一致性。

  1. 原子性(Atomicity): 事务是最小的执行单位,不容许分割。事务的原子性确保动做要么所有完成,要么彻底不起做用;
  2. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其余事务所干扰,各并发事务之间数据库是独立的;
  3. 持久性(Durability): 一个事务被提交以后。它对数据库中数据的改变是持久的,即便数据库发生故障也不该该对其有任何影响。
  4. 一致性(Consistency): 执行事务先后,数据保持一致,多个事务对同一个数据读取的结果是相同的;

Redis 是不支持 roll back 的,于是不知足原子性的(并且不知足持久性)。

Redis 官网也解释了本身为啥不支持回滚。简单来讲就是 Redis 开发者们以为不必支持回滚,这样更简单便捷而且性能更好。Redis 开发者以为即便命令执行错误也应该在开发过程当中就被发现而不是生产过程当中。

redis roll back

你能够将 Redis 中的事务就理解为 :Redis 事务提供了一种将多个命令请求打包的功能。而后,再按顺序执行打包的全部命令,而且不会被中途打断。

相关 issue :

16. 缓存穿透

16.1. 什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,致使请求直接到了数据库上,根本没有通过缓存这一层。举个例子:某个黑客故意制造咱们缓存中不存在的 key 发起大量请求,致使大量请求落到数据库。

16.2. 缓存穿透状况的处理流程是怎样的?

以下图所示,用户的请求最终都要跑到数据库中查询一遍。

缓存穿透状况

16.3. 有哪些解决办法?

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

1)缓存无效 key

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

另外,这里多说一嘴,通常状况下咱们是这样设计 key 的: 表名:列名:主键名:主键值 。

若是用 Java 代码展现的话,差很少是下面这样的:

public Object getObjectInclNullById(Integer id) { // 从缓存中获取数据 Object cacheValue = cache.get(id); // 缓存为空 if (cacheValue == null) { // 从数据库中获取 Object storageValue = storage.get(key); // 缓存空对象 cache.set(key, storageValue); // 若是存储数据为空,须要设置一个过时时间(300秒) if (storageValue == null) { // 必须设置过时时间,不然有被攻击的风险 cache.expire(key, 60 * 5); } return storageValue; } return cacheValue; }

2)布隆过滤器

布隆过滤器是一个很是神奇的数据结构,经过它咱们能够很是方便地判断一个给定数据是否存在于海量数据中。咱们须要的就是判断 key 是否合法,有没有感受布隆过滤器就是咱们想要找的那个“人”。

具体是这样作的:把全部可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器以后的缓存处理流程图以下。

image

可是,须要注意的是布隆过滤器可能会存在误判的状况。总结来讲就是: 布隆过滤器说某个元素存在,小几率会误判。布隆过滤器说某个元素不在,那么这个元素必定不在。

为何会出现误判的状况呢? 咱们还要从布隆过滤器的原理来讲!

咱们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操做:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,获得哈希值(有几个哈希函数获得几个哈希值)。
  2. 根据获得的哈希值,在位数组中把对应下标的值置为 1。

咱们再来看一下,当咱们须要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操做:

  1. 对给定元素再次进行相同的哈希计算;
  2. 获得值以后判断位数组中的每一个元素是否都为 1,若是值都为 1,那么说明这个值在布隆过滤器中,若是存在一个值不为 1,说明该元素不在布隆过滤器中。

而后,必定会出现这样一种状况:不一样的字符串可能哈希出来的位置相同。 (能够适当增长位数组大小或者调整咱们的哈希函数来下降几率)

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

17. 缓存雪崩

17.1. 什么是缓存雪崩?

我发现缓存雪崩这名字起的有点意思,哈哈。

实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,形成数据库短期内承受大量请求。 这就比如雪崩同样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

举个例子:系统的缓存模块出了问题好比宕机致使不可用。形成系统的全部访问,都要走数据库。

还有一种缓存雪崩的场景是:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,致使对应的请求直接落到了数据库上。 这样的状况,有下面几种解决办法:

举个例子 :秒杀开始 12 个小时以前,咱们统一存放了一批商品到 Redis 中,设置的缓存过时时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。致使的状况就是,相应的请求直接就落到了数据库上,就像雪崩同样可怕。

17.2. 有哪些解决办法?

针对 Redis 服务不可用的状况:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。

针对热点缓存失效的状况:

  1. 设置不一样的失效时间好比随机设置缓存的失效时间。
  2. 缓存永不失效。

18. 如何保证缓存和数据库数据的一致性?

细说的话能够扯不少,可是我以为其实没太大必要(小声 BB:不少解决方案我也没太弄明白)。我我的以为引入缓存以后,若是为了短期的不一致性问题,选择让系统设计变得更加复杂的话,彻底不必。

下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,而后直接删除 cache 。

若是更新数据库成功,而删除缓存这一步失败的状况的话,简单说两个解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本) :咱们让缓存数据的过时时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操做缓存后操做数据库的场景不适用。
  2. 增长 cache 更新重试机制(经常使用): 若是 cache 服务当前不可用致使缓存删除失败的话,咱们就隔一段时间进行重试,重试次数能够本身定。若是屡次重试仍是失败的话,咱们能够把当前更新失败的 key 存入队列中,等缓存服务可用以后,再将缓存中对应的 key 删除便可。

19. 参考

20. 公众号

若是你们想要实时关注我更新的文章以及分享的干货的话,能够关注个人公众号。

《Java 面试突击》: 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本公众号后台回复 "Java 面试突击" 便可免费领取!

Java 工程师必备学习资源: 一些 Java 工程师经常使用学习资源公众号后台回复关键字 “1” 便可免费无套路获取。

个人公众号

相关文章
相关标签/搜索