Redis学习笔记
五大常用数据类型
String
一个Redis中字符串value最多可以是512M
数据结构
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
List
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
数据结构
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
Set
set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
数据结构
Redis的Set是string类型的无序集合。它底层其实是一个所有value都指向同一个内部值的hash表,所以添加,删除,查找的复杂度都是O(1)。
Hash
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
数据结构
Redis 的字典使用哈希表作为底层实现, key 用来保存键,val 属性用来保存值.
注意这里还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。这里采用的便是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突。
解决哈希冲突的办法
开放定址法
若产生冲突, 则线性探测得到的下一个地址, 若依然冲突则继续寻址, 直到地址中内容为空
再哈希法
对产生地址冲突的关键字再次进行哈希计算,获取另一个哈希地址,直到不再产生冲突,这种方法不易产生“二次聚集”,但是增加的计算的时间。
链地址法(拉链法)
将元素为同一地址i的通过单链表链接,将单链表的头指针放在哈希表的第i个单元中,因而插入和删除较方便。
建立公共溢出区
顾名思义,在创建哈希表时,同时创建另一个表,将所有发生哈希冲突的记录都存储到溢出表。
Zset(sorted set)
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的 。
数据结构
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
Redis单命令的原子性主要得益于Redis的单线程。
新数据类型
Bitmaps
可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
实例:
每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。
设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图
发布订阅模式
# 打开客户端订阅一个channel
SUBSCRIBE channel1
# 打开另一个客户端,给channel1发布消息hello
publish channel1 hello
# 打开第一个客户端可以看到发送的消息
# 注: 发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息
事务
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队
从输入Multi
命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec
后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard
来放弃组队。
错误处理
- 组队阶段(未执行)
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
- 执行阶段
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
WATCH
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。类似乐观锁
事务特性
- 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
持久化机制
RDB(Redis DataBase)
在指定的时间间隔内将内存中的数据集快照写入磁盘, 它恢复时是将快照文件直接读到内存中
备份是如何执行的
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
持久化的文件名字为:dump.rdb
如何触发备份
- 自动
# after 900 sec (15 min) if at least 1 key changed
save 900 1
# after 300 sec (5 min) if at least 10 keys changed
save 300 10
# after 60 sec if at least 10000 keys changed
save 60 10000
- 手动
SAVE 保存是阻塞主进程,客户端无法连接redis,等SAVE完成后,主进程才开始工作,客户端可以连接
BGSAVE 是fork一个save的子进程,在执行save过程中,不影响主进程,客户端可以正常链接redis,等子进程fork执行save完成后,通知主进程,子进程关闭。很明显BGSAVE方式比较适合线上的维护操作
优点
-
适合大规模的数据恢复
-
对数据完整性和一致性要求不高更适合使用
-
节省磁盘空间
-
恢复速度快
缺点
-
Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
-
虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
-
在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
AOF(Append Only File)
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作. AOF默认不开启
持久化流程
(1)客户端的请求写命令会被append追加到AOF缓冲区内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
Rewrite(重写机制)
当AOF文件的大小超过所设定的阈值(默认64M)时, 会fork出一条新进程来将文件重写, 只保留可以恢复数据的最小指令集
优点
-
备份机制更稳健,丢失数据概率更低。
-
可读的日志文本,通过操作AOF稳健,可以处理误操作。
缺点
-
比起RDB占用更多的磁盘空间。
-
恢复备份速度要慢。
-
每次读写都同步的话,有一定的性能压力。
RDB和AOF用哪个比较好
若只作为缓存数据库, 则两个都不需要开启
否则官方建议两个都开启. 混合机制. RDB用作备份, 对指定间隔时间段的数据进行备份, AOF实时记录每次的写入操作
若都开启, 那么AOF在重写时, 也会读取RDB文件中的数据并写入AOF, 把RDB备份后面的指令追加到AOF, 所以一个备份文件里面既有RDB的数据, 也有AOF的数据, 恢复时就很快
缓存穿透/击穿/雪崩
缓存穿透
大量请求不存在的key, 导致每次从缓存中获取不到从而访问数据库, 造成数据库压力增大, 也能是由于爬虫或者黑客攻击导致.
解决方案
(1)对空值缓存
如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2) 设置可访问的名单(白名单)
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3) 采用布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
(4)进行实时监控
当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
缓存击穿
某个热点key过期了, 突然大量该key的请求过来, 从缓存中没有获取到, 都跑到了DB上
解决方案
(1)预先设置热门数据
在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)进行监控, 实时调整
现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁
第一个请求在缓存中获取不到时, 先上锁(比如Redis的SETNX), 再去DB进行访问. 此时其他请求需要再外面等待第一个请求解锁.
第一个请求从DB获取到结果后, 存入缓存中, 解锁. 其他请求就可以直接从缓存中拿到数据了.
缓存雪崩
大量热点key在同一时间段失效, 此时大量请求访问时, 都跑到了DB上
解决方案
(1)构建多级缓存架构
nginx缓存 + redis缓存 +其他缓存(ehcache等)
(2)使用锁或队列
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3)设置过期标志更新缓存
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4)将缓存失效时间分散开
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
Redis是单线程还是多线程
首先我们说的单线程还是多线程是指Redis的worker线程, 而不是说整个Redis只有一个线程在工作, Redis的worker线程需要做三步操作:
第一步: 读取客户端发送过来的命令IO流
第二步: 执行命令
第三步: 将执行结果通过IO流返回给客户端
在Redis6.0版本前, 每条命令的三步都是串行在单线程操作的, 在6.0版本引入了IO多线程的概念, 即把第一步和第三步的IO操作改成了多线程, 每链接一个客户端, 就开辟一个线程进行IO操作, 但执行计算的操作还是交给worker单线程串行执行. 另外,多线程IO默认也是不开启的,需要再配置文件中配置
Redis为什么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
2、专门设计的数据结构,性能更高
3、worker线程采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
多路I/O复用模型
下面举一个例子,模拟一个tcp服务器处理30个客户socket。
假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:
- 第一种选择:按顺序逐个验收,先验收A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误。
这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。(select/poll) - 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。(多进程多线程)
- 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。(epoll)
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求
Redis如何删除/淘汰过期的key
-
定期删除: 每隔一段时间定期抽取一些key判断是否过期, 过期则删除, 会给CPU带来负担
-
惰性删除: 当访问到key时, 如果发现key过期, 那么将key删除
-
内存不够时删除: 即内存淘汰机制, 主要分为三类:
第一类: 不处理, 内存不够时直接报错
- noeviction: 发现内存不够时,不删除key,执行写入命令时直接返回错误信息。(Redis默认的配置就是noeviction)
第二类: 从所有的key中挑选, 进行淘汰
- allkeys-random 就是从所有的key中随机挑选key,进行淘汰
- allkeys-lru 就是从所有的key中挑选最近使用时间距离现在最远的key,进行淘汰
- allkeys-lfu 就是从所有的key中挑选使用频率最低的key,进行淘汰。
第三类: 从设置了过期时间的key中挑选,进行淘汰
- volatile-random 从设置了过期时间的结果集中随机挑选key删除。
- volatile-lru 从设置了过期时间的结果集中挑选上次使用时间距离现在最久的key开始删除
- volatile-ttl 从设置了过期时间的结果集中挑选可存活时间最短的key开始删除(也就是从哪些快要过期的key中先删除)
- volatile-lfu 从过期时间的结果集中选择使用频率最低的key开始删除
缓存淘汰机制
FIFO算法
按照“先进先出(First In,First Out)”的原理淘汰数据,正好符合队列的特性,数据结构上使用队列Queue来实现。
LRU算法
Least recently used,最近最少使用. 其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”
与FIFO算法不同的是, 当访问一个key缓存命中时, 会把这个key移动到链表的头部, 而FIFO则不会做这一步操作
实现思路是使用hash表和双向链表配合使用. hash表用来保存访问的key, value为链表的Node, 通过链表进行node的移动
LFU算法
Least Frequently Used 最不经常使用. 其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”
LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。
如何进行缓存预热
系统上线时,提前将相关的缓存数据直接加载到缓存系统。如何知道哪些key需要预热
- 通过业务预测key或将已知的key进行预热
- 使用redis-faina等工具实时监控Redis热key, 预热时可能发生缓存击穿和雪崩问题
Redis实现分布式锁
分布式锁的三种实现方式:
- 基于数据库实现分布式锁(一般不使用)
- 基于缓存(Redis等)(性能最高)
- 基于Zookeeper(可靠性最高)
set sku_1 “OK” NX PX 10000
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
方式一:只setnx
只使用nx命令进行加锁和释放锁
会带来问题是获取锁后, 处理时间比较长或者处理出现异常, 导致锁无法正常释放.
方式二:设置setnx时加上锁的过期时间
在setnx锁的同时加上锁的过期时间set sku_1 “OK” NX PX 10000
, 不建议分两步使用expire
设置过期时间, 因为缺乏原子性
当程序A拿到锁后, 如果过期时间设置的不合理或者程序A出现网络波动等问题, 导致程序还没有执行完, 还没有执行到释放锁的代码del sku_1
, 就到了锁的过期时间从而锁被迫释放, 然后程序B抢到了锁, 但是B还没有执行完, 此时程序A网络正常了, 代码继续往下执行, 执行到释放锁的代码del sku_1
时, 就会误把此时的程序B上的锁给释放掉了.造成错误释放
方式三:加上UUID区分不同程序的锁
针对上面的问题, 可以在每次程序拿到锁时, 设置锁的值为当前程序生成的UUID(唯一标志), 等到执行释放锁时, 重新获取一下改锁的值, 判断是否为自己的UUID, 即可避免错误释放的问题
但是由于判断UUID和释放锁这两步操作并不是原子性的, 有可能在程序A判断当前值是自己的UUID之后, 准备执行释放锁时, 锁刚好过期了, 此时立马被程序B拿到了锁, 然后A程序还是会把B程序的锁给释放掉了. 因此不能完全避免错误释放
方式四:使用LUA脚本优化UUID策略
上述问题出在判断UUID和释放锁这两步操作并不是原子性的, 而在Redis中可以通过LUA脚本来实现原子性操作
Redis官方文档对Lua脚本原子性的解释是Redis采用相同的Lua解释器去运行所有命令,我们可以保证,脚本的执行是原子性的。作用就类似于加了MULTI/EXEC。
Redis主从复制
slaveof <主ip> <主port>
在从机执行上述命令, 即可建立简单的主从关系. 特点为:
- 只能在主机进行写入操作, 在从机执行写入会报错
- 主机挂掉后, 主机和从机还会保持自己的角色, 主从关系不会变动, 重启主机即可
- 从机挂掉后, 重启时会回到自己为master的角色, 因此需要重新执行
slaveof
, 重新主从链接. 也可以通过设置配置文件, 永久生效, 重启后不需要再手动设置主从关系
主从复制原理
- 从机连接到主机后, 会发送一个sync命令
- 主机接到命令后, 会进行一次RDB持久化, 并把持久化文件发送给从机
- 从机接收到RDB文件后会将其加载到内存中
- 后续主机执行写入命令时, 会将指令同步发送给从机, 从机执行相应命令
主从模式
一主多从
一台主机链接多台从机, 当从机机器较多时, 同步会比较慢
主从连续
一台主机关联一台或者少数从机, 从机下面也可以继续挂下一级从机. 可以缓解只有一台主机的同步压力, 但如果中间有机器挂掉了, 其下面的从机也无法进行同步
反从为主
当一个master宕机后,后面的slave可以手动执行slaveof no one
命令, 立刻升为master,其后面的slave不用做任何修改。
哨兵模式
能够检测主从连接, 当主机挂掉后, 立刻根据投票将某个从机提升为主机. 若原主机重启了, 则原主机会变成从机
配置哨兵配置文件
sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
启动哨兵
redis-sentinel xxx/sentinel.conf
选举策略
选择条件依次为:
- 选择优先级靠前的, 优先级
replica-priority
可以在配置文件中配置, 默认100, 值越小则优先级越高 - 选择偏移量最大的, 即与主机数据量最接近的从机
- 选择runid最小的, 每个redis实例启动后都会随机生成一个40位的runid
Redis集群
Redis提供了无中心化集群的配置
一个Redis集群共有16384个槽(slot), 数据库中每个键都会占用一个槽位, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 每个集群节点都会分配这16384个槽的一部分, 这样根据计算key的槽位来分配这个key应该放入哪个节点中