Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库
是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向
Redis 除了做缓存之外,也经常用来做分布式锁,甚至是消息队列
Redis 还支持事务 、持久化、Lua 脚本、多种集群方案等,可以使用众多复杂的业务场景
缓存使用的比较多的主要是 Memcached 和 Redis。不过现在使用 Memcached 做缓存的比较少
Memcached 是分布式缓存最开始兴起的时候比较常用的方案,后来随着 Redis 的发展,Redis 逐渐成为了人们的首选
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题。因为本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共享的
Redis 支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zsetsorted set:有序集合)
实际中我们最为常用的还是 string 类型,不过还有几种高级数据类型我们也需要了解掌握:BloomFilter,RedisSearch,Redis-ML,还有更高级的数据类型,BloomFilter,RedisSearch,Redis-ML 如果使用过,那么在面试官眼里都是加分项!
缓存的简单处理流程如下:
Redis 是单线程,利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销问题
512 M
将高频访问的数据放进缓存,保证高频操作可以快速响应,提高系统响应速度和用户体验
一般像 MySQL 这类数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(redis 集群的话会更高)
QPS(Query Per Second):服务器每秒可以执行的查询次数
所以增加缓存可以大大提高系统的并发能力
缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加 快数据的访问速度,而且能够有效地降低后端数据源的压力。Redis提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。可以这么说,一个合理的缓存设计能够很好的为一个网站的稳定保驾护航
消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务 解耦、非实时业务削峰等特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足
我们可以站在数据规模和数据冷热的角度来进行分析
站在数据规模的角度看,数据可以分为大规模数据和小规模数据,我们 知道 Redis 的数据是存放在内存中的,虽然现在内存已经足够便宜,但是如果数据量非常大,例如每天有几亿的用户行为数据,使用 Redis 来存储的话,基本上是个无底洞,经济成本相当的高
String 数据结构是简单的 key-value 类型,也是我们平时使用的最多的数据类型
常用命令:set,get,strlen,exists,decr,incr,setex 等等
应用场景:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等
基本操作
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)Copy to clipboardErrorCopied
批量设置
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"Copy to clipboardErrorCopied
计数器设置
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"Copy to clipboardErrorCopied
过期设置
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) 56Copy to clipboardErrorCopied
list 即是链表,链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难
常用命令:rpush,lpop,lpush,rpop,lrange,llen 等
应用场景:发布与订阅或者说消息队列、慢查询等
队列和栈的实现
通过 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"Copy to clipboardErrorCopied
通过 rpush/rpop 实现栈
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"Copy to clipboardErrorCopied
hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表),不过 Redis 的 hash 做了更多优化。另外 hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存 储对象,比如我们可以用 hash 数据结构来存储用户信息,商品信息等等
常用命令:hset,hmset,hexists,hget,hgetall,hkeys,hvals 等
应用场景:系统中对象数据的存储
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"Copy to clipboardErrorCopied
set 类似于 Java 中的 HashSet,Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。如:可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能
常用命令:sadd,spop,smembers,sismember,scard,sinterstore,sunion 等
应用场景:需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
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"Copy to clipboardErrorCopied
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表
常用命令:zadd,zcard,zscore,zrange,zrevrange,zrem 等
应用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息等信息
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"Copy to clipboardErrorCopied
bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间
常用命令: setbit 、 getbit 、 bitcount 、 bitop
应用场景: 适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计
# 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) 2Copy to clipboardErrorCopied
使用单线程的原因
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个是 Redis 中的一个性能瓶颈
虽然 Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,很快就会 Out of memory 了
Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外 persist 命令可以移除一个键的过期时间
还有一种情况需要设置过期时间,当我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效,此时设置过期时间就能比较好的处理,如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多
Redis 提供 6 种数据淘汰策略:
4.0 版本后增加以下两种:
(1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru
(2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random
Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用
rdb是直接把缓存中的数据写入内存磁盘当中的
与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案,是指所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储)保存为 aof 文件
aof持久化以日志的形式记录服务所处理的每一个写入和删除的操作,不记录查询操作,以文本的方式记录
默认没有开启 AOF,可以通过如下命令开启
appendonly yesCopy to clipboardErrorCopied
两种持久化方案对比:
ROB | AOF | |
原理 | 数据直接保存在内存中 | 记录操作数据的写入和删除的操作日志 |
优点 | 相比AOF,效率更高 | 几乎是实时的持久化,误操作后,只要没有rewrite,就可恢复 |
缺点 | 无法保证数据的完整性 | 文件占用空间大,恢复数据慢 |
Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程
Redis 主从同步有新旧两种方案,具体看下面图示
老方案:
新方案:
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。比如某个攻击者故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库上,使得数据库由于性能的原因崩溃,导致系统异常
缓存穿透问题产生
解决办法:
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,可以解决请求的 key 变化不频繁的情况,如果攻击者的恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程
缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求
还有一种缓存雪崩的场景是:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上
以上两种常见,就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了
针对 Redis 服务不可用的情况:
针对热点缓存失效的情况:
如果缓存中的某个热点数据key过期失效了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题
击穿其实可以看做是雪崩的一个子集,解决方法一般有两种,设置热点数据永不过期和设置互斥锁
所谓的互斥锁,就是保证同一时间只有一个业务线程更新缓存,对于没有获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
其实这是一个非常庞大且复杂的问题,根本不是一两句话能够说清楚的,如果要完全规避一致性问题,那么整个系统也会变得非常复杂
一般来说可以进行如下处理:更新 DB,然后删除 cache
当然此时有可能遇到更新 DB 成功,但是删除 cache 失败的情况,处理办法大致有两种:
Redis 事务可以一次执行多个命令,并且带有以下三个重要的保证:
一个事务从开始到执行会经历以下三个阶段:
命令:
(1)Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master,继续提供服务
(2)Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储
集群之间是异步复制的,最大节点个数为16384个,Redis 集群目前无法做数据库选择,默认在 0 数据库
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有 N-1 个复制品
Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面
理论上 Redis 可以处理多达 232 的 keys,并且在实际中进行了测试,每个实例至少存放了 2 亿 5 千万的 keys。任何 list、set、和 sorted set 都可以放 232 个元素。换句话说,Redis 的存储极限是系统中的可用内存值
正确设置内存淘汰策略,当 Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略,从而剩下的数据就是热点数据
使用 keys 指令可以扫出指定模式的 key 列表,但是,keys 指令会导致线程阻塞一段时间,此时整个 Redis 系统不再能接受其他指令
这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长,但是安全性更高!
如果大量的 key 过期时间设置的过于集中,到过期的那个时间点,Redis 可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些
通过 SETNX 来争抢锁,再用 EXPIRE 给锁加一个过期时间,当然为了保证 SETNX 和 EXPIRE 原子性执行,在 Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,用这一条命令就可以了:
127.0.0.1:6379> SET lock 1 EX 10 NX
OK