进军redis之旅
redis是一种基于键值对(key-value)的NoSQL数据库。并且redis将所有数据都存放在内存中,所以读写性能非常惊人。redis还可以将内存的数据利用快照和日志的形式保存到硬盘上,防止内存数据丢失。
1.速度快
(1)redis的所有数据都是存放在内存中的。
(2)redis是用c语言实现的。
(3)redis使用了单线程架构,预防了多线程可能产生的竞争问题
2.基于键值对的数据结构服务器
redis主要提供了5种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础之上演变出了位图和HyperLogLog两种,后面又增加了有关GEO(地理信息定位)的功能。
3.丰富的功能
提供了键过期功能,可以用来实现缓存。
提供了发布订阅功能,可以用来实现消息系统。
支持Lua脚本功能,可以用Lua创造出新的redis命令。
提供了简单的事务功能,能在一定程度上保证事务特性。
提供了流水线功能,这样客户端能将一批命令一次性传到redis,减少了网络的开销。
4.简单稳定
5.客户端语言多
6.持久化
redis提供了两种持久化方式:RDB和AOF,即可以用两种策略将内存的数据保存到硬盘中。
7.主从复制
redis提供了复制功能,实现了多个相同数据的redis副本(如下图所示),复制功能是分布式redis的基础。
8.高可用和分布式
1.缓存
redis提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。
2.排行榜系统
redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统
3.计数器应用
redis天然支持技术功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
4.社交网络
5.消息队列系统
redis不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对redis本身或者应用本身造成致命伤害。
redis有5种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。
1.查看所有键
keys *
2.键总数
dbsize
dbsize命令会返回当前数据库中键的总数。
注意: dbsize命令在计算键总数时不会遍历所有键,而是直接获取redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1)。而keys命令会遍历所有键,所以它的时间复杂度是O(n),当redis保存了大量键时,线上环境禁止使用。
3.检查键是否存在
exists key
如果键存在则返回1,不存在则返回0
4.删除键
//支持单键和多键的删除
del key[key ...]
del是一个通用命令,无论值是什么数据结构类型,del命令都可以将其删除。
5.键过期
expire key seconds
redis支持对键添加过期时间,当超过过期时间后,会自动删除键。
例如下面,为键hello 设置了10秒过期时间
expire hello 10
ttl命令会返回键的剩余过期时间,它有3种返回值:
(1)大于等于0的整数:键剩余的过期时间
(2)-1:键没设置过期时间。
(3)-2:键不存在
6.键的数据结构类型
type key
返回键的类型,如果键不存在,则返回none
每种数据结构都有自己底层的内部编码实现,并且都有两种以上的内部编码实现。
可以使用:
object encoding key
查询内部编码
redis这样设计有两个好处:
(1)可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
(2)多种内部编码实现可以在不同场景下发挥各自的优势。
redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。
1.引出单线程模型
redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所以的命令都会进入一个队列中,然后逐个被执行。
2.单线程快的原因
(1)纯内存访问,redis将所有数据放在内存中
(2)非阻塞I/O,redis使用epoll作为I/O多路复用技术的实现
(3)单线程避免了线程切换和竞态产生的消耗
但是,单线程会有一个问题:对于每个命令的执行时间是有要求的,如果某个命令执行过长,会造成其他命令阻塞。
字符串类型是redis最基础的数据结构。(键都是字符串类型)
1.常用命令
(1)设置值
set key value [ex seconds] [px milliseconds] [nx | xx]
下面操作设置键为hello,值为world的键值对:
set hello world
set命令有几个选项:
ex seconds:为键设置秒级过期时间
px milliseconds:为键设置毫秒级过期时间
nx:键必须不存在,才可以设置成功,用于添加
xx:与nx相反,键必须存在,才可以设置成功,用于更新
除了set选项,redis还提供了setex和setnx两个命令:
setex key seconds value
setnx key value
它们作用和ex和nx选项是一样的。
sextnx和setxx实际的应用场景?
以setnx为例子,由于redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案。
(2)获取值
get key
(3)批量设置值
mset key value [key value ...]
(4)批量获取值
mget key [key ...]
如果有些键不存在,那么它的值是nil(空),结果是按照传入键的顺序返回的。
批量操作命令可以有效的提高开发效率。 但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成redis阻塞或者网络阻塞。
假如没有mget这样命令,要执行n次get命令,具体消耗如下:
n次get时间=n次网络时间+n次命令时间,
使用mget后,具体消耗如下:
n次get时间=1次网络时间+n次命令时间
(5)计数
incr key
incr命令用于对值做自增操作,返回结果分为三种情况:
值不是整数,返回错误。
值是整数,返回自增后的结果
键不存在,按照值为0自增,返回结果为1。
2.不常用命令
(1)追加值
append key value
append可以向字符串尾部追加值
(2)字符串长度
strlen key
(3)设置并返回原值
getset key value
getset和set一样会设置值,但是不同的是,它同时会返回键原来的值
(4)设置指定位置的字符
setrange key offeset value
(5)获取部分字符串
getrange key start end
start和end分别是开始和结束的偏移量,偏移量从0开始计算
字符串类型的内部编码有3种:
int:8个字节的长整型
embstr:小于等于39个字节的字符串
raw:大于39个字节的字符串
redis会根据当前值的类型和长度决定使用哪种内部编码实现。
1.缓存功能
redis作为缓存层,mysql作为存储层。首先从redis获取数据,如果没有从redis获取到用户信息,需要从mysql中进行获取,并将结果回写到redis。
2.计数
redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
3.共享Session信息
可以使用redis将用户的Session进行集中管理。
4.限速
(1)设置值
hset key filed value
下面为user:1添加一对field-value:
hset user:1 name tom
如果设置成功返回1,反之会返回0
(2)获取值
hget key field
(3)删除field
hdel key field [field ...]
hdel会删除一个或多个field,返回结果为成功删除field的个数。
(4)计算field个数
hlen key
(5)批量设置或获取field-value
hmget key field [field ...]
hmset key field value [field value ...]
hmset和hmget分别是批量设置和获取field-value,hmset需要的参数是key和多对field-value,hmget需要的参数是key和多个field。
(6)判断field是否存在
hexists key field
包含返回结果为1,不包含返回0
(7)获取所有field
hkeys key
(8)获取所有value
hvals key
(9)获取所有的field-value
hgetall key
在使用hgetall时,如果哈希元素个数比较多,会存在阻塞redis的可能
(10)hincrby hincrbyfloat
(11)计算value的字符串长度
hstrlen key field
哈希类型的内部编码有两种:
(1)ziplist(压缩列表)
ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
(2)hashtable(哈希表)
hashtable的读写时间复杂度为O(1)
使用哈希类型来存储,如下:
相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加直观。
哈希类型和关系型数据库有两点不同之处:
(1)哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)。
(2)关系型数据库可以做复杂的关系查询,而redis去模拟关系型复杂查询开发困难,维护成本高。
下面给出三种缓存用户信息方案的实现方法和优缺点:
(1)原生字符串类型:每个属性一个键。
set user:1:name tom
set user:1:age 23
set user:1:city beijing
优点:简单直观,每个属性都支持更新操作
缺点:占用过多的键,内存占用量大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用
(2)序列化字符串类型:将用户信息序列化后用一个键保存
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到redis中。
(3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存
hmset user:1 name tom age 23 city beijing
优点:简单直观,如果使用合理可以减少内存空间的使用
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
列表(list)类型是用来存储多个有序的字符串。在redis中,可以对列表两端插入和弹出,还可以获取指定范围的元素列表、获取指定索引下标的元素等。
1.添加操作
(1)从右边插入元素
rpush key value [value ...]
(2)从左边插入元素
lpush key value [value ...]
(3)向某个元素前或者后插入元素
linsert key before | after pivot value
linsert命令会从列表中找到等于pivot的元素,在其前(before)或者后(after)插入一个新的元素value
2.查找
(1)获取指定范围内的元素列表
lrange key start end
lrange操作会获取列表指定索引范围所有的元素。索引下标有两个特点:
第一,索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。第二,lrange中的end选项包含了自身。
(2)获取列表指定索引下标的元素
lindex key index
(3)获取列表长度
llen key
3.删除
(1)从列表左侧弹出元素
lpop key
(2)从列表右侧弹出
rpop key
(3)删除指定元素
lrem key count value
lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:
count>0,从左到右,删除最多count个元素
count<0,从右到左,删除最多count绝对值个元素
count=0,删除所有
(4)按照索引范围修剪列表
ltrim key start end
保留列表中的元素
4.修改
lset key index newValue
修改指定索引下标的元素
5.阻塞操作
阻塞弹出如下:
blpop key [key ...] timeout
brpop key [key ...] timeout
blpop和brpop是lpop和rpop的阻塞版本。
brpop命令包含两个参数:
key [key …]:多个列表的键
timeout:阻塞时间(单位:秒)
在使用brpop时,需要注意:
(1)如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。
(2)如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值。
列表类型的内部编码有两种:
(1)ziplist(压缩列表)
(2)linkedlist(链表):当列表类型无法满足ziplist的条件时,redis会使用linkedlist作为列表的内部实现。
1.消息队列
2.文章列表
在实际开发中,列表的使用场景很多:
lpush + lpop = Stack(栈)
lpush + rpop = Queue (队列)
lpsh + ltrim = Capped Collection (有限集合)
lpush + brpop = Message Queue (消息队列)
集合中不允许有重复的元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
1.集合内操作
(1)添加元素
sadd key element [element ...]
返回结果为添加成功的元素个数
(2)删除元素
srem key element [element ...]
返回结果为成功删除元素的个数
(3)计算元素个数
scard key
时间复杂度为O(1),它不会遍历集合所有的元素,而是直接用redis内部的变量。
(4)判断元素是否在集合中
sismember key element
如果给定元素element在集合内返回1,反之返回为0
(5)随机从集合返回指定个数元素
srandmember key [count]
[count]是可选参数,如果不写默认为1。
(6)从集合随机弹出元素
spop key
spop操作可以从集合中随机弹出一个元素。
srandmember和spop都是随机从集合选出元素,两者不同的是spop命令执行后,元素会从集合中删除,而srandmember不会。
(7)获取所有元素
smembers key
smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞redis的可能性,这时候可以使用sscan来完成。
2.集合间操作
(1)求多个集合的交集
sinter key [key ...]
(2)求多个集合的并集
suinon key [key ...]
(3)求多个集合的差集
sdiff key [key ...]
(4)将交集、并集、差集的结果保存
sinterstore destination key [key ...]
suionstore destination key [key...]
sdiffstore destination key [key ...]
集合间的运算在元素较多的情况下 会比较耗时,所以redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在destination key中。
集合类型的内部编码有两种类型:
(1)intset(整数集合)
(2)hashtable(哈希表)
集合类型比较典型的使用场景是标签(tag)
集合类型的应用场景通常为以下几种:
sadd = Tagging (标签)
spop / srandmember = Random item (生成随机数,比如抽奖)
sadd + sinter = Social Graph (社交需求)
它保留了集合不能重复成员的特性,但不同的是,有序集合中的元素可以排序,排序依据是给每个元素设置一个分数(score)。
1.集合内
(1)添加成员
zadd key score member [score member ...]
返回结果代表成功添加成员的个数
有四个选项:
nx:member必须不存在,才可以设置成功,用于添加
xx:member必须存在,才可以设置成功,用于更新
ch:返回此次操作后,有序集合元素和分数发生变化的个数
incr:对score做增加
有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度为O(log(n)),sadd的时间复杂度为O(1)。
(2)计算成员个数
zcard key
时间复杂度为O(1)
(3)计算某个成员的分数
zscore key member
如果成员不存在返回nil
(4)计算成员的排名
zrank key member
zrevrank key member
zrank是从分数从低到高返回排名,zrevrank反之。
(5)删除成员
zrem key member [member...]
返回结果为成功删除的个数
(6)增加成员的分数
zincrby key increment member
(7)返回指定排名范围的成员
zrange key start end [withscores]
zrevrange key start end [withscores]
有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。
如果加上withscores选项,同时会返回成员的分数。
(8)返回指定分数范围的成员
zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]
其中,zrangebyscore按照分数从低到高返回,zrevrangebyscore反之
[limit offset count] 选项可以限制输出的起始位置和个数
(9)返回指定分数范围成员个数
zcount key min max
(10)删除指定排名内的升序元素
zremrangebyrank key start end
(11)删除指定分数范围的成员
zremrangebyscore key min max
返回结果为成功删除的个数
2.集合间的操作
(1)交集
(2)并集
有序集合类型的内部编码有两种:
(1)ziplist(压缩列表)
(2)跳跃表
有序集合比较典型的使用场景就是排行榜系统。
本节将按照单个键、遍历键、数据库管理三个维度对一些通用命令进行介绍。
1.键重命名
rename key newkey
renamenx命令,确保只有newkey不存在时候才被覆盖
在使用重命名命令时,有两点需要注意:
(1)由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞redis的可能性。
(2)如果rename和renamenx中的key和newkey如果是相同的,返回结果略有不同。
2.随机返回一个键
randomkey
3.键过期
ttl命令和pttl都可以查询键的剩余过期时间,但是pttl精度更高可以达到毫秒级别,有3种返回值:
大于等于0的整数:键剩余的过期时间(ttl是秒,pttl是毫秒)
-1:键没有设置过期时间
-2:键不存在。
无论是使用过期时间还是时间戳,秒级还是毫秒级,在redis内部最终使用的都是pexpireat。
需要注意的是:对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视。setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间
4.迁移键
redis提供了move、dump+restore、migrate三组迁移键的方法
(1)move
move key db
redis内部可以有多个,彼此在数据上是相互隔离的,move key db就是把指定的键从源数据库移动到目标数据库中。
(2)dump + restore
dump key
restore key ttl value
dump + restore 可以实现在不同的redis实例之间进行数据迁移的功能。
整个迁移的过程分为两步:
(1)在源redis上,dump命令会将键值序列化,格式采用的是RDB格式
(2)在目标redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间。
需要注意的是: 第一,整个迁移过程并非原子性的,而是通过客户端分步完成的;第二,迁移过程是开启了两个客户端连接
(3)migrate
实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程,并且migrate命令具有原子性
1.全量遍历键
keys pattern
pattern使用的是glob风格的通配符:
(1)* 代表匹配任意字符
(2)? 代表匹配一个字符
(3)[ ]代表匹配部分字符,例如[1,3]代表匹配1,3 ,[1-10]代表匹配1到10的任意数字。
(4)\x用来做转义,例如要匹配星号、问号需要进行转义。
由于redis单线程架构,如果redis包含了大量的键,执行keys命令很可能会造成阻塞,所以不建议使用。
但如果有遍历键的需求,可以在以下三种情况下使用:
(1)在一个不对外提供服务的redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制
(2)如果确认键值总数确实比较少,可以执行该命令
(3)使用scan命令渐进式的遍历所有键,可以有效防止阻塞
2.渐进式遍历
scan采用渐进式遍历的方式来解决keys命令可能带来阻塞的问题,每次scan命令的时间复杂度是O(1),但是真正实现keys的功能,需要执行多次scan。
scan使用方法如下:
scan cursor [match pattern] [count number]
cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
math pattern是可选参数,做模式匹配
count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。
1.切换数据库
select dbIndex
redis只是用数字作为多个数据库的实现,默认配置中是有16个数据库。
如select 0
操作将切换到第一个数据库,数据库之间的数据没有任何关联。
2.flushdb / flushall
flushdb / flushall命令用于清楚数据库,两者的区别是flushdb只清楚当前数据库,flushall会清除所有数据库
注意: 如果当前数据库键值数量比较多,flushdb / flushall 存在阻塞redis的可能性。
本章将介绍如下功能:
慢查询分析:通过慢查询分析,找到有问题的命令进行优化
redis shell:功能强大的redis shell会有意想不到的实用功能
Pipeline:通过Pipeline(管道或者流水线)机制有效提高客户端性能
事务与Lua:制作自己的专属原子命令
Bitmaps:通过在字符串数据结构上使用位操作,有效节省内存
HyperLogLog:一种基于概率的新算法,难以想象地节省内存空间
发布订阅:基于发布订阅模式的消息通信机制
GEO:提供了基于地理位置信息的功能
所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阈值,就将这条命令的相关信息记录下来。Redis也提供了类似的功能。
redis客户端执行一条命令分为如下4个部分:
(1)发送命令 (2)命令排队 (3)命令执行 (4)返回结果
需要注意的是,慢查询只统计步骤3的时间。
对于慢查询功能,需要明确两件事:
(1)预设阈值怎么设置? (2)慢查询记录存放在哪?
slowlog-log-slower-than就是那个预设阈值,它的单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000.
如果slow-log-slower-than=0会记录所有的命令,slowlog-log-slower-than<0对于任何命令都不会进行记录。
slowlog-max-len只是说明了慢查询日志最大存储多少条。
实际上redis使用了一个列表来存储慢查询日志 ,slowlog-max-len就是列表的最大长度。当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出。
虽然慢查询日志是存放在redis内存列表中,但是redis并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理。
(1)获取慢查询日志
slowlog get [n]
返回当前redis的慢查询,参数n可以指定条数
慢查询日志有4个属性组成,分别是慢查询日志的标识id、发生时间戳、命令耗时、执行命令和参数。
慢查询列表如图所示:
(2)获取慢查询日志列表当前的长度
slowlog len
(3)慢查询日志重置
slowlog reset
对列表做清理操作
slowlog-max-len配置建议: 线上建议调大慢查询列表,记录慢查询时redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能
slowlog-log-slower-than配置建议: 默认值超过10毫秒判定为慢查询,需要根据redis并发量调整该值。
慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。
由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令
可以执行redis-cli -help命令来进行查看,下面将对一些重要参数的含义以及使用场景进行说明
1.-r
-r(repeat)选项代表将命令执行多次,
2.-i
-i(interval)选项代表每隔几秒执行一次命令
但是,-i选项必须和-r选项一起使用
3.-x
-x选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参数
4.-c
-c(cluster)选项是连接redis cluster节点时需要使用的,-c选项可以防止moved和ask异常。
5.-a
如果redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要手动输入auth命令
6.–scan和–pattern
–scan选项和–pattern选项用于扫描指定模式的键
7.–slave
–slave选项是把当前客户端模拟成当前redis节点的从节点,可以用来获取当前redis节点的更新操作
8.–rdb
–rdb选项会请求redis实例生成并发送rdb持久化文件,保存到本地。可使用它做持久化文件的定期备份
9.–pipe
–pipe选项用于将命令封装成redis通信协议定义的数据格式,批量发送给redis执行
10.–bigkeys
–bigkeys选项使用scan命令对redis的键进行采样,从中找到内存占用比较大的键值
11.–eval
–eval选项用于执行指定Lua脚本
12.–latency
latency有三个选项,分别是–latency 、 --latency-history、 --latency-dist
它们都可以检测网络延迟
(1)–latency
可以测试客户端到目标redis的网络延迟
(2)–latency-history
可以以分时段的形式了解延迟信息
(3)–latency-dist
使用统计图表的形式从控制台输出延迟统计信息
13.–stat
–stat选项可以实时获取redis的重要统计信息。
14.–raw和–no-raw
–no-raw选项是要求命令的返回结果必须是原始的格式
–raw返回格式化后的结果
redis-server --test-memory可以用来检测当前系统能否稳定地分配指定容量的内存给redis,通过这种检测可以有效避免因为内存问题造成redis崩溃。
例如下面操作检测当前操作系统能否提供1g的内存给redis:
redis-server --test-memory 1024
redis-benchmark可以为redis做基准性能测试
1.-c
-c(clients)选项代表客户端的并发数量(默认是50)
2.-n
-n(num)选项代表客户端请求总量(默认是100000)
3.-q
-q选项仅仅显示redis-benchmark的requests per second信息
4.-r
使用-r(random)选项,可以向redis插入更多随机的键
5.-p
-p选项代表每个请求pipeline的数据量(默认为1)
6.-k
-k选项代表客户端是否使用keepalive,1为使用,0为不使用,默认值为1
7.-t
-t选项可以对指定命令进行基准测试
8.–csv
–csv选项会将结果按照csv格式输出,便于后序处理
redis客户端执行一条命令分为如下四个过程:
(1)发送命令 (2)命令排队 (3)命令执行 (4)返回结果
称为RTT(往返时间)
Pipeline(流水线)能将一组redis命令进行组装,通过一次RTT传输给redis,再将这组redis命令的执行结果按顺序返回给客户端。
如,传输n条命令,没有使用Pipeline,整个过程需要n次RTT;使用Pipeline执行n次命令,整个过程需要1次RTT
Pipeline执行速度一般比逐条执行要快
客户端和服务端的网络延时越大,Pipeline的效果越明显
原生批量命令是原子的,Pipeline是非原子的
原生批量命令是一个命令对应多个key,Pipeline支持多个命令
原生批量命令是redis服务端支持实现的,而Pipeline需要服务端和客户端共同实现
Pipeline只能操作一个redis实例,但是即使在分布式redis场景中,也可以作为批量操作的重要优化手段。
redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的。
如果要停止事务的执行,可以使用discard命令代替exec命令即可
如果事务中的命令出现错误,Redis的处理机制也不尽相同:
1.命令错误
例如错将set写成sett,语法错误,会造成整个事务无法执行
2.运行时错误
如误把sadd命令写成zadd。注意,redis并不支持回滚功能,需要自己修复这类问题
1.数据类型及其逻辑处理
Lua提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)
(1)字符串
如下:
local strings val="world"
其中,local代表val是一个局部变量,如果没有local代表是全局变量
(2)数组
在Lua中,如果要使用类似数组的功能,可以使用tables类型,Lua的数组下标从1开始计算
如下例子:
local tables myArray={"redis","jedis",true,88.0}
可以实现对位的操作
本节将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记作1,没有访问的用户记作0,用偏移量作为用户的id
1.设置值
setbit key offset value
设置键的第offset个位的值
为了节省空间,通常的做法是每次做setbit操作时将用户id减去这个指定数字。若第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成redis的阻塞。
2.获取值
gitbit key offset
获取键的第offset位的值(从0开始算)
3.获取Bitmaps指定范围值为1的个数
bitcount [start] [end]
4.Bitmaps间的运算
bitop op destkey key [key...]
bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、
not(非)、xor(异或)操作并将结果保存在destkey中。
5.计算Bitmaps中第一个值为targetBit的偏移量
bitpos key targetBit [start] [end]
根据访问量来决定
HyperLogLog是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计
1.添加
pfadd key element [element ...]
pfadd用于向HyperLogLog添加元素,如果添加成功返回1。
2.计算独立用户数
pfcount key [key ...]
pfcount用于计算一个或多个HyperLogLog的独立数
3.合并
pfmerge destkey sourcekey [sourcekey...]
pfmerge可以求出多个HyperLogLog的并集并赋值给destkey
注意:HyperLogLog内存占用量非常小,但是存在错误率
使用时需要注意如下:
只为了计算独立总数,不需要获取单条数据。
可以容忍一定误差率
redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令
1.发布消息
publish channel message
返回结果为订阅者个数
2.订阅消息
subscribe channel [channel...]
订阅者可以订阅一个或多个频道
注意:(1)客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe 、psubscribe、 unsubscribe、 punsubscribe命令。(2)新开启的订阅客户端,无法收到该频道之前的消息,redis不会对发布的消息进行持久化
3.取消订阅
unsubscribe [channel [channel ...]]
取消对指定频道的订阅
4.按照模式订阅和取消订阅
psubscribe pattern [pattern...]
punsubscribe [pattern [pattern...]
5.查询订阅
(1)查看活跃的频道
pubsub channels [pattern]
所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式
(2)查看频道订阅数
pubsub numsub [channel...]
(3)查看模式订阅数
pubsub numpat
利用消息解耦都可以直接使用发布订阅模式
地理信息定位功能,支持存储地理位置信息
1.增加地理位置信息
geoadd key longitude latitude member [longitude latitude member...]
longitude、 latitude、 member分别是该地理位置的经度、维度、成员。
返回结果代表添加成功的个数
2.获取地理位置信息
geopos key member [member...]
获取经纬度
3.获取两个地理位置的距离
geodist key member1 member2 [unit]
其中unit代表返回结果的单位,m代表米,km代表公里,mi代表英里,ft代表尺
4.获取指定位置范围内的地理信息位置集合
5.获取geohash
geohash key member [member...]
将二维经纬度转换为一维字符串
字符串越长,表示的位置更精确;两个字符串越相似,它们之间的距离越近。
6.删除地理位置信息
zrem key member
GEO的底层实现是zset
redis制定了RESP实现客户端与服务端的正常交互,这种协议简单高效
1.client list
能列出与redis服务端相连的所有客户端连接信息
输出的各个信息:
(1)标识:id、 addr、 fd、 name
id:客户端连接的唯一标识。 addr:客户端连接的ip和端口。 fd:socket的文件描述符。(如果fd=-1代表当前客户端不是外部客户端,而是redis内部的伪装客户端)。name:客户端的名字
(2)输入缓冲区:qbuf、qbuf-free
redis为每个客户端分配了输入缓冲区
输入缓冲使用不当会产生两个问题:
(1)一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
(2)输入缓冲区不受maxmemory控制,超过maxmemory限制,可能会产生数据丢失、键值淘汰、OMM等情况。
造成输入缓冲区过大的原因有哪些?
(1)redis的处理速度跟不上输入缓冲区的输入速度
(2)若每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况
(3)redis可能发生了阻塞,短期不能处理命令
那么如何快速发现和监控呢?
监控输入缓冲区异常的方法有两种:
(1)通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端
(2)通过info命令的info clients模块,找到最大的输入缓冲区;可以设置警报
输出缓冲区:obl、oll、 omem
实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果。
固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果如下图:
监控输出环翠区的方法依然有两种:
(1)通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出现问题的客户端
(2)通过info命令的info clients模块,找到输出缓冲区列表的最大对象数
如何预防输出缓冲区出现异常的情况
进行上述监控,设置阈值,超过阈值及时处理;
限制普通客户端输出缓冲区的< hard limit> < soft limit> < soft seconds>
适当增大slave的输出缓冲区的< hard limit> < soft limit> < soft seconds>
限制容易让输出缓冲区增大的命令,例如,高并发的monitor命令就是一个危险的命令
及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大
monitor命令
monitor命令能监听其他客户端正在执行的命令,并记录了详细的时间戳。
1.无法从连接池中获取到连接
造成连接池没有资源的原因可能如下:
客户端:高并发下连接池设置过小,出现供不应求
客户端:没有正确使用连接池,比如没有进行释放
客户端:存在慢查询操作,
服务端:由于服务端一些原因,造成客户端执行命令过程中出现阻塞
2.客户端读写超时
出现这种情况的原因:
读写超时时间设置的过短
命令本身就慢
客户端与服务端网络不正常
redis自身发送阻塞
3.客户端连接超时
原因:
连接超时设置的过短
redis发送阻塞,造成tcp-backlog已满,造成新的连接失败
客户端与网络端网络不正常
4.客户端缓冲区异常
原因:
输出缓冲区满
长时间闲置连接被服务端主动断开
不正常并发读写
1.现象
服务端现象:redis主节点内存陡增,几乎用满maxmemory,而从节点内存并没有变化,如下图:
客户端现象:客户端产生了OOM异常,也就是redis主节点使用的内存已经超过了maxmemory的设置
2.分析原因
(1)确实有大量写入,主从复制
(2)其他原因造成主节点内存使用过大:排查是否由客户端缓冲区造成主节点内存陡增,使用info clients命令查询相关信息
1.现象
客户端现象:客户端出现大量超时,并且超时是周期性出现的
服务端现象:服务端没有明显的异常,只是有一些慢查询操作
2.分析
网络原因
redis本身
客户端:和慢查询日志的历史记录对应一下时间
redis支持RDB和AOF两种持久化机制
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发
手动触发分别对应save和bgsave命令
(1)save命令:阻塞当前redis服务器,直到RDB过程完成为止。阻塞时间较长
(2)bgsave命令:redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞时间较短
注意:Redis内部所有涉及RDB的操作都采用bgsave的方式,而save命令已经废弃
bgsave是主流的触发RDB持久哈方式,流程如下
(1)执行bgsave命令,redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回
(2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞
(3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令
(4)子进程创建RDB文件,进行原子替换
(5)进程发送信号给父进程表示完成,父进程更新统计信息
RDB的优缺点:
优点:
RDB是一个紧凑压缩的二进制文件,代表redis在某个时间点上的数据快照。非常适合用于备份,全量复制等场景
redis加载RDB恢复数据远远快于AOF的方式
缺点:
RDB方式数据没办法做到实时持久化/秒级持久化。(需要创建子进程,重量级操作,频繁执行成本过高)
存储文件格式没办法做到版本兼容
AOF持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。
AOF主要解决的是数据持久化的实时性
AOF工作流程如下:
随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
AOF命令写入的内容直接是文本协议格式
AOF直接采用文本协议格式的理由:
(1)文本协议具有很好的兼容性
(2)开启AFO后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次开销处理
(3)文本协议具有可读性,方便直接修改和处理
redis引入AOF重写机制压缩文件体积。AOF文件重写是把redis进程内的数据转化为写命令同步到新AOF文件的过程
重写后AOF文件可以变小的原因:
(1)进程内已经超时的数据不再写入文件
(2)旧的AOF文件含有无效命令。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令
(3)多条命令可以合并为一个
AOF重写降低了文件占用空间,更小的AOF文件可以更快地被redis加载
当触发AOF重写时,内部工作流程,如下图:
(1)执行AOF重写请求
(2)父进程执行fork创建子进程,开销等于bgsave
(3)。。。。
AOF和RDB文件都可以用于服务器重启时的数据恢复
fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。
fork耗时问题定位:
如何改善fork操作的耗时:
(1)优先使用物理机或者高效支持fork操作的虚拟化技术,避免使用Xen
(2)控制redis实例最大可用内存,fork耗时跟内存量成正比
(3)合理配置Linux内存分配策略,避免物理内存不足导致fork失败
(4)降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等
子进程负责AOF或者RDB文件的重写,它运行过程主要涉及CPU、 内存、 硬盘三部分消耗
1.CPU
CPU消耗:子进程把进程内的数据分批写入文件,这个过程属于CPU密集操作,通常子进程对单核CPU利用率接近90%
CPU消耗优化:redis是CPU密集型服务,不要做绑定单核CPU操作,也不要和其他CPU密集型服务部署在一起,造成CPU过的竞争
2.内存
内存消耗分析:
内存消耗监控
3.硬盘
硬盘开销优化:
(1)不要和其他高硬盘负载的服务部署在一起
(2)AOF重写时会消耗大量硬盘IO,在AOF重写期间不做fsync操作
(3)分盘存储AOF文件,分摊硬盘压力
复制功能实现了相同数据的多个redis副本
参与复制的redis实例划分为主节点和从节点。
默认情况下,redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点
直接使用命令配置
slaveof {masterHost} {masterPort}
断开复制主要流程:
(1)断开与主节点复制关系
(2)从节点晋升为主节点
从节点断开复制后并不会抛弃原因数据,只是无法再获取主节点上的数据变化。
注意:切主后从节点会清空之前所有的数据
复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致,因此不建议修改从节点的只读模式
redis的复制拓扑结构可以支持单层或多层复制关系,可以分为以下三种:
一主一从、 一主多从、 树状主从结构
1.一主一从结构
用于主节点出现宕机时从节点提供故障转移支持,如下图所示:
当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰
2.一主多从结构
对于读占较大的场景,可以把读命令发送到从节点来分担主节点压力。
对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过渡消耗网络宽带,同时也加重了主节点的负载影响服务稳定性
3.树状主从结构
通过引入中间层,可以有效降低主节点负载和需要传送给从节点的数据量
(1)保存主节点信息
在从节点执行slaveof后从节点只保存主节点的地址信息便直接返回
(2)从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。
(3)发送ping命令
连接建立成功后从节点发送ping请求进行首次通信,ping请求的主要目的如下:
检测主从之间网络套接字是否可用;
检测主从节点当前是否可接受处理命令
(4)权限验证
(5)同步数据集
主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点。
(6)命令持续复制
同步过程分为:全量复制和部分复制
全量复制:一般用于初次复制场景
部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景
1.复制偏移量
参与复制的主从节点都会维护自身复制偏移量
可以对比主从节点的复制偏移量,可以判断主从节点数据是否一致。
2.复制积压缓冲区
复制积压缓冲区是保存在主节点上的一个固定长度队列,默认大小为1MB
主节点响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区。
用于部分复制和复制命令丢失的数据补救
3.主节点运行ID
运行ID的主要作用是用来唯一标识redis节点
4.psync命令
从节点使用psync命令完成部分复制和全量复制功能
命令格式如下:
psync {runId} {offset}
主从第一次建立复制时必须经历的阶段
过程:
(1)发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync ? -1
(2)主节点根据psync ? -1解析出当前为全量复制,回复+FULLRESYNC响应
(3)从节点接收主节点的响应数据保存运行ID和偏移量offset
(4)主节点执行bgsave保存RDB文件到本地
(5)主节点发送RDB给从节点,从节点把接收的RDB文件保存在本地直接作为从节点的数据文件
(6)对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB后,主节点再把缓冲区内的数据发送给从节点,保证主从数据一致性。
流程:
(1)
主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令
心跳判断机制:
(1)主从节点彼此都有心跳检测机制
(2)主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态
(3)从节点在主线程中每隔1秒发送replconf ack {offset} 命令,给主节点上报自身当前的复制偏移量
主节点不但负责数据读写,还负责把写命令同步给从节点。
写命令的发送过程是异步完成的
对于读占比较高的场景,可以通过把一部分读流量分摊到从节点(slave)来减轻主节点(master)压力,同时需要注意永远只对主节点执行写操作
当使用从节点响应读请求时,业务端可能会遇到如下问题:
复制数据延迟
读到过期数据
从节点故障
1.数据延迟
2.读到过期数据
惰性删除:主节点每次处理读取命令时,都会检查键是否超时,如果超时则执行del命令删除键对象,之后del命令也会异步发送给从节点。需要注意的是,为了保证复制的一致性,从节点自身永远不会主动删除超时数据
定时删除:redis主节点在内部定时任务会循环采样一定数量的键,当发现采样的键过期时执行del命令,之后再同步给从节点。
3.从节点故障问题
导致阻塞问题的场景大致分为内在原因和外在原因:
内在原因:不合理地使用API或数据结构、 CPU饱和、 持久化阻塞等
外在原因:CPU竞争、 内存交换、 网络问题
为了感知到阻塞,常见的做法是在应用方异常统计并通过邮件/短信/微信报警
对于高并发的场景我们应该尽量避免在大对象上执行算法复杂度超过O(n)的命令
1.如何发现慢查询
redis原生提供慢查询功能,
slowlog get {n}
可以获取最近的n条慢查询命令,对于超时的命令都会记录到一个定长队列中
2.如何发现大对象
用命令
redis-cli -h {ip} -p {port} bigkeys
内部原理采用分段进行scan操作
单线程的redis处理命令时只能使用一个CPU
CPU饱和是指redis把单核CPU使用率跑到接近100%(使用top命令)
做集群化水平扩展来分摊OPS压力
持久化引起的主线程的阻塞操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞
1.fork阻塞
fork操作发生在RDB和AOF重写时,redis主线程调用fork操作产生共享内存的子进程,由子进程完成持久化文件重写操作
2.AOF刷盘阻塞
3.HugePage写操作阻塞
每次写命令引起的复制内存页单位由4k变为2MB,放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询
进程竞争:redis是典型的CPU密集型应用,不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时,将严重影响redis吞吐量
CPU绑定:部署redis时为了充分利用多核CPU,通常一台机器部署多个实例。特殊情况:当redis父进程创建子进程进行RDB/AOF重写时,如果做了CPU绑定,会与父进程共享一个CPU。子进程重写时对单核CPU使用率通常在90%以上,父进程与子进程将产生激烈CPU竞争,极大影响redis稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU。
识别redis内存交换的检查方法如下:
(1)查询redis进程号
(2)根据进程号查询内存交换信息
预防内存交换的方法有:
(1)保证机器充足的可用内存
(2)确保所有redis实例设置最大可用内存,防止极端情况下redis内存不可控增长
(3)降低系统使用swap优先级
常见的网络问题主要有:连接拒绝、网络延迟、网卡软中断
内存消耗可以分为进程自身消耗和子进程消耗
执行info memory命令获取内存相关指标
redis进程内存消耗主要包括:自身内存 + 对象内存 + 缓冲内存 + 内存碎片
1.对象内存
对象内存是redis内存占用最大的一块,存储着用户所有的数据。
对象内存消耗可以简单理解为sizeof(keys)+ sizeof(values)。
2.缓冲内存
缓冲内存主要包括:客户端缓冲、复制积压缓冲区、 AOF缓冲区
3.内存碎片
分配内存的策略一般采用固定范围的内存块进行分配
以下场景容易出现高内存碎片问题:
(1)频繁做更新操作
(2)大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升
出现高内存碎片问题常见的解决方式如下:
(1)数据对齐:在条件允许的情况下尽量做数据对齐,比如数据量采用数字类型或者固定长度字符串等
(2)安全重启:重启节点可以做到内存碎片重新整理
子进程内存消耗主要指执行AOF/RDB重写时redis创建的子进程内存消耗
THP引入
redis主要通过控制内存上限和回收策略实现内存管理
redis使用maxmemory参数限制最大可用内存。
限制内存的目的主要有:
(1)用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放内存空间
(2)防止所用内存超过服务器物理内存
redis的内存上限可以通过config set maxmemory
进行动态修改,即修改最大可用内存
1.删除过期键对象
redis所有的键都可以设置过期属性,内部保存在过期字典中
惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除
定时任务删除:redis内部维护一个定时任务,默认每秒运行10次。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例、使用快慢两种速率模式回收键
流程说明:
(1)定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键
(2)如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止
(3)如果之前回收键逻辑超时,则在redis触发内部事件之前再次以快模式运行回收过期键任务
(4)快慢两种模式内部删除逻辑相同,只是执行的超时时间不同
2.内存溢出控制策略
当redis所用内存达到maxmemory上限时会触发相应的溢出控制策略;
6种策略:
(1)noeviction:默认策略, 不会删除任何数据,拒绝所有写入操作并返回客户端错误信息
(2)volatile-lru:根据LRU算法删除设置了超时属性的键,直到腾出足够空间为止。如果没有删除可删除的键对象,回退到noeviction策略
(3)allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止
(4)allkeys-random:随机删除所有键,直到腾出足够空间为止
(5)volatile-random:随机删除过期键,直到腾出足够空间为止
(6)volatile-ttl:根据键值对象ttl属性,删除最近将要过期数据,如果没有回退到noeviction策略
对于需要收缩redis内存的场景,可以通过调小maxmemory来实现快速回收。
redis存储的所有值对象在内部定义为RedisObject结构体
redis存储的数据都使用redisObject来封装,包括string、hash、list、set、zset在内的所有数据类型
type字段:表示当前对象使用的数据类型
encoding字段:表示redis内部编码类型
lru字段:记录对象最后一次被访问的时间
refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间
*ptr字段:与对象数据内容相关,如果是整数,直接存储数据;否则表示指向数据的指针
降低redis内存使用最直接的方式就是缩减键(key)和值(value)的长度
(1)key长度:键值越短越好
(2)value长度:选择更高效的序列化工具来降低字节数组大小
共享对象池是指redis内部维护[0-9999]的整数对象池
为什么开启maxmemory和LRU淘汰策略后对象池无效?
LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每个对象最后访问时间存储在RedisObject对象的lru字段。对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间。如果没有设置maxmemory,直到内存被用尽redis也不会触发内存回收,所以共享对象池可以正常工作
为什么只有整数对象池?
首先整数对象池复用的几率最大,其次对象共享的一个关键操作就是判断相等性,redis之所以只有整数对象池,是因为整数比较算法时间复杂度为O(1),只保留一万个整数为了防止对象池浪费
1.字符串结构
redis自己实现了字符串结构,内部简单动态字符串,结构如图所示:
有如下特点:
O(1)时间复杂度获取:字符串长度、已用长度、未用长度
可用于保存字节数组,支持安全的二进制数据存储
内部实现空间预分配机制,降级内存再分配次数
惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留
2.预分配机制
3.字符串重构
字符串重构:指不一定把每份数据作为字符串整体存储。像json这样的数据可以使用hash结构,使用二级结构存储也能节省内存
1.了解编码
为什么对一种数据结构实现多种编码方式?
主要原因是想通过不同编码实现效率和空间的平衡
2.控制编码类型
编码类型转换在redis写入数据时自动完成,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换
3.ziplist编码
ziplist编码主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。
一个ziplist可以包含多个entry(元素),每个entry保存具体的数据(整数或者字节数组)
ziplist数据结构特点如下:
(1)内部表现为数据紧凑的一块连续内存数组
(2)可以模拟双向链表结构,以O(1)时间复杂度入队和出队
(3)新增删除操作涉及内存重新分配或释放,加大了操作的复杂性
(4)读写操作涉及复杂的指针移动,最坏时间复杂度为O(n^2)
(5)适合存储小对象和长度有限的数据
4.intset编码
intset编码是集合(set)类型编码的一种,内部表现为存储有序、 不重复的整数集
通过O(log(n))时间复杂度实现查找和去重操作
对于存储相同的数据内容利用redis的数据结构降低外层键的数量,可以节省大量内存。
hash结构中降低键的数量:
(1)根据键规模在客户端通过分组映射到一组hash对象中,如存在100万个键,可以映射到1000个hash中,每个hash保存1000个元素
(2)hash的field可以用记录原始key字符串,方便哈希查找
(3)hash的value保存原始值对象,确保不要超过hash-max-ziplist-value限制
Redis Sentinel(哨兵)是redis的高可用实现方案
redis的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作用:
(1)作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备“顶”上来,并且保证数据尽量不丢失(主从复制是最终一致性)
(2)从节点可以扩展主节点的读能力,一旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力
主从复制带来的问题:
(1)一旦主节点出现故障,需要手动将一个节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点复制新的主节点,整个过程都需要人工干预
(2)主节点的写能力受到单机的限制
(3)主节点的存储能力受到单机的限制
当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。
Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给redis应用方。(整个过程完全是自动的)
Redis Sentinel与Redis主从复制模式只是多了若干Sentinel节点
Sentinel节点集合会定期对所有节点进行监控,特别是对主节点的故障实现自动转移。
Redis Sentinel具有以下几个功能:
(1)监控:Sentinel节点会定期检测Redis数据节点、其余Sentinel节点是否可达
(2)通知:Sentinel节点会将故障转移的结果通知给应用方
(3)主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
Redis Senti包含若干Sentinel节点的好处:
(1)对于节点的故障判断是由多个Sentinel节点共同完成,,这样可以有效的防止误判
(2)Sentinel节点集合是由若干个Sentinel节点组成的,这样即使个别Sentinel节点不可用,整个Sentinel节点集合依然是健壮的
Redis Sentinel通过三个定时任务完成对各个节点发现和监控:
(1)每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构
这个定时任务作用可以表现在三个方面:
通过向主节点执行info命令,获取从节点的信息;
当有新的从节点加入时都可以立刻感知出来;
节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息
(2)每隔2秒,每个Sentinel节点会向Redis数据节点的_sentinel_:hello频道上发送该Sentinel节点对于节点的判断以及当前Sentinel节点的信息
这个定时任务完成以下两个工作:
发现新的Sentinel节点
Sentinel节点之间交换主节点的状态
(3)每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确定这些节点当前是否可达。这个定时任务是节点失败判定的重要依据。
1.主观下线
进行心跳检测时,这些节点没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线
2.客观下线
当Sentinel主观下线的节点是主节点时,该Sentinel节点会向其他Sentinel节点询问对主节点的判断,当超过< quorum >个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定
故障转移的工作只需要一个Sentinel节点来完成即可,所以Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。
Redis使用了Raft算法实现领导者选举
1.从节点的作用
2.Redis Sentinel读写分离设计思路
Redis Sentinel在对各个节点的监控中,如果有对应事件的发生,都会发出相应的事件消息,所以在设计Redis Sentinel的从节点高可用时,只要能够实时掌握所以从节点的状态,把所有从节点看做一个资源池,无论是上线还是下线从节点,客户端都能及时感知到(将其从资源中添加或者删除),这样从节点的高可用目标就达到了
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点,每个节点负责整个数据的一个子集
由于Redis Cluster采用哈希分区规则,,常见的哈希分区规则如下:
(1)节点取余分区
使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N计算出哈希值,用来解决数据映射到哪一个节点上。
这种方案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
这种方式优点:
简单性,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数
扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。
(2)一致性哈希分区
一致性哈希分区实现思路是为系统中每个节点分配一个token,范围一般在0~2^32,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响,但一致性哈希存在几个问题:
加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景
当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案
普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡
(3)虚拟槽分区
虚拟槽分区巧妙使用哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽
当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区。Redis Cluster就是采用虚拟槽分区
Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内。每一个节点负责维护一部分槽以及槽所映射的键值数据
redis虚拟槽分区的特点:
(1)解耦数据和节点之间的关系,简化了节点扩容和收缩难度
(2)节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
(3)支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景
redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。