数据类型
字符串类型
字符串类型是二进制安全的,能存储包括二进制数据等任何形式的字符串,一个字符类型键允许存储的最大容量是512M。
当redis遇到incr,decr等操作时会转成数值型进行计算。
在Redis内部,String类型通过 int、SDS(simple dynamic string)作为结构存储,int用来存放整型数据,sds存放字节/字符串和浮点型数据。
hash类型
Hash是一个健值对集合,类似于在redis中存储对象,Hash结构可以使你像在数据库中 Update一个属性一样只修改某一项属性值。
用hash类型可以保存结构化的数据,而使用String的话,则需要增加对字符串进行序列化与反序列化的步骤。
列表类型
3.2版本后采用quicklist结构存储list,quicklist是一个双向链表,其每个节点都是一个ziplist,相当于linkedlist和ziplist的结合。
双向链表在链表两端进行push和pop操作,在插入节点上复杂度比较低,但是内存开
销比较大。
当list的元素个数和单个元素的长度比较小的时候,ziplist可以减少内存占用。
ziplist存储在一段连续的内存上,存储效率很高,但是插入和删除都需要频繁申请和释放内存。
使用List结构可以实现热点数据排行等功能,例如视频网站可以用来存储最新视频信息。
集合类型
set类型中不能有重复数据并且集合中的数据是无序的,内部结构是是hashtable,查找操作的时间复杂度都是O(1)。
set可以用于存储用户关注的人以及用户粉丝等。使用Redis为集合提供的求交集、并集、差集等操作,可以实现如共同关注的人等功能。
有序集合
sorted set,相比set的区别就是多了有序的功能。
有序集合为每个元素关联了一个分数,可以用来做用户得分排行功能,还能获得分数最高(或最低)的前N个元素、获得指定分数范围内的元素等与分数有关的操作。
内置策略
过期删除策略
redis 删除过期存储项的策略主要有两种。
消极方式: 在主键被访问时如果发现它已经失效,那么就删除它。
积极方式: 周期性地从设置了失效时间的主键中选择一部分失效的主键删除。
消极方式的隐患是,如果一个key过期了但是之后从来没有被访问,就会导致资源浪费。积极方式则是周期性地随机测试一些key,将已过期的key删掉。Redis每秒会进行10次操作,如下:
1. 随机测试 20 个带有timeout信息的key;
2. 删除其中已经过期的key;
3. 如果超过25%的key被删除,则重复执行步骤1;
redis同时使用了积极和消极两种方式。
内存淘汰策略
内存淘汰策略用于指示redis内存不足时如何处理,淘汰策略如下:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
默认策略:
# The default is:
# maxmemory-policy noeviction
需要注意的是,redis的最近最少算法是基于采样计算的,并不完全准确,即被淘汰的键可能并不是真正的最近最少使用的,因为在所有的数据中搜索最近最少无疑会增加系统的开销。
发布订阅
redis发布订阅中,发布者向指定的频道发送消息,所有订阅此频道的订阅者都会收到该消息,订阅者可以订阅多个频道。
发布者发布消息:
publish channel_name message
例如:
publish cctv hello # 向cctv频道发送“hello”
订阅者订阅消息:
subscribe channel [channel …]
例如:
subscribe cctv
执行subscribe命令后客户端会进入订阅状态并接收消息,但是消息不会持久化,因此订阅者不会收到订阅之前的消息。
channel分为普通channel和pattern channel。例如发布者发布了一条消息:
publish abc hello
redis会发给所有订阅abc这个普通channel的订阅者,同时还会发送给pattern channel *bc上的所有订阅者。
值得注意的是,相比于其他专业发布订阅中间来说,redis发布订阅支持的协议少、不能持久化、不支持回滚、不支持可靠性投递以及消息确认,如果需要以上特性则需使用专业消息中间件。
数据持久化
redis支持RDB和AOF(append-only-file)两种方式。两种持久化方式可以单独使用,也可以结合使用。
RDB方式
当符合预定规则时,redis会fork一个与当前进程完全相同的子进程将数据写入到一个临时文件中,然后将这个临时文件替换掉上次持久化好的文件。整个过程中,主进程不进行任何IO操作,保证了主进程性能。
策略配置:
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb # 文件名称
dir /home/work/app/redis/data/ # 文件保存路径
stop-writes-on-bgsave-error yes
rdbcompression no # 是否开启压缩
默认规则表示如果900s内有1条写入,或300秒内有10条写入,或60秒内有10000条写入,就触发一次快照操作。因为redis各时段读写请求不是均衡的,用户还可以自由定制配置规则。
stop-writes-on-bgsave-error
表示当备份进程出错时,主进程是否停止接受新的写入操作,这是为了保障持久化数据一致性问题。
RDB方式的缺点是,当服务器宕机后,redis会丢失最后一次持久化后的数据。但是如果进行大规模数据恢复,且数据的完整性不是很敏感,RDB方式比AOF方式更高效。
RDB方式的备份触发分为手动执行命令触发和自动触发,手动触发命令如下:
save:阻塞当前Redis服务器,直到持久化完成,线上禁止使用
bgsave:fork一个子进程进行持久化操作,只在fork子进程时阻塞
自动触发场景基本如下:
根据 save m n 配置规则触发
从节点全量复制时,主节点触发bgsave,然后将备份后的rdb文件发给从节点
执行flushall清除redis在内存中的所有数据时,如果save规则不为空,就会执行一次快照,如果没有save规则不会执行快照
执行shutdown时,如果没有开启aof,也会触发
关闭RBD:
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
配置处写save ""
即可。
AOF方式
AOF方式可以最大化的降低进程终止时导致的数据丢失数量。AOF方式将Redis执行
的每条写命令追加到硬盘文件中。
相关配置:
appendonly yes # 开启aof,默认为no
appendfilename "appendonly.aof" # 文件名称
appendfsync everysec
no-appendfsync-on-rewrite no # aof重写期间是否同步
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof时如果有错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
appendfsync
有三种模式:
always:把每个写命令都立即同步到aof,慢,但是很安全
everysec:每秒同步一次,是折中方案
no:redis不处理交给OS来处理,快,但是也最不安全
一般采用everysec配置兼顾速度与安全,最多损失1s的数据。
aof-load-truncated
如果为yes,在加载时发现aof尾部不正确是,会向客户端写入log然后继续执行,如果为no,发现错误立即停止,必须修复后才能重新加载。
aof文件的保存位置和RDB文件位置相同,都通过dir参数设置的,默认文件名apendonly.aof
。
AOF实现
AOF文件以纯文本记录redis执行的写命令,例如:
set test 1
set test 2
set test 3
get test
前3条写命令会保存到AOF文件。但是这3条命令中的前2条命令其实是不必要的,但是依然会被记录下来。随着记录的命令越来越多,AOF文件的大小会越来越大,但是内存中实际的数据可能没有多少。无效的命令记录导致磁盘空间浪费以及redis数据还原的过程低效的问题。
可以通过配置规则重写AOF文件:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage
表示当目前的AOF文件大小超过上一次重写时的百分之多少时会再次进行重写,如果之前没有重写过,则以启动时AOF文件大小为依据。
auto-aof-rewrite-min-size
表示限制允许重写的最小AOF文件大小,因为AOF文件很小时即使其中有很多冗余命令我们也不太关心。
AOF重写后的新AOF文件包含了当前数据集所需的最小命令集合。重写时,主进程会fork一个子进程执行重写操作,重写时并不基于原有aof文件进行整理,而是全量遍历内存中的数据,然后逐个序列到aof文件中。
在fork子进程这个过程中,服务端仍能对外提供服务,在此过程中,主进程的数据更新操作都会缓存到aof_rewrite_buf
中,当子进程重写完后再把缓存中的数据追加到新的aof文件。
当所有的数据全部追加到新的aof文件中后,把新的aof文件重命名为appendfilename
配置中的名称,此后所有的操作都会被写入新的aof文件。这样在rewrite过程中如果出现故障,不会影响原aof文件的正常工作。
aof重写的操作也分为手动触发和自动触发,手动触发则为执行命令:
bgrewriteaof
自动触发则是根据配置规则来触发。
数据恢复
重启redis,例如故障恢复时会自动加载恢复文件进行数据恢复,如果一台服务器上同时存在RDB文件和AOF文件,AOF文件的优先级要高于RDB文件,因为一般情况下AOF文件保存的数据完整性要高于RDB文件。即启动时会先检查AOF文件,如果AOF文件不存在就尝试加载RDB。
主从复制
master/slave模式,master进行读写操作,当写操作导致数据发生变化时会自动将数据同步给slave。一般slave被设置为只读属性。
主从复制分为全量复制和增量复制。
全量复制
slave在初始化时会进行一次全量复制,具体流程为slave向master发起数据同步请求,master收到请求后立即进行bgsave,将生成的快照发送给slave进行加载,master对在gbsave过程中的写请求会单独记录,然后将记录发给slave进行同步。
slave初始化成功后就能接收来自用户的读请求了。
redis的主从同步采用异步方式,master执行完客户端的写请求后会立即返回结果给客户端,然后异步的方式把命令同步给slave,来特征保证master性能不受主从复制影响。
如果在数据同步至slave期间出现网络分区,此时master无法知道有多少个slave同步成功了。这种情况可以通过哨兵来缓解。
主从相关配置:
# It is possible for a master to stop accepting writes if there are less than
# N replicas connected, having a lag less or equal than M seconds.
#
# The N replicas need to be in "online" state.
#
# The lag in seconds, that must be <= the specified value, is calculated from
# the last ping received from the replica, that is usually sent every second.
#
# This option does not GUARANTEE that N replicas will accept the write, but
# will limit the window of exposure for lost writes in case not enough replicas
# are available, to the specified number of seconds.
#
# For example to require at least 3 replicas with a lag <= 10 seconds use:
#
min-replicas-to-write 3
min-replicas-max-lag 10
增量复制
每次同步后,slave会记录同步偏移量offset,下次同步会从offset处开始同步,而不是从头开始的全量同步。
master会在内存中创建一个replication backlog,master和slave都会保存一个replica offset和master id。如果master和slave网络重新连接,slave会让master从上次的replica offset开始继续复制,但是如果没有找到对应的offset,那么就会执行一次全量复制。
例如查看master信息:
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.47.130,port=6379,state=online,offset=14,lag=1
slave1:ip=192.168.47.128,port=6379,state=online,offset=14,lag=1
master_replid:034a4acf5e3ffd21e8c07d1b9605d44924c452f2
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
slave信息:
# Replication
role:slave
master_host:192.168.47.128
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:638f8e23b6d4313147b605e2a7a7330b68143542
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
replication backlog
:复制积压队列,一个固定大小的循环队列。循环队列就是说插入数据时,一旦到达了积压队列的尾部,则重新从头部开始插入,覆盖最早插入的内容。
master_repl_offset
:全局性的计数器,每次收到客户端发来长度为len
的请求命令时,master_repl_offset
就会增加len
,这就是master的复制偏移量。当slave收到master返回的消息后,会将该值保存为自己的master_repl_offset
。
增量复制时,master收到客户端的命令请求,则将命令的长度增加到master_repl_offset
,然后将命令传播给从节点,slave收到后,也会将命令长度加到master_repl_offset,从而保证了master和slave的复制偏移量的一致性。
repl_backlog_size
:积压队列的总容量。
repl_backlog_histlen
:积压队列中当前累积数据量的大小。该值不会超过积压队列的总容
sentinel
哨兵。主从模式下,当master故障后,需要重新选举新的master,redis通过哨兵机制来完成这一过程。哨兵是一个或多个独立的进程,用于监控redis的运行状况,master故障时选举出新的master。
哨兵集群模式下,哨兵不仅会监控master和slave,还会互相监控。多个哨兵会在master节点订阅同一个频道,并向该频道注册自己的信息从而达到互相发现。新加入的哨兵会和其他节点建立长连接通信。
哨兵会定期向master节点发送心跳包来判断存活状态,一旦master节点没有正确响应,哨兵会把master设置为“主观不可用状态”,然后它会把“主观不可用”发送给其他的哨兵去确认,当确认的哨兵数大于>quorum时,则会认为是“客观不可用”,然后开始选举新master流程。
quorum配置:
# sentinel parallel-syncs <master-name> <numreplicas>
#
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel monitor mymaster 192.168.47.129 6379 1
哨兵节点中通过raft算法投票选举出leader来做决策,类似paxos算法,只要保证过半数节点通过提议即可。
Redis-Cluster
Redis Cluster由多个Redis节点组构成,节点组内分为master和slave两类节点,只有master能对外提供写服务,slave为只读服务。每个节点组就是一个分片。
redis-cluster是基于gossip的去中心化的集群,各个节点对整个集群状态的认知来自于节点之间的信息交互。在Redis Cluster,这个信息交互是通过Redis Cluster Bus来完成的。
Redis Cluster数据分区规则采用虚拟槽分区方式,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot),槽是集群内数据管理和迁移的基本单位,采用槽可以方便数据的拆分和集群的扩展。Redis Cluster槽的范围是0~16383。
hash tag
通过分片手段,可以将数据合理的划分到不同的节点上,但是有时我们希望对相关联的业务key存在同一个分片上。
例如单节点上执行MSET,它是一个原子性的操作,所有给定的key会在同一时间内被设置,不可能出现某些指定的key被更新另一些指定的key没有改变的情况。但是在集群环境下不再是原子操作,可能存在某些key更新失败情况,因为有些key可能会被分配到不同的机器上。
根据分片规则,我们要求key尽可能的分散在不同机器,但业务上我们又需要某些相关联的key分配到相同机器。
因为分片是对key做hash取模然后分配机器,在redis中引入了HashTag的概念,可以使hash算法根据key的某一个部分进行计算,让相关的key落到同一个数据分片。例如对于以下key:
users-zhangsan-fans
users-zhangsan-follows
使用hashtag:
users-{zhangsan}-fans
users-{zhangsan}-follows
当一个key包含 {} 的时候,此时不对整个key做hash,而仅对{}
包括的字符串做hash,这时能达到把这些key分配到相同机器的目的。
重定向
Redis Cluster不会代理查询,如果客户端访问了一个不存在指定key的节点,该节点会返回如下信息:
-MOVED 1256 192.168.47.129:6379
表示1256槽在IP为192.168.47.129
,端口6379
的redis实例上;如果根据key计算出的槽恰好在当前节点,则立即返回结果。
分片迁移
在某些情况下,节点和分片映射关系会变化:
新加入master节点时
某个节点宕机时
此时需要将16384个槽重新分配,槽中的键值也要迁移。迁移过程中不必宕机,但会出现不稳定状态,需要迁入的slot状态为IMPORTING,需要迁出的slot状态为MIGRATING。
为了保证slot数据的一致性,如果客户端的访问的Key还没有迁移出去,则正常处理该请求。如果key已经迁移或者不存该key,则回复客户端ASK信息去跳转到其他节点查询。
当来自客户端的正常访问不是从ASK跳转过来的,说明客户端还不知道迁移正在进行,很有可能操作了一个目前还没迁移完成的key,会返回MOVED命令让客户端重定向。这样保证了同一个key在迁移前总是在源节点上执行,迁移后总是在目标节点上执行。
缓存问题
缓存雪崩
缓存雪崩是指大量缓存在同一时刻同时失效,一般是采用了相同的过期时间或服务器宕机导致。缓存失效后,请求全部打到了数据库,数据库由于瞬间压力增大而导致崩溃。
解决方式:
对缓存的访问,如果发现从缓存中取不到值,通过加锁或队列方式保证加载缓存时为单进程操作
将缓存失效的时间分散,降低每一个缓存过期时间的重复率
如果是因为缓存服务器故障导致的问题,需要提高服务器的可用性
缓存穿透
缓存穿透是指查询根本不存在的数据,这时缓存和数据源都不会命中。缓存穿透可能会使后端数据源负载加大甚至宕机。
解决方式:
如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库
根据缓存数据Key的设计规则,将不符合规则的key进行过滤,例如采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免对底层存储系统的查询压力
布隆过滤器主要用来判断一个元素是否在集合中存在。因为是概率型的算法,所以也会存在一定的误差,如果传入一个值去布隆过滤器中检索,可能会出现检测存在的结果但是实际上可能是不存在的。