redis
2024-08-07 09:56:31 0 举报
AI智能生成
Redis是一个开源的、基于Key-Value模型的、高速的、支持多种数据结构的NoSQL数据库,采用C语言编写,具有很高的性能和灵活性。它支持String、Hash、List、Set和Sorted Set等多种数据结构,并提供了丰富的操作命令,如SET、GET、HSET等。Redis还提供了主从复制、哨兵、集群等高可用解决方案。其数据模型和数据结构使其在许多场景中成为高性能、低延迟的首选数据库。
作者其他创作
大纲/内容
redis部署
单机部署
redis特性
https://www.cnblogs.com/guapiwangxi/p/10556812.html
集群部署
主从架构
可以实现高并发
分支主题
主从
单机的redis,能够承载的QPS大概在上万到几万不等
对于缓存来说,一般是用来支撑读高并发的,因此架构可以做成一主多从
主节点负责写,并且将数据复制到其他的从节点,而从节点负责读,即主从(master-slave)架构
slave node 复制数据时不会阻塞 master node的正常工作
slave node 复制数据时也不会 阻塞 自己的查询操作,它会用旧的数据提供服务,但是完成复制后需要删除旧数据加载新数据,这时会暂停对外服务
Redis哨兵集群
实现高可用
分支主题
哨兵节点,有一个或者多个哨兵节点组成,哨兵节点可以是特殊节点,不存储数据
数据节点:主从节点都是数据节点
功能
监控Monitoring:哨兵会不断地检查主节点和从节点是否工作正常
自动故障转移Automatic Failover:当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效节点的其中一个从节点升级为新的主节点,并让其他节点改为复制的主节点。
配置提供者Configuration Provider:客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址
通知Notification:哨兵可以将故障转移的结果发送给客户端
监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现
部署
1、哨兵主从节点和普通主从节点配置一样不需要做额外配置
2、主从节点配置
#redis-6379.conf
port 6379
daemonize yes
logfile "6379.log"
dbfilename "dump-6379.rdb"
#redis-6380.conf
port 6380
daemonize yes
logfile "6380.log"
dbfilename "dump-6380.rdb"
slaveof 192.168.92.128 6379
#redis-6381.conf
port 6381
daemonize yes
logfile "6381.log"
dbfilename "dump-6381.rdb"
slaveof 192.168.92.128 6379
3、依次启动
redis-server redis-6379.conf
redis-server redis-6380.conf
redis-server redis-6381.conf
4、哨兵配置
#sentinel-26379.conf
port 26379
daemonize yes
logfile "26379.log"
sentinel monitor mymaster 192.168.92.128 6379 2
sentinel monitor mymaster 192.168. 92.128 6379 2配置的含义
该哨兵节点监控192.168.92.128:6379这个主节点
该主节点的名称是mymaster
最后的2的含义与主节点的故障判定有关:至少需要2个哨兵节点同意,才能判定主节点故障并进行故障转移
5、哨兵节点启动
二者作用完全相同
redis-sentinel sentinel-26379.conf
redis-server sentinel-26379.conf --sentinel
通过redis-cli连接哨兵查看
redis-cli -h ip -p port info Sentinel
模拟主节点挂掉
1、kill杀掉主节点
2、查看主节点还没转移
3、过一段时间查看,会发现主节点切换了
4、但是同时也可以发现从节点个数没有减少,原因哨兵并不会对从节点进行客观下线
5、当节点重启后,该节点会加入从节点中
6、在故障转移阶段,哨兵和主从节点的配置文件都会被改写
模拟故障转移和监控
总结
哨兵系统主从节点和普通主从节点没什么区别,故障发现和转移是由哨兵来控制和完成
哨兵节点本质上是Redis节点
每个哨兵节点,只需要配置监控主节点,便可以自动发现其他的哨兵和从节点
在哨兵节点启动和故障转移阶段,各个节点的配置文件会被重写
一个哨兵只监控了一个主节点,实际上,一个哨兵可以监控多个主节点,通过配置多条sentinel monitor即可实现
客户端可以通过哨兵节点+masterName获取主节点信息,在这里哨兵起到的作用就是配置提供者
注意,哨兵只是配置提供者,而不是代理
如果是配置提供者,客户端在通过哨兵获取节点信息后,会直接建立到主节点的连接,后续的请求会直接发向主节点
如果是代理,客户端的每一次请求都会发向哨兵,哨兵再通过主节点处理请求
哨兵节点在故障转移完成后,会将新的主节点信息发送给客户端,以便客户端及时切换到主节点
1、利用Redis提供的发布订阅功能
2、为每一个哨兵节点开启一个单独的线程
3、订阅哨兵节点的+switch-master频道
4、当收到消息时,重新初始化连接池
监控和故障转移
配置提供者和通知
哨兵实现的原理
哨兵节点支持的命令
基础查询
info sentinel:获取监控的所有主节点的基本信息。
sentinel masters:获取监控的所有主节点的详细信息。
sentinel master mymaster:获取监控的主节点mymaster的详细信息。
sentinel slaves mymaster:获取监控的主节点mymaster的从节点的详细信息。
sentinel sentinels mymaster:获取监控的主节点mymaster的哨兵节点的详细信息。
sentinel get - master - addr - by- name mymaster:获取监控的主节点mymaster的地址信息,前文已有介绍。
sentinel is-master-down-by-addr:哨兵节点之间可以通过该命令询问主节点是否下线,从而对是否客观下线做出判断。
增加/移除对主节点的监控
sentinel monitor mymaster2 192.168.92.128 16379 2:与部署哨兵节点时配置文件中的sentinel monitor功能完全一样,不再详述。
sentinel remove mymaster2:取消当前哨兵节点对主节点mymaster2的监控。
强制故障转移
sentinel failover mymaster:该命令可以强制对mymaster执行故障转移,即便当前的主节点运行完好;例如,如果当前主节点所在机器即将报废,便可以提前通过failover命令进行故障转移。
基本原理
定时任务:每个哨兵维护三个定时任务
通过向主节点发送info命令获取最新的主从结构
通过发布订阅获取其他哨兵节点信息
通过其他节点发送ping命令进行心跳检测,判断是否下线
主观下线
在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵就会将其进行主观下线
客观下线
哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其他哨兵节点该主节点的状态
如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。
选举领导者哨兵节点
当主节点下线后,各个节点会协商选举出一个领导哨兵节点,并由该领导者节点对其进行故障转移
监视该主节点的所有哨兵都有可能被选为领导者
选举使用的算法是Raft算法,基本思路,先到先得
即在一轮选举中,哨兵a向b发送申请,如果b没同意过其他哨兵,则会同意a成为领导者
一般谁先完成客观下线,谁就成为领导者
故障转移
选出领导者后
1、在从节点中选择新节点
1、首先过滤不健康的节点
2、选择优先级最高的从节点(slave-priority)
3、如果优先级没法判断,则选择复制偏移量最大的从节点
4、如果仍没区分,则选择runid最小的从节点
2、更新主从状态
通过salveof no one 命令,让选出来的从节点成为主节点
通过slaveof 命令让其他节点成为其从节点
3、将已经下线的主节点设为新主节点的从节点
主节点才有下线
配置与实践建议
配置
sentinel monitor {masterName} {masterIp} {masterPort} {quorum}
sentinel down-after-milliseconds {masterName} {time}
sentinel parallel - syncs {masterName} {number}
sentinel failover - timeout {masterName} {time}
建议
哨兵数量应不止一个,不同节点应部署在不同的物理机
避免哨兵本身成为高可用的瓶颈
可以减少下线的误判
哨兵数量应是奇数
便于哨兵通过投票做出决策
在docker部署应注意端口映射
集群
介绍
通过分片来进行数据共享,并提供复制和故障转移功能
节点
连接各个节点
各个节点相互独立
Cluster Meet [ip] [port]
CLUSTER NODES
查看该端口连接情况
握手过程
图解
分支主题
分支主题
分支主题
分支主题
分支主题
向A发送CLUSTER MEET
过程
分支主题
1、节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面
2、之后,节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息(message)
3、如果一切顺利,节点B将接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面
4、之后,节点B将向节点A返回一条PONG消息
5、如果一切顺利,节点A将接收到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功地接收到了自己发送的MEET消息
6、之后,节点A将向节点B返回一条PING消息
7、如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功地接收到了自己返回的PONG消息,握手完成
b节点和集群其他节点建立联系
节点a会将节点b的信息通过Gossip协议传播给集群的其他节点,让其节点也与节点b进行握手
启动节点
根据cluster-enabled配置选项是否开启服务器
节点会继续使用redisServer结构来保存服务器的状态
使用redisClient结构来保存客户端的状态
redisClient结构和clusterLink结构都有自己的套接字描述符和输入、输出缓冲区
redisClient结构中的套接字和缓冲区是用于连接客户端的
clusterLink结构中的套接字和缓冲区则是用于连接节点的
集群数据结构
节点将数据保存到了cluster.h/clusterNode结构、cluster.h/clusterLink结构,以及cluster.h/clusterState结构里面
clusterNode
记录节点当前状态
节点创建时间、名字、节点当前的配置纪元、节点的ip和端口号
分支主题
每个节点都会使用一个clusterNode结构来记录自己的状态,并为其他节点创一个相应的clusterNode结构
clusterNode的Link
clusterLink结构
保存连接节点所需的有关信息
分支主题
clusterState
记录当前节点的视角下集群目前所处状态
分支主题
槽指派
槽指派介绍
1、redis集群通过分片的方式保存数据的键值对
2、集群的整个数据库被分为16384个槽slot
3、每个键值对都属于这16384个槽的其中一个
4、每个节点都可以处理0或者最多16384个槽
5、当数据库的16384个槽都有节点处理时,集群处于上线
6、相反,如果有任一个槽没得到处理,则处于下线状态
分配槽CLUSTER ADDSLOTS <slot> [slot...]
查看槽信息
CLUSTER INFO
槽记录
clusterNode
slots属性和numslot属性记录了节点负责处理哪些槽
结构体
分支主题
slots属性是一个二进制数组,这个数组的长度为16384/8=2048个字节
根据索引i上的二进制的值来判断节点是否负责处理槽i
如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i
如果slots数组在索引i上的二进制位的值为0,那么表示节点不负责处理槽i
至于numslots属性则记录节点负责处理的槽的数量,也即是slots数组中值为1的二进制位的数量
一个节点除了负责自己处理的槽记录,还会告知其他节点自己目前负责处理哪些槽
每个接收到slots数组的节点都会将数组保存到相应节点的clusterNode结构里面
所以每个节点都知道数据库中的16384哥槽分别被指派给了集群中的哪些节点
结构
分支主题
slots数组包含16384个项,如果slots[i]为空,则未被指派
如果slots[i]指针指向一个clusterNode结构,那么表示槽i已经指派给了clusterNode结构所代表的节点
命令执行
客户端命令执行
流程
分支主题
计算键属于哪个槽
算法
分支主题
CRC16(key)语句用于计算键key的CRC-16校验和
&16383语句则用于计算出一个介于0至16383之间的整数作为键key的槽号
CLUSTER KEYSLOT命令
判断槽是否当前节点处理
如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令
如果clusterState.slots[i]不等于clusterState.myself,那么说明槽i并非由当前节点负责,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点
MOVED错误
当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个moved错误
moved错误格式:MOVED <slot> <ip>:<port>
slot为所在的槽,ip和port为负责处理槽slot节点的ip地址和端口号
集群环境下不会打印moved错误
会打印转向信息
单机的环境下会打印moved错误
因为单机模式的redis-cli不知道moved错误的作用
重新分片
可以将已经指派给某个节点的槽改为指派给另一个节点
重构新分片可以是在线进行,集群不需要下线
指令
redis-cli -c -p port
CLUSTER MEET
实现原理
重新分片
由Redis的集群管理软件redis-trib负责执行的
Redis提供了进行重新分片所需的所有命令
而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作
迁移键
分支主题
重新分片过程
分支主题
故障转移
redis集群节点分为主节点和从节点
主节点处理槽
从节点复制槽,并在被复制的主节点下线时,替代主节点继续处理命令请求
问题
如果主节点下线后,他的从节点怎么办?
如果这个主节点没有从节点怎么办?
设置从节点
命令
CLUSTER REPLICATE <node_id>
可以让接收命令的节点成为node_id所指定节点的从节点
接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构
并将自己的clusterState.myself.slaveof指针指向这个结构
故障检测
集群中每个节点都会定期向其他节点发送ping消息,检测是否在线
如果节点收到ping消息,但是没在规定时间内回pong消息,那么该节点会被发送ping消息的节点标为疑似下线
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL)
故障转移
步骤
1、在复制下线主节点的所有从节点中选一个节点出来
2、被选中的从节点会执行SLAVEOF no one命令,成为新的主节点
3、新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
4、新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
5、新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成
问题
复制下线主节点的从节点怎么办?
从节点从复制下线主节点转移到复制新主节点
如果下线主节点重新上线,他将复制新主节点,成为新主节点的从节点
选举
主节点有票权
消息
MEET
建立连接,并加入集群
PING
心跳检测,检测其他节点是否在线
PONG
响应MEET、PING消息
向集群广播自己的PONG消息,让其他节点立即刷新关于这个节点的认识
FALL
当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线
PUBLISH
当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令
redis介绍
是什么?
优点
数据类型
String
List列表
结构
基于双向链表,支持正反方向遍历
Hash哈希表
Set集合
Zset有序集合
redis实战
Jedis实战
Jedis介绍
jedis是一个高性能的Java客户端,是redis官方推荐的Java开发工具
Jedis基本的使用十分简单,在每次使用时,构建Jedis对象即可。
一个Jedis对象代表一条和Redis服务进行连接的Socket通道。使用完Jedis对象之后,需要调用Jedis.close()方法把连接关闭,否则会占用系统资源。
maven依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${redis.version}</version>
</dependency>
连接
Jedis jedis = new Jedis("host",port);//指定主机和端口
jedis.auth("***");//密码
//......访问redis服务
jedis.close();//关闭连接
实战
操作String
操作List
操作哈希Hash
操作Set
操作Zset
Jedis连接池实战
JedisPool介绍
Jedis开源库提供了一个负责管理Jedis连接对象的池,名为JedisPool类,位于redis.clients.jedis包中。
配置
maxTotal:资源池中最大的连接数,默认值为8。
maxIdle:资源池允许最大空闲的连接数,默认值为8。
minIdle:资源池确保最少空闲的连接数,默认值为0。
blockWhenExhausted:当资源池用尽后,调用者是否要等待,默认值为true。当为true时,maxWaitMillis才会生效。
maxWaitMillis:当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)。默认值为-1,表示永不超时,不建议使用默认值。
testOnBorrow:向资源池借用连接时,是否做有效性检测(ping命令),如果是无效连接,会被移除,默认值为false,表示不做检测。如果为true,则得到的Jedis实例均是可用的。
testOnReturn:向资源池归还连接时,是否做有效性检测(ping命令),如果是无效连接,会被移除,默认值为false,表示不做检测。
testWhileIdle:如果为true,表示用一个专门的线程对空闲的连接进行有效性的检测扫描,如果有效性检测失败,即表示无效连接,会从资源池中移除。默认值为true,表示进行空闲连接的检测。这个选项存在一个附加条件,需要配置项timeBetweenEvictionRunsMillis的值大于0;否则,testWhileIdle不会生效。
timeBetweenEvictionRunsMillis:表示两次空闲连接扫描的活动之间,要睡眠的毫秒数,默认为30000毫秒,也就是30秒钟。
numTestsPerEvictionRun:表示空闲检测线程每次最多扫描的Jedis连接数,默认值为-1,表示扫描全部的空闲连接。
jmxEnabled:是否开启jmx监控,默认值为true,建议开启。
如果JedisPool开启了空闲连接的有效性检测,如果空闲连接无效,就销毁。销毁连接后,连接数量就少了,如果小于minIdle数量,就新建连接,维护数量不少于minIdle的数量。minIdle确保了线程池中有最小的空闲Jedis实例的数量。
JedisPool创建和预热
创建
分支主题
预热
虽然JedisPool定义了最大空闲资源数、最小空闲资源数,但是在创建的时候,不会真的创建好Jedis连接并放到JedisPool池子里。这样会导致一个问题,刚创建好的连接池,池子没有Jedis连接资源在使用,在初次访问请求到来的时候,才开始创建新的连接,不过,这样会导致一定的时间开销。为了提升初次访问的性能,可以考虑在JedisPool创建后,为JedisPool提前进行预热,一般以最小空闲数量作为预热数量。
分支主题
在自己定义的JredisPoolBuilder连接池Builder类中,创建好连接池实例,并且进行预热。然后,定义一个从连接池中获取Jedis连接的新方法——getJedis(),供其他模块调用。
分支主题
使用
使用方法
可以使用前面定义好的getJedis()方法,间接地通过pool.getResource()从连接池获取连接
也可以直接通过pool.getResource()方法获取Jedis连接。
一定要close()
由于Jedis类实现了java.io.Closeable接口,故而在JDK 1.7或者以上版本中可以使用try-with-resources语句,在其隐藏的finally部分自动调用close方法。
try(...){...}
spring-data-redis
CRUD
创一个实体类,此类拥有一些简单的属性,且这些属性都具备基本的getter和setter方法
然后定义一个完成crud操作的service接口
saveUser()创建和更新
getUser()完成查询
deleteUser()删除
分支主题
配置
依赖
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>${springboot}</version>
</dependency>
配置redis
RedisTemplate
模板类位于核心包org.springframework.data.redis.core
封装了五大数据类型集合API
ValueOperations字符串类型操作API集合
ListOperations列表类型操作API集合
SetOperations集合类型操作API集合
ZSetOperations有序集合类型API集合
HashOperations哈希类型操作API集合
基础操作
getKeys(Object patens)
分支主题
expire(String key, long time)指定失效时间
getExpire(String key)获取失效时间
hashKey(String key)检查key是否存在
del(String... key)删除缓存
其他
String
分支主题
分支主题
分支主题
List
分支主题
分支主题
Hash
分支主题
分支主题
Set
分支主题
分支主题
分支主题
Spring的Redis缓存注解
使用
@CachePut
设置缓存
先执行方法,后把结果缓存起来
@CacheEvict
删除缓存
在执行方法之前,删除缓存
@Cacheable
查询缓存
首先检查注解中的Key键是否在缓存中,如果是,则返回Key的缓存值,不再执行方法;否则,执行方法并将方法结果缓存起来。从后半部分来看,@Cacheable也具备@CachePut的能力。
注解内属性
condition属性:指定缓存的条件
allEntries属性:表示是否全部清空
如果为true,则key就没什么用了,全部清空
beforeInvocation属性:表示是否在方法执行前操作缓存
名字空间
分支主题
@Caching注解
cacheable
用于指定一个或者多个@Cacheable注解的组合,可以指定一个,也可以指定多个。如果指定多个@Cacheable注解,则直接使用数组的形式,即使用花括号,将多个@Cacheable注解包围起来。用于查询一个或多个key的缓存,如果没有,则按照条件将结果加入缓存。
put属性
用于指定一个或者多个@CachePut注解的组合,可以指定一个,也可以指定多个,用于设置一个或多个key的缓存。如果指定多个@CachePut注解,则直接使用数组的形式。
evict属性
用于指定一个或者多个@CacheEvict注解的组合,可以指定一个,也可以指定多个,用于删除一个或多个key的缓存。如果指定多个 @CacheEvict注解,则直接使用数组的形式。
往往需要进行外键的级联删除:在删除一个主键时,需要将一个主键的所有级联的外键,通通都删掉。如果外键都进行了缓存,在级联删除时,则可以使用@Caching注解,组合多个@CacheEvict注解,在删除主键缓存时,删除所有的外键缓存。
@Caching使用
分支主题
SpringEL
可以定义“上下文变量”,这些变量在表达式中采用“#variableName”的方式予以引用
在创建变量上下文Context实例时,还可以在构造器参数中设置一个rootObject作为根,可以使用“#root”引用根对象,也可以使用“#this”引用根对象。
分支主题
参数访问
使用“#p参数index”形式访问方法的参数
分支主题
使用“#参数名”形式访问方法的参数
分支主题
依赖、配置
依赖
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
配置
特性
RDB的优缺点
RDB持久化
特点
1、可以手动持久化,可以根据服务器配置选项定期执行
2、RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件
RDB文件的创建与载入
RDB文件创建
方式
SAVE
特点
1、会阻塞redis服务器进程,直到RDB文件创建完毕为止
2、服务器进程阻塞期间,服务器不能处理任何命令请求
BGSAVE
特点
1、BGSAVE命令会派生出一个子进程,由子进程负责创建RDB文件
2、服务器进程(父进程)继续处理命令请求
在BGSAVE执行期间,客户端发送的SAVE命令会被服务器拒绝,同时也会拒绝BGSAVE
BGSAVE命令执行期间,客户端发送的BGREWRITEAOF会被延后到BGSAVE执行完
BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝
实际操作
创建RDB文件的实际工作由rdb.c/rdbSave函数完成
伪代码
分支主题
RDB文件载入
RDB文件的载入工作是服务器启动时自动执行
Redis没有专门用于载入RDB文件的命令
只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件
如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件还原数据库状态
只有AOF持久化功能处于关闭状态时,服务器才会使用RDB文件还原数据库状态
载入期间会一直处于阻塞状态
自动隔离保存
介绍
根据save选项设置的保存条件,自动执行BGSAVE命令
用户没有设置自动保存体条件
采取默认条件
save 900 1
save 300 10
save 60 10000
900秒内一次修改数据库,自动执行BGSAVE,以此类推
执行原理
服务器会根据save选项设置的保存条件,设置服务器状态redisServer结构的saveParems属性
saveParams属性是一个数组,数组每个元素都是一个saveparam结构,每个saveparam结构都保存一个save选项设置保存条件
分支主题
dirty计数器
dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)
lastsave属性
lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间
检查保存条件是否满足
周期性检查操作函数serverCron默认每隔100毫秒就会执行一次
负责检查是否满足保存条件
满足的话就执行BGSAVE命令
分支主题
RDB文件结构
RDB
文件最开头是REDIS部分,这部分长度为个字节
保存“REDIS”五个字符
RDB文件
操作
我们可以使用od命令来分析redisRDB 文件
给定-c参数可以以ASCII编码的方式打印输入文件
分支主题
给定-x参数可以以十六进制的方式打印输入文件
不包含任何数据库数据时
RDB文件组成结构
五个字节的字符“REDIS”字符串
四个字节的版本号(db_version)
一个字节的EOF常量
八个字节的校验和
包含字符串键的RDB文件
数据库保存到RDB文件
数据库组成
一个一字节的特殊值SELECTDB
一个长度可能为一个字节或者两个或者五个字节的数据库号码
一个或者以上数量的键值对
包含过期时间的字符串键的RDB文件
一个带过期时间的键值对结构
一个一字节的EXPIRETIME_MS特殊值
一个八字节长度的过期时间
一个一字节的类型
一个键和一个值
优缺点
优点
RDB是一个紧凑的二进制文件
适用于备份,全量复制
加载RDB恢复数据远远快于AOF
缺点
RDB方式数据没办法做到实时持久化/秒级持久化
因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高
RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题
AOF的优缺点
持久化
持久化实现
AOF持久化特点
保存服务器执行的写命令来记录数据库状态的
命令追加
当AOF持久化功能打开时,服务器在执行一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
文件写入
数据写入,会先写入到缓冲区,当缓冲区被填满之后或者超过时限之后,才真正地将数据写入磁盘
AOF持久化的效率和安全性
服务器配置appendfsync选项的值直接决定AOF持久化功能的效率和安全性
always:每条Redis写命令都同步写入硬盘
everysec:每秒执行一次同步,将多个命令写入硬盘
no:由操作系统决定何时同步
文件同步
解决
提供了fsync和fdatasync两个函数
可以强制让操作系统立即将缓冲区写入磁盘
AOF文件的载入和还原
服务器只要读入并重新执行一遍AOF文件里面保存的写的命令,就还原了服务器关闭之前数据库状态
分支主题
AOF重写
原因
AOF持久化是通过保存被执行的写命令来记录数据库状态的
随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能
通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令
实现
原理
1、虽然是重写文件,但是不需要对现有的文件进行读取
2、这个功能是通过读取服务器当前的数据库状态来实现的
3、首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令
4、因为aof_rewrite函数生成的新AOF文件只包含还原当前数据库状态所必须的命令
5、为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量
后台重写
原因
AOF重写程序aof_rewrite函数可以很好地完成创建一个新AOF文件的任务
但是调用这个函数的线程将被长时间阻塞
解决
Redis不希望AOF重写造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程里执行
子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求
子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性
问题
子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求
服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改
从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致
解决
Redis服务器设置了一个AOF重写缓冲区
这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区
在子进程执行AOF重写的时候服务器需要做以下工作
执行客户端发来的命令
将执行后的写命令追加到AOF缓冲区
将执行后的写命令追加到AOF重写缓冲区
概要
AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行
从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面
子进程完成AOF重写之后
向父进程发送一个信号
父进程接收到信号之后
将AOF重写缓冲区中的所有内容写入到新的AOF文件
对新的AOF文件进行改名,原子地覆盖现有的AOF文件
优缺点
优点
数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次
通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题
AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
缺点
AOF 文件比 RDB 文件大,且恢复速度慢
数据集大的时候,比 rdb 启动效率低
Redis的事务
事务的实现
事务经历的阶段
事务开始
命令入队
事务执行
事务开始
MULTI命令的执行标志着事务的开始
MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态
这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的
命令入队
当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行
当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作
如果客户端发送的命令为EXEC、DISCARD、WATCH、MULTI四个命令的其中一个,那么服务器立即执行这个命令
与此相反,如果客户端发送的命令是EXEC、DISCARD、WATCH、MULTI四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复
分支主题
事务队列
每个Redis客户端都有自己的事务状态
这个事务状态保存在客户端状态的mstate属性里面
分支主题
事务状态包含一个事务队列,以及一个已入队命令的计数器
分支主题
事务队列是一个multiCmd类型的数组
数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量
分支主题
事务队列以先进先出(FIFO)的方式保存入队的命令,较先入队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面
执行事务
当一个处于事务状态的客户端向服务器发送EXEC命令时
这个EXEC命令将立即被服务器执行
服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令
最后将执行命令所得的结果全部返回给客户端
WATCH命令的实现
一致性哈希算法原理
哈希算法
哈希算法图解
分支主题
一致性哈希算法
图解
分支主题
问题
缓存不均匀
哈希偏斜
解决
图解
分支主题
建立虚拟节点
比如说,由a变成a1,a2,a3,...,an
redis的单线程原子操作设计
订阅和发布
redis订阅和发布
一种消息通信模式
发送者发送消息
订阅者接收消息
通过命令
PUBLISH
发布
图示
分支主题
PUBLISH "news.it" "hello"
客户端向"news.it"频道发送消息
SUBSCRIBE
订阅
SUBSCRIBE "news.it"
客户端通过执行该命令就订阅”news.it“这个频道的
PSUBSCRIBE
客户端还可以通过执行PSUBSCRIBE命令订阅一个或多个模式
从而成为这些模式的订阅者:每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,它还会被发送给所有与这个频道相匹配的模式的订阅者
图示
分支主题
频道的订阅和退订
频道订阅的存储结构
客户端和被订阅的频道之间建立了订阅关系
redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面
这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端
结构图
分支主题
订阅频道
每当客户端执行SUBSCRIBE命令订阅某个或者某些频道的时候
服务器都会将客户端与被订阅的频道在pubsub_channels字典中进行关联
关联操作
如果频道已经有其他订阅者,那么它在pubsub_channels字典中必然有相应的订阅者链表,程序唯一要做的就是将客户端添加到订阅者链表的末尾
如果频道还未有任何订阅者,那么它必然不存在于pubsub_channels字典,程序首先要在pubsub_channels字典中为频道创建一个键,并将这个键的值设置为空链表,然后再将客户端添加到链表,成为链表的第一个元素
退订频道
命令
UNSUBSCRIBE
服务器从pubsub_channels解除客户端和频道的订阅关联
1、找到相应的链表,然后删除退订客户端信息
2、如果订阅者链表成了空链表,程序将从pubsub_channels字典中删除频道对应的键
模式的订阅和退订
模式的存储结构
服务器也将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面
pubsub_patterns属性是一个链表,链表中的每个节点都包含着一个pubsub Pattern结构
结构
分支主题
这个结构的pattern属性记录了被订阅的模式
client属性则记录了订阅模式的客户端
订阅模式
当客户端执行PSUBSCRIBE命令订阅某个或某些模式的时候
1、新建一个pubsubPattern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端
2、将pubsubPattern结构添加到pubsub_patterns链表的表尾
退订模式
PUNSUBSCRIBE
1、客户端退订的时候,服务器将在pubsub_patterns链表中查找并删除那些pattern属性为被退订模式
2、并且client属性为执行退订命令的客户端的pubsubPattern结构
退订示例
退订之前
分支主题
退订之后
分支主题
发送消息
PUBLISH<channel><message>
执行动作
1、将消息message发送给channel频道的所有订阅者
2、如果有一个或多个模式pattern与频道channel相匹配,那么将消息message发送给pattern模式的订阅者
发送给频道的订阅者
在pubsub_channels找到键,遍历链表
发送给模式的订阅者
PUBLISH命令要做的就是遍历整个pubsub_patterns链表
查看订阅信息
PUBSUB CHANNELS[pattern]
两种情况
如果不给定pattern参数,那么命令返回服务器当前被订阅的所有频道
如果给定pattern参数,那么命令返回服务器当前被订阅的频道中那些与pattern模式相匹配的频道。
示例
分支主题
分支主题
PUBSUB NUMSUB[channel-1 channel-2...channel-n]
接受任意多个频道作为输入参数,并返回这些频道的订阅者数量
分支主题
PUBSUB NUMPAT
用于返回服务器当前被订阅模式的数量
分支主题
分支主题
简单示例
javaApi
maven
发布者
订阅者
订阅频道
缓存问题
缓存击穿
缓存的作用
为数据库提供一层缓解数据库压力的保护层
当缓存找不到数据,再去数据库中查询
缓存击穿
当频繁访问缓存没找到数据,那么缓存就是去了意义
瞬间所有请求都落在了数据库上,从而导致数据库连接异常
解决方案
1、设置定时任务,主动的去更新缓存数据
但是当key比较分散的时候,操作起来还是比较复杂的
2、分级缓存,设置两层缓存保护层,1级失效时间比较短,2级失效时间比较长
请求过来优先到1级缓存中去查找,如果没有相关数据,则对该线程加锁,这个线程再到数据库中获取数据,更新至1级和2级缓存。这期间其他线程则到2级缓存中获取
问题
如果这期间其他线程也没获取到数据呢?
3、提供一个拦截机制,内部维护一系列合法的key值。
当请求的key不合法时,直接返回
缓存雪崩
雪崩定义
指缓存由于某些原因整体crash掉了,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难
如何避免
1、给缓存加上一定区间内的随机生效时间,不同的key设置不同的失效时间,避免同一时间集体失效
2、和缓存击穿解决方案类似,做二级缓存,原始缓存失效时从拷贝缓存中读取数据
3、利用加锁或者队列方式避免过多请求同时对服务器进行读写操作
分布式锁
什么是分布式锁
分布式锁用来解决分布式环境下共享资源的同步问题
http://www.redis.cn/articles/20181020004.html
分布式锁实现方案
方案一
原理
获取锁之前先查询一下以该锁为key对应的value存不存在,如果存在,则说明该锁被其他客户端获取了,否则的话就尝试获取锁
优缺点
优点
获取锁的方法很简单,只要以该锁为key,设置一个随机的值就行了
缺点
服务器宕机了,导致所没有正常被释放
可能会导致任务漏算
解决
Redis设置key的时候可以指定一个过期时间,只要获取锁的时候设置一个合理的过期时间
获取到的所不一定是排他锁
也就是说同一把锁同一时间可能被不同客户端获取到
伪代码
获取锁
业务过程
方案二
原理
同方案一
可以找到Redis提供了一个只有在某个key不存在的情况下才会设置key的值的原子命令,该命令也能设置key值过期时间
解决了方案一的问题’
伪代码
命令
SET my_key my_value NX PX milliseconds
获取锁
问题
在客户端A获取到锁,没执行完,锁的时间到期了,Redis过期机制自动释放了
客户端B获取到了锁
方案三
原理
在方案二的基础上
我们设置key的时候,将value设置为一个随机值r,当释放锁,也就是删除key的时候,不是直接删除,而是先判断该key对应的value是否等于先前设置的随机值,只有当两者相等的时候才删除该key,由于每个客户端产生的随机值是不一样的,这样一来就不会误释放别的客户端申请的锁了
伪代码
释放锁
方案四
原理
只要保证释放锁的代码是原子性的就能解决该问题了
同三
在Redis中执行原子操作不止有通过官方提供的命令的方式,还有另外一种方式,就是Lua脚本
代码
lua
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
0 条评论
下一页