Redis 简介
概念:Redis是用基于C语言开发的一个开源的高性能键值对(Key-value)数据库。
特征:
- 数据之间没有必然的关联关系
- 内部采用单线程机制进行工作
- 高性能。50个并发执行十万个请求,读的速度是11万次/秒,写的速度是8.1万次/秒
- 多数据类型支持
- 字符串类型 string 类似 String
- 列表类型 list 类似LinkedList
- 散列类型 hash 类似HashMap
- 集合类型 set 类似HashSet
- 有序集合类型 sorted_set 类似 TreeSet
- 支持持久化,不能老在内存中存着吧,断电了怎么办,所以支持持久化,还可以进行数据灾难恢复。
Redis 的使用场景
-
热点数据缓存
-
任务队列,比如秒杀、抢购、排队购票
-
即使信息查询,比如排行榜、网站访问量统计、公交到站信息、聊天室或网站的在线人数信息、设备信号
-
时效性信息,比如验证码的有效时间
-
分布式数据共享,比如分布式集群架构中的session分离
-
消息队列
-
分布式锁
Redis常用的数据类型
先说一下redis的数据存储格式
redis自身是一个Map,所有的数据都是使用key:value的形式存储,而我们说的数据类型是值value的类型,key的类型永远都是字符串。
![image-20210409202024012](https://i-blog.csdnimg.cn/blog_migrate/7082050bcffd8a47ee0f968d8ccf7f75.png)
字符串类型 string
基本操作
set key value 添加、修改数据
get key 获取数据
del key 删除数据
mset key1 value1 key2 value2.. 添加/修改多个数据
mget key1 key2... 获取多个数据
strlen key 获取数据字符串长度
append key value 追加信息到原始信息后面。如果不存在则新建
set 和 mset 该怎么选择?
set就是一次向redis服务器发送一条数据,mset就是一次性向redis服务器发送多条数据,所以发送数据的时候要考虑发送的时长的服务器处理的时长,如果一条数据非常大,发送的时长也非常长,那么无疑选择将大数据分割成一条条的小数据比较合适,具体的选择还需要通过实际测试对比一下才能做出选择。
string类型的扩展操作:
- 设置数据按照指定范围增加或者减少。
incr key // 让key对应的value加1
incrby key increment // 让key对应的value加increment
incrbyfloat key increment // 让key对应的value加一个小数
decr key
decrby key increment
string在redis内部存储默认就是一个字符串,但是遇到incr decr操作的时候会转换成数值进行加减操作。
在大型企业级应用中,分表操作是常见的,也就是使用多张表存储同类型的数据,但是对应的主键id必须保证唯一性,不能重复。因此,redis可以用于控制数据表的主键id,位数据库表主键提供生成策略,保障数据库表的主键唯一性。
**注意:**按照数值进行数据的操作的时候,如果原始数据不能转换成数值或者超越了redis数值上限的范围,将报错,上限的范围和Java中的long类型一样,Long.MAX_VALUE
- 设置数据具有指定的生命周期
redis控制数据的生命周期,通过数据是否失效控制业务行为。
setex key seconds value 例如:setex tel 10 17803831111 则tel在10秒后就没了
psetex key milliseconds value
string类型数据操作的注意事项
-
数据为获取到
nil 等同于null
-
数据最大存储量
512MB
string类型的业务场景案例
主页高频访问信息显示的控制,例如微博大V主页显示粉丝量与微博数量。
解决方案:在redis中为大V用户设定用户信息,以用户主键和属性值作为key,后台设定定时刷新策略即可
粉丝数: user:id:11211200:fans --> 1000000
博客数: user:id:11211200:blogs --> 2000
关注数: user:id:11211200:focuss --> 88
也可以使用json的格式存储,但是更改的时候需要先拿出来,比较麻烦
key的命名约定
数据库中的热点数据key命名惯例
表名:主键名:主键值:字段名
列表类型 list
操作具有先后顺序的数据
散列类型 hash
存储结构
![image-20210409211042144](https://i-blog.csdnimg.cn/blog_migrate/da24b82d8aae394ac6b97769a732101f.png)
![image-20210409211308421](https://i-blog.csdnimg.cn/blog_migrate/378a9e17ac2bbbae3e2caf2244170645.png)
一个key对应一堆数据,其底层使用的是哈希表结构
hash存储结构会自动优化:field少的时候,存储结构优化为类似数组结构;field多的时候,优化为hashmap结构
为什么需要hash类型?
-
新的存储需求
对一系列存储的数据进行编组,方便管理,可以应用于存储对象信息
-
需要的存储结构
一个存储空间可以保存多个键值对数据
基本操作
hset key field value 添加/修改hash的某个field值
hget key field 获取hash的某个field的值
hgetall key 获取key对应的hash结构的所有field值
hdel key field1 [field2] 删除数据
hmset key field1 value1 [field2 value2] 添加/修改多个数据
hmget key field1 field2 获取多个数据
hlen key 获取哈希表中字段的数量
hexists key field 哈希表中是否存在指定的字段
注意事项
- hash类型下的value只能存储字符串,不能存储其他类型,不能存在嵌套。
- 每个hash可以存储2^32 -1 个键值对
- hash类型十分贴近对象的数据存储形式,并且可以灵活添加删除对象属性。但是hash的初衷不是为了存储大量对象,不能说hash就是为了存对象用的
集合类型 set
set类型
存储大量的数据,高效的内部存储机制,在查询方面提供更高的效率
![image-20210410093640015](https://i-blog.csdnimg.cn/blog_migrate/8b8aaaefe091c4d74c60589fc56ce0e5.png)
其实就是基于hash的结构
支持随机获取集合中指定数量的数据
适用于随机推荐类信息检索
求交集、并集、差集
有序集合类型 sorted_set(zset)
![image-20210410100017025](https://i-blog.csdnimg.cn/blog_migrate/90a48b5bdd83c68419ac015c037f3f89.png)
各个数据类型使用场景总结
string
- 与计数相关热点数据,微博中明星的粉丝数量
hash
- 存储对象
- 存储购物车
list:应用于具有先后操作顺序的数据控制
- 微信朋友圈的点赞,按照点赞的顺序显示点赞好友的信息
- 按照关注的顺序显示出粉丝
- 最新消息的显示
set:
- 可以随机获取数据,可应用于随机推荐信息
- set可以求交集、差集、并集,可用于实现共同关注、共同好友
- 统计网站的PV(访问量)、UV(独立访客)、IP(独立IP)
zset
- 与排名相关的功能,比如电影热榜前十名
高级数据类型
Bitmaps
该数据类型存放的实际上就是字符串,但是它可以实现对位的操作。可以把Bitmaps想象成一个以位为单位的数组,每个数组的单元只能存储0或者1,数组的下标在Bitmaps中叫做偏移量。
该类型常应用信息状态统计
HyperLoglog
用来统计基数的个数。基数:数据集去重后的元素个数。
该数据类型不保存具体数据,只是记录数量,占用空间很小,最大只有12KB
GEO
GEO用于地理位置的计算
Redis持久化
什么是redis的持久化?
将redis中的数据保存在磁盘上,然后在特定的时间将保存的数据进行恢复的工作机制就是持久化
为什么要进行持久化?
防止数据的意外丢失,确保数据的安全性
持久化过程保存什么?
在redis中的持久化有两种形式:
- 将当前数据状态以快照的形式保存,存储格式简单,关注点主要在数据。(RDB)
- 将数据的操作过程以日志的形式保存,存储格式复杂,关注点主要在数据的操作过程(AOF)
RDB
存储到硬盘
1.手动保存
命令:save
每执行一次就会保存一次当前数据的快照信息
注意:redis是单线程的,当多个客户端并发向客户端发送指令的时候,会按照多个指定的到达顺序装进任务执行队列,当执行到save指令的时候,save指令的执行会阻塞当前redis服务器,直到但其概念RDB过程完成为止,如果数据量过大有可能会造成长时间阻塞,线上环境不建议使用。
**解决方案:**使用bgsave
指令让其在后台执行保存操作,不是立即执行,会在合理的时间进行执行,不用我们管。
其实现原理是:redis客户端向服务器发送bgsave
指令的时候,服务器给客户端返回一条后台保存开启的消息并且服务器会调用fork()函数生成一个子进程,让子进程去执行后台保存操作,子进程执行结束后向服务器发送执行完毕的消息,可以在日志文件中查看到该消息。
因此,bgsave
是针对save
的阻塞问题做的优化
2.自动保存
save second changes
注意该操作在后台执行的是bgsave指令
在指定时间范围内key的变化次数达到指定数量即进行持久化。 在配置文件中配置就行
例如 save 60 10
:在60秒内发生了10次变化就进行持久化,如果到达了时间,没有变化那么多次,就重新计时
RDB启动方式对比
方式 | save | bgsave |
---|---|---|
读写 | 同步 | 异步 |
阻塞客户端指令 | 是 | 否 |
额外内存消耗 | 否 | 是 |
启动新线程 | 否 | 是 |
RBD优缺点
优点:
- RDB存储的是紧凑的二进制压缩文件,存储效率较高
- 内部存储的是redis在某个时间点的数据快照,适用于数据备份、全量复制等场景
- RDB恢复数据要比AOF快
缺点:
- 无法做到实时持久化,很可能丢失数据,也就是宕机带来的数据丢失风险
- bgsave要创建子进程,所以会牺牲性能
- redis的各个版本中RDB文件格式不统一,可能存在各个版本间数据不兼容的现象
- 存储数据量大的时候,效率会比较低
恢复到内存
启动的时候自动恢复到内存
AOF
AOF持久化:以独立日志的方式记录每次的命令,重启时再重新执行AOF文件中的命令恢复数据。与RDB相比,就是改记录数据为记录数据产生的过程。
AOF的主要作用是可以做到数据持久化的实时性,目前已经是Redis持久化的主流方式
AOF写数据的三种策略
-
always(每次)
每次执行完写入操作都要同步到AOF文件中,数据零误差,性能较低,不建议使用
-
everysec(每秒)
每秒将AOF缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高,建议使用,也是默认配置
-
no(系统控制)
由操作系统控制每次同步到AOF文件的周期,整体过程不可控
Redis事务
Redis中的事务是什么?
redis中的事务就是将一系列的命令放进一个队列中,这个队列中的命令会按照顺序执行,在执行的过程中不会被打断或干扰。
事务的基本操作
- 开启事务:
multi
- 执行事务:
exec
开启事务之后输入的命令都不会立即执行,会先添加到队列中,当收到exec命令的时候才按照添加的顺序执行
锁
使用watch
对某个key加锁,在执行exec之前如果该key的值发生的变化,将终止事务的执行
Redis的删除策略
##数据删除策略
数据删除策略的目标是:**在内存占用和CPU占用之间寻找一种平衡。**因为这两者无论哪一个走极端都会造成redis性能的下降。
定时删除
创建一个定时器,由定时任务对到达过期时间的key执行删除操作。
优点:能够及时删除过期的key,释放内存
**缺点:**如果CPU很忙的时候有key过期,CPU也得立马去执行删除操作,增大了CPU的压力
这是一种以时间换空间的方式。
惰性删除
如果有key过期,不做处理。放下次访问redis中的数据时,如果该key没过期,直接返回;如果该key过期了,则删除该key,返回不存在该数据。
**优点:**减轻了CPU压力,等到发现必须删除的时候才会删除。
**缺点:**增大了内存占用
这是一种以空间换时间的方式。
定期删除
以上两种方式,都各有弊端,定期删除是一种折中的方式。
每隔一段时间,对数据库进行一次检查,删除过期键,由具体的算法决定删除多少过期键和检查多少数据库
注意:
如果删除太频繁会退化成定时删除,删除次数太少会退化成惰性删除。
##逐出算法(策略)
当redis中有新数据进来,如果内存不足,就要清理掉部分数据腾出内存,具体要清理掉那些数据就是由逐出算法决定。
列举八种数据逐出策略:
- 检测易失数据
- volatile-LRU : 挑选最近最久使用的数据淘汰
- volatile-LFU:挑选最近使用次数最少的数据淘汰
- volatile-TTL:挑选将要过期的数据淘汰
- volatile-random:选择任意数据淘汰
- 检索全库数据
-
allkeys-LRU: 挑选最近最久未使用的数据淘汰
-
allkeys-LFU:挑选最近使用次数最少的数据淘汰
-
allkeys-random:任意选择数据淘汰
- 放弃数据驱逐
- no-enviction:禁止逐出数据(redis4.0中的默认策略),引发错误OOM
主从复制
简介
首先说一下互联网的“三高”架构:
- 高并发
- 高性能
- 高可用
单机的Redis存在的风险与问题:机器故障、容量瓶颈。为了避免这些问题,就需要准备多态服务器,互相连通,将数据复制多个副本保存在不同的服务器上,连接在一起,并且保证数据是同步的。这样即使有一台服务器宕机了,还可以有其他机器支撑,继续提供服务,从而实现redis的高可用,同时实现数据的冗余备份。
这时候就要用到主从复制:将主服务器中的数据及时有效地复制到slave中
- 主服务器(master):提供数据。职责:负责写数据,并将发生变化的数据自动同步到从服务器中
- 从服务器(slave):接收数据。职责:负责读数据
将master中的数据复制到slave中。
主从复制的作用
- 读写分离:主服务器写,从服务器读,提高服务器的读写负载能力
- 负载均衡:slave分担master的负载,并根据需求的变化,改变slave的数量,大大提高redis服务器的并发量和数据吞吐量
- 故障恢复:当master出现问题时,由slave服务器提供服务,实现快速的故障恢复
- 数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式
- 高可用基石:基于主从复制,构建哨兵模式和集群,实现redis的高可用方案
主从复制工作流程
大体上可分为三个阶段:建立连接阶段(准备阶段)、数据同步阶段、命令传播阶段
阶段一 :建立连接阶段
建立slave到maser的连接,使master能够识别slave,并且保存slave端口号。
阶段二:数据同步阶段
在slave初次连接到master后,会将master中的所有数据复制到slave(全量复制)
将slave的redis数据库状态更新成master当前的数据库状态(数据恢复)
阶段三:命令传播阶段
当master数据库数据发生改变的时候,需要将这种改变同步到slave数据库,同步的动作就是命令传播。
master数据更改的命令发送给slave,slave接收到命令后执行命令
哨兵模式
哨兵模式简介
哨兵(sentinel)是一个分布式系统, 用于对主从结构中的每台服务器进行监控,当出现故障的时候,通过投票机制选择新的master并将所有slave连接到新的master。
哨兵的作用
也是哨兵在进行主从切换的过程中经历的三个阶段:
-
监控
不断检查master和slave是否正常运行
-
通知(提醒)
当被监控的服务器出现问题的时候,向客户端发出通知
-
自动故障转移
master出问题的时候,选取一个slave作为新的master,将其他slave连接到新的master,并告知客户端新的master的地址
注意:哨兵也是一台redis服务器,只是不提供数据服务而已。通常哨兵的数量是单数
Redis集群
集群的概念
集群就是使用网络将多个计算机连接起来,并统一管理提供服务,对外呈现出单机的效果。
集群的作用
- 分散访问压力,实现负载均衡
- 分散存储压力,实现可扩展性
- 降低单台服务器宕机时带来的业务灾难
集群下的数据存储
- 通过特定算法,计算出key的存放位置
- 将所有的存储空间分割成16384分,每台主机保存一部分。其中每份代表一个存储空间,并不是一个key的保存空间
- 将key按照计算出的结果放到对应的存储空间
集群内部的通信
各个数据库相互通信,保存各个库中槽的编号,查询的时候若能够一次命中则直接返回,一次未命中的时候,会告知该key存放的位置,再次查找。
问题解决方案
缓存预热
针对现象
服务器启动后迅速宕机
问题排查
- 请求数量较多
- 主从数据库之间数据吞吐量较大,数据同步操作频率较高
解决
缓存预热:在系统启动之前,提前将相关的缓存数据加载到缓存系统,避免在用户请求到来的时候先查询数据库再将数据缓存的问题,让用户直接查询事先缓存好的数据。
缓存雪崩
缓存雪崩就是大量的数据瞬间过期,导致对数据库服务器造成压力。如果能将过期的时间错开,可以有效并避免这个问题。
解决:
- 避免对大量的key设置相近的失效期
- 超热数据使用永久key
- 加锁
缓存击穿
缓存击穿就是单个数据过期的瞬间,对该数据由大量的请求,由于无法再redis命中,所有向数据库发起了对该数据的大量请求,对数据库造成压力。
解决:
- 加锁
- 设置永久key
缓存穿透
缓存穿透就是访问不存在的数据,每次都不经过redis直接访问数据库,对数据库造成压力,另外由于缓存一直不命中,也失去了缓存的意义。
解决
-
由于查询的是不存在的数据,所以会返回null,我们将返回的null也进行缓存,有效时间设置短一点就好了。
-
白名单策略
- 提前把分类数据id映射到bitmaps,id作为bitmaps的offset,相当于设置了数据白名单,当加载正常数据的时候就放行,加载异常数据的时候就拦截。不过这种方式效率比较低
- 使用布隆过滤器(下面有介绍什么是布隆过滤器),将数据库中所有的查询条件放入缓存数据库中,当一个请求过来时,先经过布隆过滤器进行筛查,如果判断请求查询的值可能存在,则继续查;如果判断请求的查询值一定不存在,则直接拦截。
扩展:布隆过滤器
什么是布隆过滤器?
布隆过滤器是由一个很长的bit数组和一组哈希映射函数组成,可以用于检索一个元素是否一定不在集合中或者可能在集合中。
布隆过滤器的原理?
布隆过滤器是一个bit数组,如果我们向把一个key映射到布隆过滤器中,就利用所有的哈希函数对这个key进行映射得到一系列的哈希值,将哈希值当成数组的下标,把下标对应的数组元素置为1。那么当判断一个key是否在bit数组中的时候,就判断以该key经过哈希函数映射的哈希值为下标的元素是不是都为1,如果都为1,那么该key可能在集合中;如果有任何一个不为1,那么该key一定不在该集合中。
为什么全部为1,可能在集合中?
因为经过哈希映射是可能存在哈希冲突的,在发生哈希冲突的时候,如果刚好所有的映射值都被设置为了1,就会影响判断结果,这也是布隆过滤器的一个缺点,为了做到时间和空间上的高效率,布隆过滤器选择牺牲一点准确率。还有一个缺点是,布隆过滤器的删除比较困难,如果要删除一个key的映射,不能简单的把该key哈希值对应位置上的元素置为0,因为要删除的这个key可能会和其他key发生了哈希冲突,另外的key也映射到了该位置,那么直接置为0的话就会影响其他key的判断。
其他应用场景:
- Google Chrome 使用布隆过滤器识别恶意 URL
- 使用布隆过滤器避免推荐给用户已经读过的文章/视频
- 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤
的bit数组和一组哈希映射函数组成,可以用于检索一个元素是否一定不在集合中或者可能在集合中。
布隆过滤器的原理?
布隆过滤器是一个bit数组,如果我们向把一个key映射到布隆过滤器中,就利用所有的哈希函数对这个key进行映射得到一系列的哈希值,将哈希值当成数组的下标,把下标对应的数组元素置为1。那么当判断一个key是否在bit数组中的时候,就判断以该key经过哈希函数映射的哈希值为下标的元素是不是都为1,如果都为1,那么该key可能在集合中;如果有任何一个不为1,那么该key一定不在该集合中。
为什么全部为1,可能在集合中?
因为经过哈希映射是可能存在哈希冲突的,在发生哈希冲突的时候,如果刚好所有的映射值都被设置为了1,就会影响判断结果,这也是布隆过滤器的一个缺点,为了做到时间和空间上的高效率,布隆过滤器选择牺牲一点准确率。还有一个缺点是,布隆过滤器的删除比较困难,如果要删除一个key的映射,不能简单的把该key哈希值对应位置上的元素置为0,因为要删除的这个key可能会和其他key发生了哈希冲突,另外的key也映射到了该位置,那么直接置为0的话就会影响其他key的判断。
其他应用场景:
- Google Chrome 使用布隆过滤器识别恶意 URL
- 使用布隆过滤器避免推荐给用户已经读过的文章/视频
- 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤
- 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱