一篇搞懂redis

01:什么是 Redis

Redis 就是⼀个使⽤ C 语⾔开发的数据库,与传统数据库不同的是 Redis 的数据是存在内存中的,也就是它是内存数据库,所以读写速度⾮常快,因此 Redis 被⼴泛应⽤于缓存⽅向。Redis 除了做缓存之外,也经常⽤来做分布式锁,甚⾄是消息队列。Redis 提供了多种数据类型来⽀持不同的业务场景。Redis 还⽀持事务、持久化、Lua 脚本、多种集群⽅案。

02:分布式缓存常⻅的技术选型⽅案

分布式缓存使⽤的比较多的主要是 Memcached 和 Redis。现在基本上都是直接使用 Redis。分布式缓存主要解决的是单机缓存的容量受服务器限制并且⽆法保存通⽤的信息的问题。因为本地缓存只在当前服务⾥有效,⽐如部署了两个相同的服务,他们两者之间的缓存数据是⽆法共享。的

03:Redis 和 Memcached 的区别

共同点:
都是基于内存的数据库,⼀般都⽤来当做缓存使⽤,都有过期策略,两者的性能都⾮常⾼。
区别:
1:Redis ⽀持更丰富的数据类型,⽀持更复杂的应⽤场景。Redis 不仅仅⽀持简单的 k/v 类 型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只⽀持最简单的 k/v 数据类型。
2:Redis ⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进⾏使⽤,⽽ Memecache 把数据全部存在内存之中。
3:Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
4:Redis 在服务器内存使⽤完之后可以将不⽤的数据放到磁盘上。Memcached 在服务器内存使⽤完之后就会直接报异常。
5:Redis 原⽣⽀持集群 cluster 模式。Memcached 没有原⽣的集群模式,需要依靠客户端来实现往集群中分⽚写⼊数据。
6:Redis 使⽤单线程的多路 IO 复⽤模型。Memcached 是多线程⾮阻塞 IO 复⽤的⽹络模型。
7:Redis ⽀持发布订阅模型、Lua 脚本、事务等功能,Memcached 不⽀持。并且 Redis ⽀持更多的编程语⾔。
8:Redis 同时使⽤了惰性删除与定期删除。Memcached 过期数据的删除策略只⽤了惰性删除 。

04:缓存数据的处理流程

如果⽤户请求的数据在缓存中就直接返回,缓存中不存在的话就看数据库中是否存在,数据库中存在的话就更新缓存中的数据,数据库中不存在的话就返回空数据。

05:为什么要⽤ Redis

为了提升⽤户体验以及应对更多的⽤户,主要从⾼性能和⾼并发这两点来看待这个问题。
⾼性能:⽤户访问的数据属于⾼频数据并且不会经常改变,就可以很将该⽤户访问的数据存在缓存中。保证⽤户下⼀次再访问这些数据的时候就可以直接从缓存中获取, 速度非常快。但是需要保持数据库和缓存中的数据的⼀致性, 如果数据库中的对应数据改变的之后,就要同步改变缓存中相应的数据。
高并发:一般像 MySQL 这类的数据库的 QPS(服务器每秒可以执⾏的查询次数)⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存后,就单机 redis 的情况很容易达到 10w+,甚⾄最⾼能达到 30w+,搭建 Redis 集群的话会更⾼。直接操作缓存能够承受的数据库请求数量远远⼤于直接访问数据库,所以可以考虑把数据库中的部分数据转移到缓存中去,提⾼系统整体的并发。

06:Redis 常⻅数据结构以及使⽤场景

string:
介绍:string 数据结构是简单的 key-value 类型。Redis 并没有使用 C 的原生字符串,而是构建了一种简单动态字符串。这些简单动态字符串不光可以保存⽂本数据还可以保存⼆进制数据,并且获取字符串⻓度复杂度为 O(1),而 C 的原生字符串为 O(N)。并且 Redis 的简单动态字符串 API 是安全的,不会造成缓冲区溢出。
应用场景:⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等等。
常用命令:set、get、strlen、exists、dect、incr、setex 等。

1. # 普通字符串的基本操作
2.  127.0.0.1:6379>  set key value #设置key-value类型的值
3.  OK
4.  127.0.0.1:6379>  get key # 根据key获得对应的value
5.  "value"
6.  127.0.0.1:6379> exists key # 判断某个key是否存在
7.  (integer)  1
8.  127.0.0.1:6379> strlen key # 返回key所储存的字符串值的⻓度。
9.  (integer)  5
10.  127.0.0.1:6379>  del key # 删除某个key对应的值
11.  (integer)  1
12.  127.0.0.1:6379>  get key
13.  (nil)
14.  # 批量设置
15.  127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置key-value类型的值
16.  OK
17.  127.0.0.1:6379> mget key1 key2 # 批量获取多个key对应的value
18.  1)  "value1"
19.  2)  "value2
20.  # 计数器(字符串的内容为整数的时候可以使⽤)
21.  127.0.0.1:6379> set number 1
22.  OK
23.  127.0.0.1:6379> incr number # 将key中储存的数字值增⼀
24.  (integer) 2
25.  127.0.0.1:6379> get number
26.  "2"
27.  127.0.0.1:6379> decr number # 将key中储存的数字值减⼀
28.  (integer) 1
29.  127.0.0.1:6379> get number
30.  "1"
31.  # 过期
32.  127.0.0.1:6379> expire key 60 # 数据在60s后过期
33.  (integer) 1
34.  127.0.0.1:6379> setex key 60 value # 数据在60s后过期 (setex:[set] + [ex]pire)
35.  OK
36.  127.0.0.1:6379> ttl key # 查看数据还有多久过期
37.  (integer) 56

list:
介绍:list 即是链表。易于数据元素的插⼊和删除并且且可以灵活调整链表⻓度,但是链表的随机访问困难。C 语⾔并没有实现链表,Redis ⾃⼰实现了 list 链表数据结构。Redis 的 list 是⼀个双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过也带来了部分额外的内存开销。
应用场景:发布与订阅或者说消息队列、慢查询。
常用命令:rpush、lpop、lpush、rpop、lrange、llen 等。

1.  # 通过rpush/lpop实现队列
2.  127.0.0.1:6379> rpush myList value1 # 向list的头部(右边)添加元素
3.  (integer)  1
4.  127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素
5.  (integer)  3
6.  127.0.0.1:6379> lpop myList # 将list的尾部(最左边)元素取出
7.  "value1"
8.  127.0.0.1:6379> lrange myList 0  1  # 查看对应下标的list列表,0为start,1为end
9.  1)  "value2"
10.  2)  "value3"
11.  127.0.0.1:6379> lrange myList 0  -1  # 查看列表中的所有元素,-1表示倒数第⼀
12.  1)  "value2"
13.  2)  "value3"
14.  # 通过rpush/rpop实现栈
15.  127.0.0.1:6379> rpush myList2 value1 value2 value3
16.  (integer)  3
17.  127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
18.  "value3"
19.  # 通过lrange查看对应下标范围的列表元素,lrange还可以基于list实现分⻚查询,且性能⾮常⾼
20.  127.0.0.1:6379> rpush myList value1 value2 value3
21.  (integer)  3
22.  127.0.0.1:6379> lrange myList 0  1  # 查看对应下标的list列表,0为start,1为end
23.  1)  "value1"
24.  2)  "value2"
25.  127.0.0.1:6379> lrange myList 0  -1  # 查看列表中的所有元素,-1表示倒数第⼀
26.  1)  "value1"
27.  2)  "value2"
28.  3)  "value3"
29.  # 通过llen查看链表⻓度
30.  127.0.0.1:6379> llen myList
31.  (integer)  3

hash:
介绍:hash 类似于 JDK1.8 前的 HashMap,内部基于数组 + 链表实现,不过 Redis 做了更多的优化。hash 是⼀个 string 类型的 field 和 value 的映射表, 特别适合⽤于存储对象。后续操作的时候,可以直接仅仅修改这个对象中的某个字段值。
应用场景:⽐如我们可以 hash 数据结构来存储⽤户信息,商品信息等等。
常用命令:hset、hmset、hexists、hget、hgetall、hkeys、hvals 等。

1.  127.0.0.1:6379> hset userInfoKey name "guide" description "dev" age "24"
2.  OK
3.  127.0.0.1:6379> hexists userInfoKey name # 查看key对应的value中指定的字段是否存在。
4.  (integer)  1
5.  127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。
6.  "guide"
7.  127.0.0.1:6379> hget userInfoKey age
8.  "24"
9.  127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定key的所有字段和值
10.  1)  "name"
11.  2)  "guide"
12.  3)  "description"
13.  4)  "dev"
14.  5)  "age"
15.  6)  "24"
16.  127.0.0.1:6379> hkeys userInfoKey # 获取key列表
17.  1)  "name"
18.  2)  "description"
19.  3)  "age"
20.  127.0.0.1:6379> hvals userInfoKey # 获取value列表
21.  1)  "guide"
22.  2)  "dev"
23.  3)  "24"
24.  127.0.0.1:6379> hset userInfoKey name "GuideGeGe"  # 修改某个字段对应的值
25.  127.0.0.1:6379> hget userInfoKey name
26.  "GuideGeGe"

set:
介绍:set 类似于 Java 中的 HashSet。Redis 中的 set 类型是⼀种⽆序集合,在存放的数据不能重复以及需要获取多个数据源交集和并集等场景中,Set 是一个很好的选择。并且 set 提供了判断某个成员是否在⼀个 set 集合内的重要接⼝。
应用场景:可以将⼀个⽤户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。可以⾮常⽅便的实现如共同关注、共同粉丝、共同喜好等功能。
常用命令:sadd、spop、smembers、sismember、scard、sinterstore、sunion 等。

1.  127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去
2.  (integer)  2
3.  127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素
4.  (integer)  0
5.  127.0.0.1:6379> smembers mySet # 查看set中所有的元素
6.  1)  "value1"
7.  2)  "value2"
8.  127.0.0.1:6379> scard mySet # 查看set的⻓度
9.  (integer)  2
10.  127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set中,只能接收单个元素
11.  (integer)  1
12.  127.0.0.1:6379> sadd mySet2 value2 value3
13.  (integer)  2
14.  127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取mySet和mySet2的交集并存放在mySet3中
15.  (integer)  1
16.  127.0.0.1:6379> smembers mySet3
17.  1)  "value2"

sorted set:
介绍:和 set 相⽐,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score 进⾏有序排列,还可以通过 score 的范围来获取元素的列表。
应用场景:在直播系统中,实时排⾏信息包含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息等信息。
常用命令:zadd、zcard、zscore、zrange、zrevrange、zrem 等。

1.  127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到sorted set中3.0为权重
2.  (integer)  1
3.  127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # ⼀次添加多个元素
4.  (integer)  2
5.  127.0.0.1:6379> zcard myZset # 查看sorted set中的元素数量
6.  (integer)  3
7.  127.0.0.1:6379> zscore myZset value1 # 查看某个value的权重
8.  "3"
9.  127.0.0.1:6379> zrange myZset 0  -1  # 顺序输出某个范围区间的元素,0 -1表示输出所有元素
10.  1)  "value3"
11.  2)  "value2"
12.  3)  "value1"
13.  127.0.0.1:6379> zrange myZset 0  1  # 顺序输出某个范围区间的元素,0 为 start 1为stop
14.  1)  "value3"
15.  2)  "value2"
16.  127.0.0.1:6379> zrevrange myZset 0  1  # 逆序输出某个范围区间的元素,0为start1为stop
17.  1)  "value1"
18.  2)  "value2"

07:Redis 单线程模型

为什么 Redis 是单线程模型:
Redis 基于 Reactor 模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型,这套事件处理模型对应的是 Redis 中的⽂件事件处理器,由于⽂件事件处理器是单线程⽅式运⾏的,所以⼀般说 Redis 是单线程模型。
Redis 单线程怎么监听⼤量的客户端连接:
Redis 通过 IO 多路复⽤程序 来监听来⾃客户端的⼤量连接,它会将感兴趣的事件及读写类型注册到内核中并监听每个事件是否发⽣。I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗。
Redis 服务器的⽂件事件处理器包含哪些部分:
1:多个 socket(客户端连接)。
2:IO 多路复⽤程序(⽀持多个客户端连接的关键)。
3:⽂件事件分派器(将 socket 关联到相应的事件处理器)。
4:事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。

08:Redis 为什么不使⽤多线程

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

09:Redis 为什么又引入了多线程

Redis4 引⼊了多线程,主要是针对⼀些⼤键值对的删除操作的命令,这些命令就由主处理之外的其他线程来异步处理。
Redis6 引⼊了多线程,主要是为了提⾼⽹络 IO 读写性能,因为 Redis 的瓶颈主要受限于内存和⽹络。
虽然 Redis6 引⼊了多线程,但是 Redis 的多线程只是在⽹络数据的读写这类耗时操作上使⽤了,执⾏命令仍然是单线程顺序执⾏,所以线程还是安全的。
Redis6 的多线程默认是禁⽤的,只使⽤主线程。如需开启需要修改 Redis 配置⽂件 redis.conf 设置开启多线程及线程数使得多线程生效:

1.  io-threads-do-reads yes
2.  io-threads 4  # 官⽹建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

10:Redis 给缓存数据设置过期时间的作用

内存是有限的,如果缓存中的所有数据都是⼀直保存的话,就会出现 Out of memory。Redis ⾃带了给缓存数据设置过期时间的功能:

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

设置过期时间还可以用于在具体业务场景中,⽐如短信验证码只在 1 分钟内有效,⽤户登录的 token 可能只在 1 天内有效。如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。

11:Redis 如何判断数据是否过期

Redis 通过⼀个叫做过期字典,可以看作是 hash 表来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key,过期字典的值是⼀个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间,该时间的精度是毫秒的 UNIX 时间戳。

1.  typedef  struct redisDb {
2.  ...
4.  dict *dict;  // 数据库键空间,保存着数据库中所有键值对
5.  dict *expires // 过期字典,保存着键的过期时间
6.  ...
7.  } redisDb;

12:Redis 过期的数据的删除策略

1:惰性删除:只会在取出 key 的时候才对数据进⾏过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
2:定期删除:每隔⼀段时间抽取⼀批 key 执⾏删除过期 key 操作。并且 Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对CPU更加友好。Redis 采⽤的是定期删除 + 惰性 / 懒汉式删除。但是仅仅通过给 key 设置过期时间还是有问题的,因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 导致⼤量过期 key 堆积在内存⾥,出现 Out of memory。解决方案就是 Redis 的内存淘汰机制。

13: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 支持两种持久化机制,⼀种是叫快照(snapshotting,RDB),另⼀种⽅式是只追加⽂件 (append-only file, AOF)。
RDB:
Redis 可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis 创建快照 之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本,还可以将快照留在原地以便重启服务器的时候使⽤。Redis 默认采⽤ RDB 的持久化方式,在 redis.conf 配置⽂件中设置:

1.  save 900  1  # 在900秒(15分钟)之后,如果⾄少有1个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。
2.  save 300  10  # 在300秒(5分钟)之后,如果⾄少有10个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。
3.  save 60  10000  # 在60秒(1分钟)之后,如果⾄少有10000个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。

AOF:
AOF 持久化的实时性更好,因此已成为主流的持久化⽅案。开启 AOF 持久化后每执⾏⼀条会更改 Redis 中的数据的命令,Redis 就会将该命令写⼊硬盘中的 AOF ⽂件。AOF ⽂件的保存位置和 RDB ⽂件的位置相同,默认的⽂件名是 appendonly.aof。
在 redis.conf 配置⽂件中设置:

1.  appendonly yes

Redis 的配置⽂件中存在三种不同的 AOF 持久化⽅式:

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

15:Redis 事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务。

1.  > MULTI
2.  OK
3.  > INCR foo
4.  QUEUED
5.  > INCR bar
6.  QUEUED
7.  > EXEC
8.  1)  (integer)  1
9.  2)  (integer)  1

使⽤ MULTI 命令后可以输⼊多个命令。Redis 不会⽴即执⾏这些命令,⽽是将它们放到队列,当 调⽤了 EXEC 命令将执⾏所有命令。
Redis 不⽀持 roll back,因⽽不满⾜事务的原⼦性,且 Redis 不满足事务的持久性。

16:缓存穿透

⼤量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这⼀层。
解决方案:
1:参数校验:⼀些不合法的参数请求直接抛出异常信息返回给客户端。⽐如查询的数据库 id 不能⼩于 0,传⼊的邮箱格式不对时直接返回错误消息给客户端等等。
2:缓存⽆效 key:将未查询到的无效 key 写到 Redis 中去并设置过期时间。这种⽅式可以解决请求的 key 变化不频繁的情况,但不能从根本上解决问题。建议尽量将⽆效的 key 的过期时间设置短⼀点,⽐如 1 分钟。
3:布隆过滤器:布隆过滤器是一种数据结构,通过布隆过滤器可以⾮常⽅便地判断⼀个给定数据是否存在于海量数据中。具体做法是把所有可能存在的请求的值都存放在布隆过滤器中,当⽤户请求过来,先判断⽤户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会继续执行。布隆过滤器可能会存在误判的情况,因为布隆过滤器底层是哈希计算,所以布隆过滤器说某个元素存在,⼩概率会误判,布隆过滤器说某个元素不在,那么这个元素⼀定不在。

17:缓存雪崩

缓存在同⼀时间⼤⾯积的失效,后⾯的请求都直接落到了数据库上,造成数据库短时间内承受⼤量请求。比如系统的缓存模块出了问题⽐如宕机导致不可⽤,造成系统的所有访问都要⾛数据库,或者秒杀等热点缓存,在秒杀开始 12 个⼩时之前存放了⼀批商品到 Redis 中,设置的缓存过期时间也是 12 个⼩时,当秒杀开始的时候,这些秒杀的商品的访问直接失效。
解决方案:
针对 Redis 服务不可用的情况:
1:采⽤ Redis 集群,避免单机出现问题整个缓存服务都没办法使⽤。
2:限流,避免同时处理⼤量的请求。
针对热点缓存失效的情况:
1:设置不同的失效时间⽐如随机设置缓存的失效时间。
2:缓存永不失效。

18:如何保证缓存和数据库数据的⼀致性

Cache Aside Pattern(旁路缓存模式):更新 DB,然后直接删除 cache。
1:缓存失效时间变短,不推荐,治标不治本 :让缓存数据的过期时间变短,缓存就会从数据库中加载数据。另外这种解决办法对于先操作缓存后操作数据库的场景不适⽤。
2:增加 cache 更新重试机制,常⽤: 如果 cache 服务当前不可⽤导致缓存删除失败的话,就隔⼀段时间进⾏重试,重试次数可以自定义。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存⼊队列中,等缓存服务可⽤之后,再将缓存中对应的 key 删除即可。

你可能感兴趣的:(一篇搞懂redis)