书单:
《 Redis的设计与实现》
《Netty、Redis、Zookeeper高并发实战》
1. Redis是什么
Redis是一个C语言开发的高性能Key-Value数据库。
2. Redis的使用场景
- 缓存
- 分布式锁
- GEO可以支持地理位置搜索
- 消息订阅
2.1缓存
2.1.1 缓存穿透
不存在的kye,缓存穿透指的客户端不断的查询一个数据库不存在的值,多次缓存都没有命中,缓存失去了存在的意义,导致DB不断被直接访问,产生很大压力,就是缓存穿透。
解决
- 布隆过滤器 ->将不存在的数据在布隆过滤器中做过滤
- 存储空值->直接将不存在的数据也进行存储,只不过是空值
以上方法虽然可以解决缓存穿透,但是有可能会有数据一致性的问题。
2.1.2 缓存击穿
存在的key,热点数据缓存失效后,大量请求并发访问,同时去缓存数据,导致的数据库压力暴增。
解决
2.1.3 缓存雪崩
大量数据同时过期,然后去缓存的适合,引发大量访问数据库,数据库压力过大甚至down机
解决
- 随机时间过期
- 缓存永不过期策略
- 缓存添加锁机制和重试,保证某一个缓存在同一时刻,只能被一个线程缓存,
- 对于部分新功能增加的热点缓存,可以在程序启动后主动进行缓存 ,进行数据预热
- Redis高可用,(集群,见下面集群篇)
2.2 缓存一致性的问题
注意,无论是先写缓存后写数据库,还是先写数据库后写缓存,都是有问题的,因为读写是并发的,都会出现数据不一致的情况。
1. 写后删除
在写入数据库成功后,删除缓存,这样在数据访问的适合,重新缓存,就能获取到最新的数据。这种其实也是有几率失败了,如一个读操作没有命中缓存,然后准备存储空的时候,写操作执行了删除,然后读操作在写缓存的时候就是错误的。
2. 延时双删
为了解决上面的问题,可以使用延时双删的策略,一般是 删除缓存->更新数据->sleep(n) ->删除缓存的方式解决
3. 只写缓存
只写缓存,然后异步更新到mysql类似与mq,msyql之类的刷盘机制,即我们只操作缓存,然后通过异步的方式,去更新数据库
4.借助于canal工具更新
程序只写入数据库,然后另外的线程监听canal,更新缓存
这里学习了左耳耗子的博客 https://coolshell.cn/articles/17416.html
2.2 分布式锁
分布式锁需要满足的条件:
- 存储空间 -> zai
- 唯一标示
- 至少两个可变状态
分布式锁核心概念
- 加锁
- 解锁
- 锁超时
2.2.1 Redis分布式锁
- 加锁
基于set 命令的NX PX/EX 选项来实现加锁(在2.6之后可以用,2.6之前需要使用lua)
NX:不存在才设置
PX/EX:超时时间(区别 一个毫秒/一个秒)
value: 唯一标示字符串,在解锁的时候使用 - 解锁
需要使用LUA脚本,将加锁时候Value的唯一字符串进行判断,如果相同,则说明是当前锁,
redisson实现了基于Redis的分布式锁
2.2.2 ZK实现的分布式锁
最基本的是利用ZK节点不能重复的特性,来实现分布式锁,但是这样可能产生惊群效应
ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。
ZK临时顺序节点是天然的发号器
ZK的递增节点可以保证锁的公平性
ZK的节点监听可以保证节点的锁的传递
ZK的节点监听可以避免惊群效应
加锁过程
- 在一个永久节点下面创建一个临时节点,然后判断自己是否是最小的那个节点,如果不是,就监听前一个节点
- 如果监听到前一个节点释放,就判断自己是否是最小的那个节点,如果是则加锁成功,否则持续监听,
- 完成后删除自己的节点,进行锁释放工作
缺点
性能不高
Menagerie 实现了基于ZK的分布式锁
分布式锁选择
并发高,性能要求高redis ,高可靠,高可用,公平,ZK
- 基于Redis的分布式锁。适用于并发量很大、性能要求很高而可靠性问题可以通过其他方案去弥补的场景。
- 基于ZooKeeper的分布式锁。适用于高可靠(高可用),而并发量不是太高的场景。
基础知识
数据库
Redis服务器默认会创建16个数据库,自定义是由 db num属性决定的
数据库键空间
- dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)
- 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime命令可以查看键key的闲置时间。
- 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看。
key的过期时间
- redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)
- 过期字典的值是一个long 类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳。
过期键的判定
1)检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。2)检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
过期键的删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存是友好的,可以保证键被尽快删除,但是如果过多键同时到期,对cpu极度不友好,影响服务器的响应时间。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对内存不友好,可能造成内存中存在很多过期的数据,有内存泄漏的危险。但是对cpu是友好的,因为只有在用的时候才回去清楚。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
AOF和RDB对过期的处理
- AOF: 在键没有被删除的时候是没有任何影响的,删除的键会产生一个DEL语句,在AOF重写的过程中,会把DEL的语句优化掉。
- RDB: 过期的键不会载入RDB文件,从RDB文件加载的时候,过期的键也不会加载到内存
主从复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制,从服务器不会去主动删除。当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键
持久化
RDB
- 服务器中的非空数据库以及它们的键值对统称为数据库状态。
- RDB将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
- RDB可以手动执行也可以自动执行
- SAVE命令 和 BGSAVE命令都可以生产RDB文件。
- SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:
- BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求:
-
如果开启AOF,则默认使用AOF,因为AOF更新频率更高
- 服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
- 自动间隔性保存:服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。包括像多少时间进行了多少次修改,
- Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
- 对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。
AOF
- AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的
- AOF写入时机:每次在结束文件事件前,都会考虑是否把新内容添加到AOF缓冲区中,AOF缓冲区有三种方式刷盘,1 每次刷盘 2. 每隔1s刷 3. 等缓冲区满后刷新
- AOF文件的读取,创建伪客户端,由伪客户端一条一条的执行AOF日志,直到执行完毕
AOF重写
- 重写是在子进程中完成
- 重写和原AOF文件无关,根据数据库当前状态,生成一份新的AOF文件
- 在重写的过程中,会维持一个重写缓冲区,在重写过程中执行的命令都是在缓冲区进行记录,在重写完成后追加到新文件结尾,然后替换旧的文件,完成一个重写。
事件
Redis服务器是一个事件驱动程序,服务器需要处理文件事件和事件事件
文件事件
- 文件事件处理器是基于Reactor模式实现的网络通信程序。
- 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
-
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
- 当多个IO事件发生的时候,IO复用程序会将所有的套接字放在一个队列中,以有序,同步,一个一个的处理套接字。当上一个处理完成之后,才会去处理下一个套接字,这也就是为什么总有人说Redis是单线程的原因。
时间事件
- 服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件。
事件的调度与执行
- 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占。
- 时间事件的实际处理时间通常会比设定的到达时间晚一些。
serverCron函数
Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。
更新服务器时间缓存
- 在精度要求不高的情况下,如果需要获得时间信息,Redis是直接从缓存获取,一般包括日志,是否持久化等情况下
更新LRU时间信息
- 每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间
Redis的淘汰策略
- 从已经设置过期时间的key中挑选最近最少使用的
- 从已经设置过期时间的key中最快过期的
- 从已经设置过期时间的key随机淘汰
- 从整个数据选择最近最少使用
- 从整个数据随机淘汰
- 不淘汰
Redis高可用
1. 复制
- 基于RDB实现初始化,也就是同步操作,使用AOF实现同步复制,也就是命令传播
- 缺点:不支持自动扩容,没有高可用,可能产生数据不一致的问题
旧版复制功能
同步操作 - SYNC命令
复制的时机
- 从初次连接同步主的内容
- 从断线之后,再次去同步主的内容
问题
- 同步十分低效,耗费大量的资源
- 在断线重联的情况下,大部分数据是相同的,但是还是要执行全量同步
命令传播
在同步操作完成后,暂时主从一致,但是如果主一旦发生写操作,主会将写操作命令发送给从,从服务其执行后,再次回到一致的状态
新版复制功能
PSYNC命令
- 在初次同步相同,但是断线重联的时候,只会发送在断线期间执行的写操作,而不是全量同步
- 主从维持着一个复制的偏移量,通过偏移量,很容易就知道主从是否一致
- 在主发送写操作的时候,同时会将写操作保存在一个固定队列中,这样,如果重联只会的偏移量在队列中,就可以迅速使用队列中的命令进行恢复
心跳检测
在命令传播的阶段,从服务器会每秒1次向主服务器发送心跳请求
用来:
- 检测主从服务器的网络连接状态
- 辅助实现min-slaves配置选项,主服务会检测从服务器最少多少个,延迟多少的时候才写入数据。
- 检测命令丢失
2. 哨兵
基于主从复制,可以自动实现切换主从结构
- 哨兵由一个或者多个组成哨兵集群
- 哨兵检测主从所有的节点
- 当主节点挂掉的时候,哨兵会从其中一个从节点挑选一个作为主节点,当挂掉的主节点从新上线后,会降级当前主节点的从节点。
3. Cluster
- 实际上辅助和哨兵至是实现了Redis的高可用,但是其每一个机器都是存储完整数据,资源浪费很严重
- 节点通过握手来将其他节点添加到自己所处的集群当中。
- 集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
- 集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽又被指派给了其他节点。
- 在Cluster执行lua脚本的时候,会有一定的问题,因为Redis要求执行lua的脚本都在一个节点上面,如果你的lua脚本包含多个节点,就会报错,解决方法是在key中增加{},如果key中包含{}, 就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。
- 节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个MOVED错误,MOVED错误携带的信息可以指引客户端转向至正在负责相关槽的节点。
- 使用了跳跃表来保存槽和键之间的关系
- 重新分片: 将节点A的槽数据至节点B,Redis使用了redis-trib来实现。
- 过程:1. 通知A做好准备,2,通知B做好准备 3,获取n个键值 4,发送migrate命令,将选中的进行迁移 5,重复直到复制完成 6,将指派信息通知集群。
- ask错误:如果节点A正在迁移槽i至节点B,那么当节点A没能在自己的数据库中找到命令指定的数据库键时,节点A会向客户端返回一个ASK错误,指引客户端到节点B继续查找指定的数据库键。
- Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。