Redis7学习笔记

357 阅读1小时+

本文为尚硅谷redis7课程笔记

--中文文档 : Redis中文网

-- 官网 : Docs

-- 视频链接: 尚硅谷Redis

1. 数据类型

1.1. 十大数据类型概述

1.1.1. redis字符串(String)

String是redis最基本的数据类型,一个key对应一个value。

string类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。

string类型是Redis最基本的数据类型,一个redis中字符串value最多可以是512M

1.1.2. redis列表(List)

Redis列表是最简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)\textcolor{blue}{头部(左边)或者尾部(右边)},它的底层实际是个双端链表\textcolor{red}{双端链表},最多可以包含2^32-1个元素(4294967295,每个列表超过40亿个元素)

1.1.3. redis哈希表(Hash)

Redis Hash是一个string类型的field(字段)和value(值)的映射表,Hash特别适合用户存储对象。

Redis中每个Hash可以存储2^32-1个键值对(40多亿)

1.1.4. redis集合(Set)

Redis的Set是string类型的无序集合\textcolor{red}{无序集合}。集合成员是唯一的,这就意味着集合中不能出现重复的数据,集合对象的编码可以是intset或者Hashtable。

Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

集合中最大的成员数为2^32-1(4294967295,每个集合可存储40多亿个成员)

1.1.5. redis有序集合(ZSet)

zset(sorted set:有序集合)

Redis 的 Zset 和 Set 一样,都是字符串类型元素的集合,且不允许重复的成员。

不同的是,每个元素都会关联一个类型为 double 的分数(score) ,Redis 通过分数为集合中的成员进行从小到大的排序。

  • 成员是唯一的,但是分数(score)可以重复
  • 集合是通过跳表(Skip List)实现的,因此添加、删除、查找的时间复杂度为 O(log(n))
  • 集合中最大的成员数量为 232−12^{32} - 1232−1(约 40 亿个)。

1.1.6. redis地理空间(GEO)

Redis GEO主要用于存储地理位置信息,并对存储的信息进行操作,包括:

添加地理位置的坐标。

获取地理位置的坐标。

计算两个位置之间的距离。

根据用户给定的经纬度坐标来获取指定范围内的地址位置集合。

1.1.7. redis基数统计(HyperLogLog)

HyperLogLog是用来做基数统计\textcolor{red}{基数统计}的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需要的空间总是固定且是很小的。

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。

1.1.8. redis位图(bitmap)

由0和1状态表现的二进制位的bit数组

1.1.9. redis位域(bitfield)

通过bitfield命令可以一次性操作多个比特位域(指的是连续的多个比特位)\textcolor{red}{比特位域(指的是连续的多个比特位)},它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应的执行结果。

说白了就是通过bitfield命令我们可以一次性对多个比特位域进行操作。

1.1.10. redis流(Stream)

Redis Stream是Redis5.0版本新增加的数据结构。

Redis Stream主要用于消息队列(MQ,Message Queue),Redis本身就是一个Redis发布订阅(pub/sub)来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis宕机等,消息就会被丢弃。

简单来说发布订阅(pub/sub)可以分发消息,但无法记录历史消息。

而Redis Stream提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

1.2. 通用指令 官网 : Commands

  • KEYS:查看符合模板的所有key
    • 不建议在生产环境设备上使用,因为Redis是单线程的,执行查询的时候会阻塞其他命令,当数据量很大的时候,使用KEYS进行模糊查询,效率很差
  • DEL:删除一个指定的key
    • 也可以删除多个key,DEL name age,会将name和age都删掉
  • EXISTS:判断key是否存在
    • EXISTS name,如果存在返回1,不存在返回0
  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
    • EXPIRE name 20,给name设置20秒有效期,到期自动删除
  • TTL:查看一个key的剩余有效期(Time-To-Live)
    • TTL name,查看name的剩余有效期,如果未设置有效期,则返回-1
DEL key	#该命令用于在 key 存在时删除 key。

DUMP key	#序列化给定 key ,并返回被序列化的值。

EXISTS key	#检查给定 key 是否存在。

EXPIRE key seconds	#为给定 key 设置过期时间,以秒计。


EXPIREAT key timestamp	
#EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。

PEXPIRE key milliseconds	#设置 key 的过期时间以毫秒计。

PEXPIREAT key milliseconds-timestamp	
#设置 key 过期时间的时间戳(unix timestamp) 以毫秒计

KEYS pattern	#查找所有符合给定模式( pattern)的 key 。

MOVE key db	#将当前数据库的 key 移动到给定的数据库 db 当中。

PERSIST key	#移除 key 的过期时间,key 将持久保持。

PTTL key	  #以毫秒为单位返回 key 的剩余的过期时间。

TTL key	#以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。

RANDOMKEY	#从当前数据库中随机返回一个 key 。

RENAME key newkey	#修改 key 的名称

RENAMENX key newkey	   #仅当 newkey 不存在时,将 key 改名为 newkey 。

SCAN cursor [MATCH pattern] [COUNT count]	#迭代数据库中的数据库键。

TYPE key	返回 key 所储存的值的类型。

KEYS:查看符合模板的所有key
#不建议在生产环境设备上使用,因为Redis是单线程的,执行查询的时候会阻塞其他命令,当数据量很大的时候,使用KEYS进行模糊查询,效率很差

select dbindex
#切换数据库【0-15】,默认为0

10.dbsize
#查看当前数据库key的数量

flushdb
#清空当前库

flushall
#通杀全部库

1.3. Redis字符串(String)

1.3.1. 概述

String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。

String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。

String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

1.3.2. 常用命令

命令描述
SET添加或者修改一个已经存在的String类型的键值对
GET根据key获取String类型的value
MEST批量添加多个String类型的键值对
MGET根据多个key获取多个String类型的value
INCR让一个整形的key自增1
INCRBY让一个整形的key自增并指定步长值,例如:incrby num 2,让num值自增2
INCRBYFLOAT让一个浮点类型的数字自增并指定步长值
SETNX添加一个String类型的键值对,前提是这个key不存在,否则不执行,可以理解为真正的新增
SETEX添加一个String类型的键值对,并指定有效期

SET key value 
[NX | XX] [GET] [EX seconds | PX milliseconds |EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]		添加键值对

get <key>:查询对应键值

append <key><value>:将给定的<value> 追加到原值的末尾

strlen <key>获得值的长度

setnx <key><value>:只有在 key 不存在时,设置 key 的值

incr <key>:将 key 中储存的数字值增1;只能对数字值操作,如果为空,新增值为1,具有原子性

decr <key>:将 key 中储存的数字值减1;只能对数字值操作,如果为空,新增值为-1

incrby / decrby <key><步长>:将 key 中储存的数字值增减<步长>,自定义步长。

mset <key1><value1> <key2><value2> ..... :同时设置一个或多个 key-value对

mget <key1> <key2> <key3> .....:同时获取一个或多个 value

msetnx <key1><value1> <key2><value2> ..... :同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。具有原子性,有一个失败则都失败

getrange <key><起始位置><结束位置>:获得值的范围,类似java中的substring,前包,后包

setrange <key><起始位置><value>:用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。

setex <key><过期时间><value>:设置键值的同时,设置过期时间,单位秒。

getset <key><value>:以新换旧,设置了新值同时获得旧值。

set指令可跟随的参数

  • EX seconds -- 设置指定的到期时间,以秒为单位(正整数)。
  • EXAT timestamp-seconds -- 设置键值对过期的指定 Unix 时间,以秒为单位(正整数)。
  • NX-- 仅设置尚不存在的键值对。
  • XX-- 仅设置已存在的键值对。
  • KEEPTTL-- 保留与键值对关联的过期时间。

注意 : m 的批量操作具有原子性 , 要么一起成功 ,要么不会执行

1.3.3. 数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。

是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

1.4. Redis列表(List)

1.4.1. 概述

  • 单键多值
  • Redis 列表是简单的字符串列表,按照插入顺序排序。
  • 你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
  • 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差

1.4.2. 常用命令

命令描述
LPUSH key element …向列表左侧插入一个或多个元素
LPOP key移除并返回列表左侧的第一个元素,没有则返回nil
RPUSH key element …向列表右侧插入一个或多个元素
RPOP key移除并返回列表右侧的第一个元素
LRANGE key star end返回一段角标范围内的所有元素
BLPOP和BRPOP与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
lpush/rpush <key><value1> <value2><value3> .... :从左边/右边插入一个或多个值。

lpop/rpop <key>:从左边/右边吐出一个值。值在键在,值光键亡。

lrange <key> <start> <stop>:按照索引下标获得元素(从左到右)

lrange <key> 0-1: 0左边第一个,-1右边第一个,(0-1表示获取所有)

lindex <key> <index>:按照索引下标获得元素(从左到右)

llen <key>:获得列表长度

linsert <key> before/after "<value>" "<newvalue>":在<value>的 后面/前面 插入<newvalue>插入值

lrem <key> <n> "<value>":从左边删除n个value(从左到右)

lset <key> <index> <value>:将列表key下标为index的值替换成value

1.4.3. 数据结构

  • List的数据结构为快速链表quickList。

  • 首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

  • 当数据量比较多的时候才会改成quicklist,因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

  • Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

1.5. Redis集合(Set)

1.5.1. 概述

  • Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的
  • 当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
  • Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
  • 一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

特点

  • Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:
    • 无序
    • 元素不可重复
    • 查找快
    • 支持交集、并集、差集等功能

1.5.2. 常用命令

命令描述
SADD key member …向set中添加一个或多个元素
SREM key member …移除set中的指定元素
SCARD key返回set中元素的个数
SISMEMBER key member判断一个元素是否存在于set中
SMEMBERS获取set中的所有元素
SINTER key1 key2 …求key1与key2的交集
SUNION key1 key2 …求key1与key2的并集
SDIFF key1 key2 …求key1与key2的差集
sadd <key> <value1> <value2> ..... :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

smembers <key>:取出该集合的所有值。

sismember <key><value>:判断集合<key>是否为含有该<value>值,有1,没有0

scard<key>:返回该集合的元素个数。

srem <key><value1><value2> ....: 删除集合中的某个元素。

spop <key>:随机从该集合中吐出一个值,会从集合中删除。

srandmember <key><n>:随机从该集合中取出n个值。不会从集合中删除 。

smove <source-key><destination-key><value>:把集合中一个值从一个集合移动到另一个集合
sinter <key1><key2>:返回两个集合的交集元素。

sintercard <key1><key2> 返回交集的元素个数

sunion <key1><key2>:返回两个集合的并集元素。

sdiff <key1><key2>:两个集合的差集元素(在key1中不包含key2中的)
数据结构

1.6. Redis有序集合Zset(sorted set)

1.6.1. 概述

  • Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
  • 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。
  • 集合的成员是唯一的,但是评分可以是重复了 。
  • 因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
  • 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
  • SortedSet具备下列特性:
    • 可排序
    • 元素不重复
    • 查询速度快
  • 因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

1.6.2. 常见命令

命令描述
ZADD key score member添加一个或多个元素到sorted set ,如果已经存在则更新其score值
ZREM key member删除sorted set中的一个指定元素
ZSCORE key member获取sorted set中的指定元素的score值
ZRANK key member获取sorted set 中的指定元素的排名
ZCARD key获取sorted set中的元素个数
ZCOUNT key min max统计score值在给定范围内的所有元素的个数
ZINCRBY key increment member让sorted set中的指定元素自增,步长为指定的increment值
ZRANGE key min max按照score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max按照score排序后,获取指定score范围内的元素
ZDIFF、ZINTER、ZUNION求差集、交集、并集

注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:

  • 升序获取sorted set 中的指定元素的排名:ZRANK key member
  • 降序获取sorted set 中的指定元素的排名:ZREVRANK key memeber
zadd <key> <score1><value1> <score2><value2>…:将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

zrange <key><start><stop> [WITHSCORES]:返回有序集 key 中,下标在<start><stop>之间的元素
带WITHSCORES,可以让分数一起和值返回到结果集。

zrangebyscore key min max [withscores] [limit offset count]:返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

zrevrangebyscore key max min [withscores] [limit offset count]:同上,改为从大到小排列。

zincrby <key><increment><value>:为元素的score加上增量increment

zrem <key><value>:删除该集合下,指定值的元素

zcount <key><min><max>:统计该集合,分数区间内的元素个数

zrank <key><value>:返回该值在集合中的排名,从0开始。

1.6.3. 数据结构

  • SortedSet(zset)是Redis提供的一个非常特别的数据结构
  • 一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score
  • 一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
  • zset底层使用了两个数据结构
  • hash作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
  • 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

1.7. Redis哈希(Hash)

1.7.1. 概述

  • Redis hash 是一个键值对集合。

  • Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象,类似Java里面的Map<String,Object>

  • 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

1.7.2. 常用命令

命令描述
HSET key field value添加或者修改hash类型key的field的值
HGET key field获取一个hash类型key的field的值
HMSET批量添加多个hash类型key的field的值
HMGET批量获取多个hash类型key的field的值
HGETALL获取一个hash类型的key中的所有的field和value
HKEYS获取一个hash类型的key中的所有的field
HINCRBY让一个hash类型key的字段值自增并指定步长
HSETNX添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
hset <key><field><value>:给<key>集合中的 <field>键赋值<value>

hget <key1><field>:从<key1>集合<field>取出 <value>

hmset <key1> <field1><value1> <field2><value2>... :批量设置hash的值

hexists <key1><field>:查看哈希表 key 中,给定域 field 是否存在。

hkeys <key>:列出该hash集合的所有field

hvals <key>:列出该hash集合的所有value

hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量<increment>

hsetnx <key><field><value>:将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .

1.7.3. 数据结构

  • Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)

  • 当field-value长度较短且个数较少时,使用ziplist,否则使hashtable

1.8. Redis位图(bitmap)

1.8.1. 概述

现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位

Redis提供了Bitmaps这个“数据类型”可以实现对位有效地提高内存使用率和开发效率

Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作

Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量

1.8.2. 常用指令

setbit设置Bitmaps中某个偏移量的值(0或1)注:*offset:偏移量从0开始offset从0开始
getbit获取Bitmaps中某个偏移量的值
strlen key返回所占用的字节数
bitcount [start end]统计字符串从start字节到end字节比特值为1的数量(注意是字节)
bitop and(or/not/xor) [key…]bitop是一个复合操作, 它可以做多个Bitmaps的and(交集)、or(并集)、 not(非) 、 xor(异或) 操作并将结果保存在destkey中
setbit<key><offset><value>		#设置Bitmaps中某个偏移量的值(01)
#注:*offset:偏移量从0开始offset从0开始


getbit<key><offset>:		获取Bitmaps中某个偏移量的值

strlen key			返回所占用的字节数

bitcount <key> [start end]  #统计字符串从start字节到end字节比特值为1的数量(注意是字节)

bitop and(or/not/xor) <destkey> [key…]	bitop是一个复合操作, 它可以做多个Bitmaps的and(交集)、or(并集)、 not(非) 、 xor(异或) 操作并将结果保存在destkey中
    • redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置
    • start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位

1.8.3. 案例对比

Bitmaps与set对比

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的

但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示

很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0,虽然只有10w用户,但是10w用户中可能 有人的id是1000w,因此必须要分配1000w位

1.9. Redis基数(HyperLogLog)

1.9.1. 概述

1.9.1.1. 背景 :

像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?

这种求集合中不重复元素个数的问题称为基数问题。

解决基数问题有很多种方案:

  • 数据存储在MySQL表中,使用distinct count计算不重复个数
  • 使用Redis提供的hash、set、bitmaps等数据结构来处理

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。

能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog

1.9.1.2. 介绍

Redis HyperLogLog 是用来做基数统计的算法

  • HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

  • 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

  • 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

那什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素的个数)为5。

基数估计就是在误差可接受的范围内,快速计算基数。

1.9.2. 指令

pfadd < element> [element ...]添加指定元素到 HyperLogLog 中
pfcount [key ...]计算HLL的近似基数,可以计算多个HLL
pfmerge [sourcekey ...]将一个或多个HLL()合并后的结果存储在另一个HLL()中
pfadd <key>< element> [element ...]	添加指定元素到 HyperLogLog 中

pfcount<key> [key ...] 	计算HLL的近似基数,可以计算多个HLL

pfmerge<destkey><sourcekey> [sourcekey ...]	将一个或多个HLL(<sourcekey>)合并后的结果存储在另一个HLL(<destkey>)中
  • Redis 3.2 中增加了对GEO类型的支持。
  • GEO,Geographic,地理信息的缩写。
  • 该类型,就是元素的2维坐标,在地图上就是经纬度。
  • redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

1.10. Redis(地理空间)Geospatial

1.10.1. 概述

    • Redis 3.2 中增加了对GEO类型的支持。
    • GEO,Geographic,地理信息的缩写。
    • 该类型,就是元素的2维坐标,在地图上就是经纬度。
    • redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
    • 底层是基于 zset 实现的 , 只不过这里的 score 是经纬度

1.10.2. 指令

  1. geoadd
  • 格式geoadd< longitude> [longitude latitude member...]:

添加地理位置(经度,纬度,名称)

  • 注意
    • 两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。
    • 有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。
    • 当坐标位置超出指定范围时,该命令将会返回一个错误。
    • 已经添加的数据,是无法再次往里面添加的
  1. geopos
  • 格式geopos [member...]

  • 获得指定地区的坐标值

  1. geodist
  • 格式 geodist [m|km|ft|mi ]
  • 获取两个位置之间的直线距离
  • 注意
    • 单位:
      m 表示单位为米[默认值]。
      km 表示单位为千米。
      mi 表示单位为英里。
      ft 表示单位为英尺。

    • 如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位

  1. georadius
  • 格式georadius< longitude>radius m|km|ft|mi

  • 以给定的经纬度为中心,找出某一半径内的元素

1.11. Reids 流( stream)

1.11.1. 概述

Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能 , 消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。

基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。

上图解析:

  • Consumer Group :消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。
  • last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
  • pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。

1.11.2. 指令

1.11.2.1. 消息队列相关命令:
  • XADD - 添加消息到末尾
  • XTRIM - 对流进行修剪,限制长度
  • XDEL - 删除消息
  • XLEN - 获取流包含的元素数量,即消息长度
  • XRANGE - 获取消息列表,会自动过滤已经删除的消息
  • XREVRANGE - 反向获取消息列表,ID 从大到小
  • XREAD - 以阻塞或非阻塞方式获取消息列表
1.11.2.1.1. XADD

使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列,XADD 语法格式:

XADD key ID field value [field value ...]
● key :队列名称,如果不存在就创建
● ID :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
● field value : 记录
1.11.2.1.2. XTRIM

使用 XTRIM 对流进行修剪,限制长度, 语法格式:

XTRIM key MAXLEN [~] count
● key :队列名称
● MAXLEN :长度
● count :数量
1.11.2.1.3. XDEL

使用 XDEL 删除消息,语法格式:

XDEL key ID [ID ...]	
● key:队列名称
● ID :消息 ID
1.11.2.1.4. XLEN

使用 XLEN 获取流包含的元素数量,即消息长度,语法格式:

XLEN keykey:队列名称
1.11.2.1.5. XRANGE

使用 XRANGE 获取消息列表,会自动过滤已经删除的消息 ,语法格式:

XRANGE key start end [COUNT count]
● key :队列名
● start :开始值, - 表示最小值
● end :结束值, + 表示最大值
● count :数量
1.11.2.1.6. XREAD

使用 XREAD 以阻塞或非阻塞方式获取消息列表 ,语法格式:

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
● count :数量
● milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式
● key :队列名
● id :消息 ID

案例

消费者通过 XREAD 命令从消息队列中读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取(注意是输入消息 ID 的下一条信息开始读取,不是查询输入ID的消息)。

# 从 ID 号为 1654254953807-0 的消息开始,读取后续的所有消息(示例中一共 1 条)。
> XREAD STREAMS mymq 1654254953807-0
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"

如果想要实现阻塞读(当没有数据时,阻塞住),可以调用 XRAED 时设定 BLOCK 配置项,实现类似于 BRPOP 的阻塞读取操作。

比如,下面这命令,设置了 BLOCK 10000 的配置项,10000 的单位是毫秒,表明 XREAD 在读取最新消息时,如果没有消息到来,XREAD 将阻塞 10000 毫秒(即 10 秒),然后再返回。

# 命令最后的“$”符号表示读取最新的消息
> XREAD BLOCK 10000 STREAMS mymq $
(nil)
(10.00s)

Stream 的基础方法,使用 xadd 存入消息和 xread 循环阻塞读取消息的方式可以实现简易版的消息队列,交互流程如下图所示:

1.11.2.2. 消费者组相关命令:
  • XGROUP CREATE - 创建消费者组
  • XREADGROUP GROUP - 读取消费者组中的消息
  • XACK - 将消息标记为"已处理"
  • XGROUP SETID - 为消费者组设置新的最后递送消息ID
  • XGROUP DELCONSUMER - 删除消费者
  • XGROUP DESTROY - 删除消费者组
  • XPENDING - 显示待处理消息的相关信息
  • XCLAIM - 转移消息的归属权
  • XINFO ****- 查看流和消费者组的相关信息;
  • XINFO GROUPS - 打印消费者组的信息;
  • XINFO STREAM - 打印流信息
1.11.2.2.1. XGROUP CREATE

使用 XGROUP CREATE 创建消费者组,语法格式:

XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
● key :队列名称,如果不存在就创建
● groupname :组名。
● $ : 表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。
从头开始消费:
XGROUP CREATE mystream consumer-group-name 0-0  

从尾部开始消费:
XGROUP CREATE mystream consumer-group-name $
1.11.2.2.2. XREADGROUP GROUP

使用 XREADGROUP GROUP 读取消费组中的消息,语法格式:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
● group :消费组名
● consumer :消费者名。
● count : 读取数量。
● milliseconds : 阻塞毫秒数。
● key : 队列名。
● ID : 消息 ID。

eg : XREADGROUP GROUP consumer-group-name consumer-name COUNT 1 STREAMS mystream >
1.11.2.2.3. 案例介绍

Stream 可以以使用 XGROUP 创建消费组,创建消费组之后,Stream 可以使用 XREADGROUP 命令让消费组内的消费者读取消息。

创建两个消费组,这两个消费组消费的消息队列是 mymq,都指定从第一条消息开始读取:

# 创建一个名为 group1 的消费组,0-0 表示从第一条消息开始读取。
> XGROUP CREATE mymq group1 0-0
OK
# 创建一个名为 group2 的消费组,0-0 表示从第一条消息开始读取。
> XGROUP CREATE mymq group2 0-0
OK

消费组 group1 内的消费者 consumer1 从 mymq 消息队列中读取所有消息的命令如下:

# 命令最后的参数“>”,表示从第一条尚未被消费的消息开始读取。
> XREADGROUP GROUP group1 consumer1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"

消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息

比如说,我们执行完刚才的 XREADGROUP 命令后,再执行一次同样的命令,此时读到的就是空值了:

> XREADGROUP GROUP group1 consumer1 STREAMS mymq >
(nil)

但是,不同消费组的消费者可以消费同一条消息(但是有前提条件,创建消息组的时候,不同消费组指定了相同位置开始读取消息)

比如说,刚才 group1 消费组里的 consumer1 消费者消费了一条 id 为 1654254953808-0 的消息,现在用 group2 消费组里的 consumer1 消费者消费消息:

> XREADGROUP GROUP group2 consumer1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"

因为我创建两组的消费组都是从第一条消息开始读取,所以可以看到第二组的消费者依然可以消费 id 为 1654254953808-0 的这一条消息。因此,不同的消费组的消费者可以消费同一条消息。

使用消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的。

例如,我们执行下列命令,让 group2 中的 consumer1、2、3 各自读取一条消息。

# 让 group2 中的 consumer1 从 mymq 消息队列中消费一条消息
> XREADGROUP GROUP group2 consumer1 COUNT 1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"
# 让 group2 中的 consumer2 从 mymq 消息队列中消费一条消息
> XREADGROUP GROUP group2 consumer2 COUNT 1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654256265584-0"
         2) 1) "name"
            2) "xiaolincoding"
# 让 group2 中的 consumer3 从 mymq 消息队列中消费一条消息
> XREADGROUP GROUP group2 consumer3 COUNT 1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654256271337-0"
         2) 1) "name"
            2) "Tom"

1.11.3. 思考题

1.11.3.1. 基于 Stream 实现的消息队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?

Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。

消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成,整个流程的执行如下图所示:

如果消费者没有成功处理消息,它就不会给 Streams 发送 XACK 命令,消息仍然会留存。此时,消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息

例如,我们来查看一下 group2 中各个消费者已读取、但尚未确认的消息个数,命令如下:

127.0.0.1:6379> XPENDING mymq group2
1) (integer) 3
2) "1654254953808-0"  # 表示 group2 中所有消费者读取的消息最小 ID
3) "1654256271337-0"  # 表示 group2 中所有消费者读取的消息最大 ID
4) 1) 1) "consumer1"
      2) "1"
   2) 1) "consumer2"
      2) "1"
   3) 1) "consumer3"
      2) "1"

如果想查看某个消费者具体读取了哪些数据,可以执行下面的命令:

# 查看 group2 里 consumer2 已从 mymq 消息队列中读取了哪些消息
> XPENDING mymq group2 - + 10 consumer2
1) 1) "1654256265584-0"
   2) "consumer2"
   3) (integer) 410700
   4) (integer) 1

可以看到,consumer2 已读取的消息的 ID 是 1654256265584-0。

一旦消息 1654256265584-0 被 consumer2 处理了,consumer2 就可以使用 XACK 命令通知 Streams,然后这条消息就会被删除

> XACK mymq group2 1654256265584-0
(integer) 1

当我们再使用 XPENDING 命令查看时,就可以看到,consumer2 已经没有已读取、但尚未确认处理的消息了。

> XPENDING mymq group2 - + 10 consumer2
(empty array)

好了,基于 Stream 实现的消息队列就说到这里了,小结一下:

  • 消息保序:XADD/XREAD
  • 阻塞读取:XREAD block
  • 重复消息处理:Stream 在使用 XADD 命令,会自动生成全局唯一 ID;
  • 消息可靠性:内部使用 PENDING List 自动保存消息,使用 XPENDING 命令查看消费组已经读取但是未被确认的消息,消费者使用 XACK 确认消息;
  • 支持消费组形式消费数据
1.11.3.2. Redis 基于 Stream 消息队列与专业的消息队列有哪些差距?

一个专业的消息队列,必须要做到两大块:

  • 消息不丢。
  • 消息可堆积。
  1. Redis Stream 消息会丢失吗?

使用一个消息队列,其实就分为三大块:生产者、队列中间件、消费者,所以要保证消息就是保证三个环节都不能丢失数据。

Redis Stream 消息队列能不能保证三个环节都不丢失数据?

  • Redis 生产者会不会丢消息?生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。 从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 ( MQ 中间件) 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,如果返回异常则进行消息重发,那么这个阶段是不会出现消息丢失的。
  • Redis 消费者会不会丢消息?不会,因为 Stream ( MQ 中间件)会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,但是未被确认的消息。消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。等到消费者执行完业务逻辑后,再发送消费确认 XACK 命令,也能保证消息的不丢失。
  • Redis 消息中间件会不会丢消息?,Redis 在以下 2 个场景下,都会导致数据丢失:

可以看到,Redis 在队列中间件环节无法保证消息不丢。像 RabbitMQ 或 Kafka 这类专业的队列中间件,在使用时是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,也就是有多个副本,这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。

  1. Redis Stream 消息可堆积吗?

Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。

所以 Redis 的 Stream 提供了可以指定队列最大长度的功能,就是为了避免这种情况发生。

当指定队列最大长度时,队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。这么来看,Stream 在消息积压时,如果指定了最大长度,还是有可能丢失消息的。

但 Kafka、RabbitMQ 专业的消息队列它们的数据都是存储在磁盘上,当消息积压时,无非就是多占用一些磁盘空间。

因此,把 Redis 当作队列来使用时,会面临的 2 个问题:

  • Redis 本身可能会丢数据;
  • 面对消息挤压,内存资源会紧张;

所以,能不能将 Redis 作为消息队列来使用,关键看你的业务场景:

  • 如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。
  • 如果你的业务有海量消息,消息积压的概率比较大,并且不能接受数据丢失,那么还是用专业的消息队列中间件吧。

补充:Redis 发布/订阅机制为什么不可以作为消息队列?

发布订阅机制存在以下缺点,都是跟丢失数据有关:

  1. 发布/订阅机制没有基于任何数据类型实现,所以不具备「数据持久化」的能力,也就是发布/订阅机制的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,发布/订阅机制的数据也会全部丢失。
  2. 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。
  3. 当消费端有一定的消息积压时,也就是生产者发送的消息,消费者消费不过来时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是 client-output-buffer-limit pubsub 32mb 8mb 60。

所以,发布/订阅机制只适合即时通讯的场景,比如构建哨兵集群(opens new window)的场景采用了发布/订阅机制。

1.12. Redis 位域(bitfield)

2. Redis 持久化技术

四大持久化模式

  1. RDB 2.AOF 3.RDB + AOF 4. 纯缓存

2.1. RDB 概述

RDB持久性以指定的时间间隔执行数据集的时间点快照
也就是说在一定的时间间隔内,将某一时刻的数据和状态以文件的形式写到磁盘上,这个快照文件交dump.rdb

在redis.conf中配置文件名称,默认为dump.rdb

2.2. RDB 快照触发流程

2.2.1. 触发场景

  • 配置文件中默认的快照配置
  • 手动 save/bgsave 命令
  • 执行flush / flushdb 命令也会产生 dump.rdb 文件,但里面是空的,无意义
  • 执行 shutdown 且没有设置开启 AOF 持久化
  • 主从复制时,主节点自动触发

2.2.2. 自动触发

  1. Redis7 默认的更新策略

可以根据业务需求,修改配置

  1. RDB 自动快照的关闭

方法 1 : 动态所有停止 RDB 保存规则的方法: redis-cli config set save “”

方法 2: 修改配置文件 , 解除 save " "的注释(推荐)

2.2.3. 手动触发

  • save:优先级高,执行该命令时其他进程都会停下来等这个命令执行完,因此在此期间redis对外缓存功能失效,很少用
  • bgsave:在执行该命令的时候,redis产生一个fork,相当于复制了一个父进程,由此来异步保存RDB
lastsave   //查看上一次保存的时间戳,在linux下查看日期

2.3. RDB优化参数

2.3.1. 12.6.3 flushall命令

执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义

2.3.2. stop-writes-on-bgsave-error

当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes

2.3.3. rdbcompression 压缩文件

  • 对于存储到磁盘中的快照,可以设置是否进行压缩存储。
  • 如果是的话,redis会采用LZF算法进行压缩。
  • 如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。
  • 推荐yes

2.3.4. 12.6.8 rdbchecksum 检查完整性

  • 在存储快照后,还可以让redis使用CRC64算法来进行数据校验,
  • 这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
  • 推荐yes.

2.4. AOF 概述

2.4.1. 是什么

  • AOF以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录)
  • 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据
  • 换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
  • AOF默认不开启 可以在redis.conf配置中开启

2.4.2. AOF持久化流程

  • 客户端的请求写命令会被append追加到AOF缓冲区内;
  • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
  • AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  • Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

2.4.3. AOF 缓冲区三种写回策略

  • always 同步写回,每个写命令执行完立刻同步地将日志写回磁盘
  • everysec (默认)每秒写回,每个写命令执行完,只是先把日志写到AOF缓冲区,每隔1s把缓存区地数据写入磁盘
  • no 操作系统控制写回,只是将日志先写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘

2.5. AOF 的重写机制

  • AOF采用文件追加方式,文件会越来越大
  • 为避免出现文件会越来越大此种情况,新增了重写机制,
  • 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

2.5.1. 重写原理

  • AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(与rdb相同也是先写临时文件最后再rename)
  • redis4.0版本后的重写,是指就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作
2.5.1.1. no-appendfsync-on-rewrite 指令:
  • 如果no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
  • 如果no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞(数据安全,但是性能降低)。

2.5.2. 重写时间

  1. 自动配置

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。

(可根据业务需求修改配置 .conf 文件)

  1. 手动配置
  • 客户端向服务器发送 bgrewriteaof 命令

2.5.3. 重写流程

  • bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
  • 主进程fork出子进程执行重写操作,保证主进程不会阻塞。
  • 子程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

①子进程写完新的AOF文件后,向主进程发信号,父进程更新统 计信息。

②主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

  • 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

2.5.4. 配置指令

2.6. RDB+AOF混合

AOF默认是关闭的,当两者共存时,AOF的优先级高

当redis 重启时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整

RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。

2.7. 纯缓存模式

同时关闭RDB + AOF

  1. save “”
  • 禁用rdb
  • 禁用db持久化模式下,我们仍然可以使用命令save、bgsave生成rdb文件
  1. appendonly no
  • 禁用aof

  • 禁用aof持久化模式下,我们仍然可以使用命令 bgrewriteaof生成aof文件

3. Redis 事务

3.1.1. Redis的事务定义

  1. Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  2. Redis事务的主要作用就是串联多个命令防止别的命令插队。
  3. 工作流程 : Redis事务实现的一个核心结构是事务队列,当服务器以事务状态运行时,针对于接收到的不同命令会有不同的操作:
  • 如果是MULTI、EXEC、WATCH和DISCARD其中的任意一个,服务器立刻执行
  • 如果不是上述的四个命令,那么服务器就会将其放入到一个事务队列中,然后向服务器返回QUEUED恢复,表示命令已经入队,等待执行

3.1.2. Redis事务三大特性

  1. 单独的隔离操作:
    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 redis单线程

  2. 没有隔离级别的概念:
    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

  3. 不保证原子性:
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

3.1.3. 事务指令

  • Multi -- 开启事务
  • Exec -- 执行
  • discard -- 半途中断

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

组队的过程中可以通过discard来放弃组队。

3.1.4. 事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消 (全部取消)

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。 (部分取消)

3.1.5. 事务冲突问题

3.1.6. 例子

一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000

3.1.6.1. 悲观锁(解决事务冲突)


悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

3.1.6.2. 乐观锁(解决事务冲突)


乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的

3.1.7. 乐观锁在Redis中的具体使用

指令 : watch key [key…]

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断


unwatch

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

4. * 发布和订阅

客户端可以订阅频道如下图

当给这个频道发布消息后,消息就会发送给订阅的客户端

发布订阅命令行实现

打开一个客户端订阅频道一(channel1)

#打开客户端
/usr/local/bin/redis-cli
#订阅频道
SUBSCRIBE channel1

打开另一个客户端,给channel1发布消息hello

#打开客户端
/usr/local/bin/redis-cli
#给channel1发布消息hello
PUBLISH channel1 hello

5. Redis 管道

5.1. 背景

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。一个请求会遵循以下步骤:

1 客户端向服务端发送命令分四步(发送命令-命令排队-命令执行-返回结果),并监听Socket返回,通常以阻塞模式等待服务端响应。

2 服务端处理命令,并将结果返回给客户端。

上述两步称为:Round Trip Time(简称RTT,数据包往返于两端的时间),问题笔记最下方

如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁调用系统IO,发送网络请求,同时需要redis调用多次read()和write()系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好

5.2. 管道概述

Redis管道(Pipeline)是一种批量执行Redis命令的机制。

简单讲:Pipeline是为了解决RTT往返回时,仅仅是将命令打包一次性发送,对整个Redis的执行不造成其它任何影响,也就是,批处理命令变种优化措施,类似Redis的原生批命令(mget和mset)

  1. 特性

Redis管道特性适用于需要高性能、高并发、批量操作的场景,可以有效地提升Redis的操作效率和系统的整体性能。但管道操作是无序的,返回结果的顺序可能与命令的发送顺序不一致,因此在使用管道时需要根据实际情况进行结果的解析和处理。

由于管道是无序的,所以返回结果的顺序可能与命令的发送顺序不一致。在使用管道时,客户端需要根据实际情况进行结果的解析和处理。

5.3. 案例讲解

在/myredis目录下新建一个cmd.txt文件,里面写好要执行的命令

5.4. 管道小结

  • Pipeline与原生批量命令对比

原生批量命令是原子性(例如:mset, mget),pipeline是非原子性;
原生批量命令一次只能执行一种命令(没办法执行不同类型的命令),pipeline支持批量执行不同命令;

原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成;

  • Pipeline与事务对比

事务具有原子性,管道不具有原子性;
管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到exec命令后才会执行,管道不会;

执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会;

  • 使用Pipeline注意事项

pipeline缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令;

使用pipeline组装的命令个数不能太多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存;

5.5. 面试题 - 管道和事务的区别

  • 1.事务具有原子性,管道不具有原子性
    1. 管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到exec命令后才会执行,管道不会
    1. 执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会
  • 4.pipeline其实是一个客户端行为(批量发命令而已),对于redis服务端是不感知的。事务是服务端支持的功能,具有原子性。

6. 主从复制

6.1. 是什么

主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主

6.2. 能干嘛

  • 读写分离,性能扩展
  • 容灾快速恢复

6.2.1. 具体操作

  1. 启动三台 redis
  2. 查看三台主机运行情况

指令 : info replication
打印主从复制的相关信息

  1. 配从(库)不配主(库)

指令: slaveof ip port
成为某个实例的从服务器

注意 ; 在配置文件上写则为永久生效 , 在命令行上写则当前启动生效

6.2.2. 细节

  1. 权限细节很重要

master如果配置了requirepass参数,需要密码登陆

那么slave就要配置masterauth来设置校验密码,否则的话master会拒绝slave的访问请求

可以查看复制节点的主从关系和配置信息

    • replicaof主库IP主库端口

一般写入进redis.conf配置文件内

    • slaveof主库IP主库端口
  1. 每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件
    在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步,重新拜码头
    • slaveof no one

使当前数据库停止与其他数据库的同步,转成主数据库,自立为王

6.2.3. 案例

  1. 在6380和6381上执行: slaveof 127.0.0.1 6379
  2. 在主机上写,在从机上可以读取数据
    在从机上写数据报错
  3. 主机挂掉,重启就行,一切如初
  4. 从机重启需重设:slaveof 127.0.0.1 6379
    可以将配置增加到文件中。永久生效。

6.3. 场景分析

6.3.1.1. 一主二仆

切入点问题?slave1、slave2是从头开始复制还是从切入点开始复制?比如从k4进来,那之前的k1,k2,k3是否也可以复制?

从机是否可以写?set可否?

主机shutdown后情况如何?从机是上位还是原地待命?

主机又回来了后,主机新增记录,从机还能否顺利复制?

其中一台从机down后情况如何?依照原有它能跟上大部队吗?

6.3.1.2. 薪火相传

上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。

用 slaveof ip port
中途变更转向:会清除之前的数据,重新建立拷贝最新的
风险是一旦某个slave宕机,后面的slave都没法备份
主机挂了,从机还是从机,无法写数据了


6.3.1.3. 反客为主

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。

用 slaveof no one 将从机变为主机。

6.4. 复制原理

6.4.1. 流程

  1. slave启动,同步初请

slave启动成功连接到master后会发送一个sync命令。

slave首次全新连接master,一次完全同步(全量复制)将被自动执行,slave自身原来有的数据就会被全覆盖掉。

  1. 首次连接,全量复制
    • master节点收到sync命令后会开始在后台保存快照(即RDB持久化,主从复制时会触发RDB),同时收集所有接收郅的用于修改数据集命令缓存起来,master节点执行RDB持久化完后,master将rdb快照文件和所有缓存的命令发送到所有slave,以完成一次完全同步
    • 而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化
  1. 心跳持续,保持通信

repl-ping-replica-period 10

master发出PING包的周期,默认是10秒

  1. 进入平稳,增量复制

Master继续将新的所有收集到的修改命令自动依次传给slave,完成同步

  1. 从机下线,重连续传

master会检查backlog里面的offset,master和slave都会保存一个复制的offset还有一个masterId,offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给Slave,类似断点续传

6.4.2. 总结

主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制

主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,可以把一部分从服务器升级为「经理角色」,让它也有自己的从服务器,通过这样可以分摊主服务器的压力。

第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。

如果遇到网络断开,增量复制就可以上场了,不过这个还跟 repl_backlog_size 这个大小有关系。

如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率

6.4.3. 复制的缺点

  • 复制延时,信号衰减

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

  • master也会挂掉

默认情况下,不会在slave节点中自动重选一个master

难不成每一次都要人工干预?------------>无人值守安装变成刚需(哨兵)从剩下的从节点中选出来一个当master

6.4.4. 哨兵模式(sentinel)

d

7. 哨兵

7.1. 概述

吹哨人巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外服务。哨兵起步3个。

7.1.1. 作用

  • 监控redis运行状态,r包括master和slave
  • 当master down机,能自动将slave切换成新master

  1. 能做的
  • 主从监控

监控主从redis库运行是否正常

  • 消息通知

哨兵可以将故障转移的结果发送给客户端

  • 故障转移

如果Master异常,则会进行主从切换,将其中一个slave作为新Master

  • 配置中心

客户端通过连接哨兵来获得当前Redis服务的主节点地址

7.2. * 实例

Redis Sentinel架构,前提说明

  • 3个哨兵自动监控和维护集群,不存放数据,只是吹哨人
  • 1主2从 ,用于数据读取和存放

3个哨兵直接在master的机器上做,启动哨兵服务就是读取哨兵的配置文件,复制三份使用不同的端口。

[root@localhost myredis]# cp -r /opt/redis-7.0.0/sentinel.conf .

配置文件中两个重要的选项

  • sentinel monitor

设置要监控的master服务器

quorum表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数。

我们知道,网络是不可靠的,有时候一个sentinel会因为网络堵塞而误以为一个master redis已经死掉了,在sentinel集群环境下需要多个sentinel互相沟通来确认某个master是否真的死了,quorum这个参数是进行客观下线的一个依据,意思是至少有quorum个sentinel认为这个master有故障,才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因,导致无法连接master,而此时master并没有出现故障,所以,这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。

  • sentinel auth-pass

master设置了密码,连接master服务的密码

  • sentinel down-after-milliseconds :

指定多少毫秒之后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线

  • sentinel parallel-syncs :
    表示允许并行同步的slave个数,当Master挂了后,哨兵会选出新的Master,此时,剩余的slave会向新的master发起同步数据
  • sentinel failover-timeout :
    故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败
  • sentinel notification-script :
    配置当某一事件发生时所需要执行的脚本
  • sentinel client-reconfig-script :

客户端重新配置主节点参数脚本

7.2.1.1. 1.1、哨兵配置文件配置

现在先把主从复制的master和两个slave启动起来,测试一下能否正常同步

现在启动我们的那三个哨兵

redis-sentinel sentinel26379.conf --sentinel
redis-sentinel sentinei26380.conf --sentinel
redis-sentinel sentinel26381.conf --sentinel

三个哨兵已经启动

我们查看一下哨兵的日志文件,它所监控的master的信息以及master下面的两个slave的信息都在这里,还有另外两个哨兵也在这里。

注意:日志中有一个Sentinel new configuration saved on disk这句话表示哨兵会自动给配置文件添加内容,里面包含了我所监控的master信息和master下面的从机有哪些,以及另外两个哨兵是谁

现在让master挂掉

关心的问题

  • 两台从机数据是否OK

两台从机数据在master挂掉的一点时间内,服务会断开,数据读不出来。那是因为哨兵正在进行判断master没了,然后法定票数选举,重新发送心跳包建立连接。过一小会再读取两台从机的数据就可以读出来,之前断开的连接都不需要你自己手动连接的就可以恢复。

  • 是否会从剩下的2台机器上选出新的master

master让给了6380这个从节点了,但我的另一个6381并没有连接上新master

  • 之前down机的master机器重启回来,
    谁将会是新老大?会不会双master冲突?

即使master回来了,master的位置也不是它了,换成6380了,它6379只能跟着6380混了。

vim sentinel26379.log  查看日志

6379的身份变成slave了

6380下面跟着6379和6381

现在查看一下6379的配置文件,到最底部,发现最下面的内容是自动生成的,这里配置了新的maaster的ip和端口号

然后去看一下6380的配置文件,发现以前我们配置的以6379为master的那一行选项被自动删除了

replicaof 192.168.111.19 6379   这个被自动删除了,自然它自己就变成master了

下面又自动新加的内容

7.2.1.1.1. 结论
  • 文件的内容,在运行期间会被sentinel动态进行更改
  • Master-Slave切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即master_ redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换
  • 生产都是不同机房不同服务器,很少出现3个哨兵全挂掉的情况
  • 可以同时监控多个master,一行一个

7.3. 工作原理

哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。从“哨兵”这个名字也可以看得出来,它相当于是“观察者节点”,观察的对象是主从节点。

当然,它不仅仅是观察那么简单,在它观察到有异常的状况下,会做出一些“动作”,来修复异常状态。

哨兵节点主要负责三件事情:监控、选主、通知

所以,我们重点要学习这三件事情:

  • 哨兵节点是如何监控节点的?又是如何判断主节点是否真的故障了?
  • 根据什么规则选择一个从节点切换为主节点?
  • 怎么把新主节点的相关信息通知给从节点和客户端呢?

7.3.1. 主观下线

当一个主从配置中的master失效之后,sentinel可以选举出一个新的master,用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换

  • SDown主观下线(Subjectively Down)

SDOWN(主观不可用)是单个sentinel自己主观上检测到的关于master的状态,从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,那么这个Sentinel会主观的(单方面的)认为这个master不可以用了。

sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度。

sentinel down-after-milliseconds <masterName> <timeout>  默认30秒

表示master被当前sentinel实例认定为失效的间隔时间,这个配置其实就是进行主观下线的一个依据,master在多长时间内(默认30秒)一直没有给Sentine返回有效信息,则认定该master主观下线。也就是说如果多久没联系上redis-servevr,认为这个redis-server进入到失效(SDOWN)状态。

7.3.2. 客观下线

  • ODown客观下线(Objectively Down)

ODOWN需要一定数量的sentinel,多个哨兵达成一致意见才能认为一个master客观上已经宕掉。

quorum这个参数是进行客观下线的一个依据,法定人数/法定票数。

意思是至少有quorum个sentinel认为这个master有故障才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因导致无法连接master,而此时master并没有出现故障,所以这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。

  • 选举出领导者哨兵(哨兵中选出兵王)

当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(兵王)并由该领导者节点,也即被选举出的兵王进行failover(故障迁移)

7.3.3. 选取哨兵领导者(兵王)

使用的是Raft算法

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;

Raft算法的基本思路是先到先得:

即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。

7.3.4. 兵王开始推动故障切换流程

1.1. 新主登基

选出新master的规则,剩余slave节点健康前提下

      • redis.conf文件中,优先级slave-priority或者replica-priority最高的从节点(数字越小优先级越高)
      • 复制偏移位置offset最大的从节点
      • 最小Run ID的从节点
        字典顺序,ASCII码

    1. 群臣俯首

被选出来的新master先执行slaveof no one命令成为新的主节点,并通过slaveof命令让其他节点成为其从节点。

Sentinel leader会对选举出的新master执行slaveof no one操作,将其提升为master节点;

Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave;

    1. 旧主拜服

将之前已下线的老master设置为新选出的新master的从节点,当老master重新上线后,它会成为新master的从节点。

Sentinel leader会让原来的master降级为slave并恢复正常工作。

上述的failover操作均由sentinel自己独自完成,完全无需人工干预。

7.3.5. 哨兵使用注意事项

  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用;

  • 哨兵节点的数量应该是奇数;

  • 各个哨兵节点的配置应一致;

  • 如果哨兵节点部署在Docker等容器里面,尤其要注意端口的正确映射;

  • 哨兵集群+主从复制,并不能保证数据零丢失;

7.4. 总结

Redis 在 2.8 版本以后提供的哨兵( Sentinel )机制,它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。

哨兵一般是以集群的方式部署,至少需要 3 个哨兵节点,哨兵集群主要负责三件事情:监控、选主、通知

哨兵节点通过 Redis 的发布者/订阅者机制,哨兵之间可以相互感知,相互连接,然后组成哨兵集群,同时哨兵又通过 INFO 命令,在主节点里获得了所有从节点连接信息,于是就能和从节点建立连接,并进行监控了。

1、第一轮投票:判断主节点下线

当哨兵集群中的某个哨兵判定主节点下线(主观下线)后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。

当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。

2、第二轮投票:选出哨兵 leader

某个哨兵判定主节点客观下线后,该哨兵就会发起投票,告诉其他哨兵,它想成为 leader,想成为 leader 的哨兵节点,要满足两个条件:

  • 第一,拿到半数以上的赞成票;
  • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

3、由哨兵 leader 进行主从故障转移

选举出了哨兵 leader 后,就可以进行主从故障转移的过程了。该操作包含以下四个步骤:

  • 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点,选择的规则:
    • 过滤掉已经离线的从节点;
    • 过滤掉历史网络连接状态不好的从节点;
    • 将剩下的从节点,进行三轮考察:优先级、复制进度、ID 号。在每一轮考察过程中,如果找到了一个胜出的从节点,就将其作为新主节点。
  • 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
  • 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
  • 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;