一、什么是Redis
Redis
是一种用C语言
开发的数据库,Redis的数据是存在内存中的,也叫内存数据库,读写速度非常快,常常用来做缓存。
Redis
支持丰富的数据类型,除了k/v
类型的数据,还支持list、set、hash、zset
。Redis
支持数据的持久化,可以将内存中的数据存在磁盘中,重新加载的时候再读取出来。Redis
有灾难恢复机制,因为其支持持久化。Redis
在内存快使用完之后,将内存中不用的数据存到磁盘中。Redis
支持集群模式。Redis
是单线程的IO多路复用
模型。(Redis6.0
后引入了多线程)Redis
支持发布订阅模型、lua
脚本、事务等功能。Redis
混合使用惰性删除
和定期删除
策略来删除过期的数据。
二、缓存数据的处理流程
用户请求数据时,先判断缓存中是否存在,存在则直接返回数据,不存在,在数据库中查找,数据库中存在,则更新缓存数据,并返回该数据,不存在,则返回空数据。
三、为什么要使用缓存/Redis
主要有两点:高性能和高并发。
- 高性能
用户第一次访问数据时,是从磁盘中读取的,速度比较慢,若这些数据是用户高频率访问的数据,则可以将这些数据放在缓存中,下次用户直接从缓存中获取,操作缓存就是操作内存,速度是非常快的。 - 高并发
一般像MySQL
这种数据库的QPS
(服务器每秒可以执行的查询次数)差不多是1w
左右(4核8g),但是Redis
的QPS
可以轻松达到10w
左右。最高能达到30w
(单机Redis
,集群会更高)。
直接操作缓存能够承受的数据库请求量是远远高于直接操作数据库的。所有可以将一部分数据放到缓存中,这样可以提高系统的并发量。
四、Redis数据结构分析
1、string
string
数据结构是简单的k/v类型。常用命令如下
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
2、list
list是链表,Redis实现的list是双向链表,支持反向查询和遍历。
常用命令如下:
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"
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"
127.0.0.1:6379> llen myList # 查看链表长度
(integer) 3
3、hash
hash
是一种string
类型的filed
和value
的映射表,类似于Java8
之前的HashMap
,内部实现是数组和链表。适合于用于存储对象。
127.0.0.1:6379> hset 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"
4、set
set
类似于Java
中的HashSet
,是一种无序不重复的集合。同时set
提供了一个判断某个值是否存在于集合的接口。set
还可以实现交集、并集、差集的操作。
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"
5、sorted set
和set
相比,sorted set
提供了一个权重参数score
,让集合中的元素可以按照score
来排序。
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、bitmap
bitmap
存储的是连续的二进制数字(0 和 1),通过bitmap
, 只需要一个bit
位来表示某个元素对应的值或者状态,key
就是对应元素本身 。
# 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
五、Redis缓存数据过期时间的设置
1、为什么要设置过期时间
因为内存空间是有限的,如果缓存中的数据是一直保存的话,会导致out of menory
。
其次是业务场景需要,比如短信登陆验证码,是1分钟内有效的,就需要设置过期时间为1分钟。就不用我们自己在业务代码中来出来过期问题。
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
其中string
类型的数据有自己独特的setex
方法来设置过期时间,其他类型的数据都需要通过expire
来设置过期时间。
2、Redis是怎么判断数据过期的
Redis
是通过过期字典
来判断的(可以看作hash
表),过期字典
中的key
值指向Redis
数据库中某一个key
值(键),过期字典
中的value
则是存储的long
型数据,记录的是所指向key
(键)的过期时间。过期字典是存储在redisDb
中。
3、过期数据的删除策略
假如你给一个key
设置的过期时间是1分钟,那么1分钟后,Redis
是怎么处理这个数据的了?
惰性删除
只有在读取key
的时候才对key
进行过期检查删除。对cpu
友好,但是会导致有过多的过期数据没有删除。定期删除
每隔一段时间抽取一批key
来执行过期key删除操作。Redis
会限制删除操作的时长和频率来减少对cpu
的影响。
Redis
采用的策略是定期删除+惰性删除
。
但是仅仅给key
设置过期时间还是不够的,即使采用了定期删除+惰性删除
策略,还是可能会有一些数据没有被删除掉,导致out of memory
。为了解决这个问题,Redis
使用了内存淘汰机制
。
4、内存淘汰机制
(1)olatile-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
:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
(7)volatile-lfu(least frequently used)
:从已设置过期时间的数据集(server.db[i].expires)
中挑选最不经常使用的数据淘汰。
(8)allkeys-lfu(least frequently used)
:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
。
六、Redis持久化机制
持久化就是将数据从内存中写入到磁盘数据库中的,主要是为了重用数据(如重启服务,Redis挂了等)。
Redis实现持久化有两种操作方式:快照(RDB)
和只追加文件(AOF)
。
1、快照持久化RDB
Redis可以通过创建快照来存储内存中的数据在某个时间节点的副本。该方式是Redis默认的持久化机制,Reids.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命令创建快照。
2、AOF持久化
于快照持久化相比,AOF
的实时性更好,目前是主流的持久化方案。通过appendonly
参数开启。
appendonly yes
开启AOF
持久化后每执行一条会更改Redis
中数据的命令,Redis
都会将该命令写入到磁盘中的AOF
文件中。
Redis
配置文件中存在三种不同的AOF
持久化方案:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
比较常用的是everysec
。兼顾了数据和写入性能,用户最多会损失1秒中之内产生的数据。
七、Redis事务
Redis
通过MULTI、EXEC、DISCARD、WATCH
等命令来实现事务功能。
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
使用MULTI
来开启事务,Redis会将接下来的命令放入队列中,当调用EXEC
命令后会执行队列中的命令。调用DISCARD
命令会清除队列中的命令。
一般事务具有四大特性:原子性、隔离性、持久性、一致性
。
但是Redis
是不支持回滚的,所有Redis事务
不满足原子性
(也不满足持久性
)。
八、Redis缓存穿透
缓存穿透就是有大量的请求key
不在缓存中,直接请求到了数据库,相当于越过了缓存,直接访问数据库服务。
有两种解决方法:
1、缓存无效key
将缓存和数据库中都查不到的key
设置一个空值存在缓存中,并设置过期时间。这种方式是针对请求的key
变化不频繁的时候。若大量请求的key值都不一样,则会导致大量无效的空值key
存在缓存中。
2、使用布隆过滤器
什么是布隆过滤器:布隆过滤器
把所有可能存在的请求的值存放在布隆过滤器中,当用户请求时,先判断该请求是否存在于布隆过滤器,若不存在,则直接返回错误,则接着向下走缓存的流程。
九、Redis缓存雪崩
缓存雪崩是指缓存在同一时间大面积的失效,导致大量的请求直接交给数据库处理。
- 一种情况是
Redis
服务宕机了,导致所有的请求直接走数据库服务。
(1)可以采用Redis
集群,避免单机Redis
宕机而影响整个缓存服务。
(2)限流,避免同时处理大量的请求。 - 另一种情况是大量的热点数据在同一时间失效了(过期了),导致大量的请求访问数据库。
(1)给热点数据设置不同的失效时间。
(2)缓存永不失效。
十、Redis缓存和数据库一致性问题
如何保证缓存中的数据和数据库的数据一致了?
目前有三种常用的缓存读写策略
1、旁路缓存模式(Cache Aside Pattern)
旁路缓存模式
是比较常用的一种缓存读写模式,适用于大量读请求的业务。
旁路缓存模式中服务端需要同时维护数据库和缓存,并且以数据库中的数据为准。
- 写
(1)直接更新数据库。
(2)删除对应缓存。 - 读
(1)先读缓存,缓存中存在则直接返回数据。
(2)缓存不存在,则读取数据库返回值。
(3)将值更新到缓存中。
2、读写穿透模型(Read/Write Through Pattern)
读写穿透模型
是以缓存中的数据为主,从中读取数据和写入数据,缓存服务之后再将数据同步到数据库。
- 写
(1)先查缓存,缓存中不存在,则直接更新数据库。
(2)缓存中存在,则更新缓存,之后缓存服务自己将数据同步到数据库。 - 读
(1)从缓存中读取,存在的话直接返回。
(2)不存在,从数据库中加载,更新到缓存中后返回值。
3、异步缓存写入模型(Write Behind Pattern)
异步缓存写入模型
和读写穿透模型相似,都是由缓存服务来负责缓存和数据库的读写。不过异步缓存写入模型只更新缓存,数据库在之后由缓存服务异步批量执行更新。