此笔记是我学习 B站 尚硅谷相关课程的学习笔记

一、课程大纲

image-20220824110521483

二、安装、启动与关闭

安装去网上学。https://blog.51cto.com/niuben/5203089

启动

启动服务:

1
service start redis

启动客户端:

1
redis-cli

多个端口启动:

1
redis-cli -p 6379

测试:ping

image-20220824110903208

查看运行状态

1
systemctl status redis

关闭

单实例关闭客户端:

1
redis-cli shutdown

进入终端关闭

image-20220824111046500

多实例关闭,指定端口关闭:

1
redis-cli -p 6379 shutdown

关闭服务:

1
service stop redis

三、相关知识

端口:6379

  • 默认16个数据库,类似数组下标从0开始,初始默认使用0号库。

  • 使用命令 select <dbid>来切换数据库。如: select 8 统一密码管理,所有库同样密码。

  • dbsize 查看当前数据库的key的数量

  • flushdb 清空当前库

  • flushall 通杀全部库

Redis 是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用

四、常用五大数据类型

常见数据类型操作命令:http://www.redis.cn/commands.html

4.1、Redis 键(key)

  • keys *:查看当前库所有key (匹配:keys *1
  • exists <key>:判断<key>是否存在
  • type <key>:查看<key>的类型
  • del <key>:删除指定的<key>的数据
  • unlink key:根据value选择非阻塞删除。仅将keys冲keyspace元数据中删除,真正的删除会在后续异步操作
  • expire <key> <time>:设置<time>秒后<key>过期
  • ttl <key>:查看<key>还有多少秒过期,-1表示永不过期,-2表示已过期

  • select <n>:切换到n号数据库
  • dbsize:查看当前数据库的key的数量
  • flushdb:清空当前数据库
  • flushall:清空全部数据库

4.2、Redis字符串(String)

4.2.1、简介

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

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

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

4.2.2、常用命令

增:setsetnxsetexappendmsetsetrange

改:getsetincrdecrincrbydecrbysetsetexsetrange

查:getstrlenmgetgetrange

  • set <key> <value>:添加键值对

    image-20220824162552062

    • NX:当数据库中key不存在时,可以将key-value添加到数据库

    • XX:当数据库中key存在时,可以将key-value添加到数据库,与NX参数互斥

      当key存在时,再设置这个key,相当于是修改这个key对应的键值

    • EX:key的超时秒数

    • PX:key的超时毫秒数,与EX互斥

  • get <key>:查询<key>对应键值

  • append <key> <value>:将给定的<value>追加到原值的末尾,返回值是追加后字符串的长度

    image-20220824163129841

  • strlen <key>:获取<key>对应的键值的长度

    image-20220824163344231

  • setnx <key> <value>:只有 key 不存在时,才能设置key的值。相当于set命令带了NX参数

    image-20220824163600073


对数字类型的操作(如果value中没有字母只有数字就是数字类型):

  • incr <key>:将 <key> 中存储的数字类型的值增1,只能对数字类型的值操作,如果为空,新增值为1

    image-20220824164021139

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

  • incrby/decrby <key> <step>:给<key>的数字值增加/减少step

    image-20220824164344942

redis的操作是原子性的

所谓原子操作是指不会被线程调度机制打断的操作

这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

  • 在单线程中, 能够在单条指令中完成的操作都可以认为是”原子操作”,因为中断只能发生于指令之间。
  • 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。

Redis单命令的原子性主要得益于Redis的单线程。


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

  • mget <key1> <key2> <key3>...:同时获取一个或多个key对应的value值

  • msetnx <key1> <value1> <key2> <value2>...:同时设置一个或多个key-value对,当且仅当所有指定的key都不存在是才能添加成功。由于原子性,有一个失败则都失败

  • getrange <key> <起始位置> <结束位置>:获取值的范围,类似于java中的substring方法,左右都是闭区间(索引从0开始

    image-20220824170133260

  • setrange <key> <起始位置> <value>:用value覆写<key>所存储的字符串值,从<起始位置>开始(索引从1开始,将该位置之后的值都替换为<value>

    image-20220824170441493

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

    image-20220824170851446

  • getset <key> <value>:以新换旧,设置了新值同时获得(返回)旧值

    image-20220824170659272

4.2.3、数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

image-20220824165923342

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。

  • 当字符串长度小于1M时,扩容都是加倍现有的空间
  • 如果超过1M,扩容时一次只会多扩1M的空间。

需要注意的是字符串最大长度为512M。

4.3、Redis 列表(List)

4.3.1、简介

单键多值

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

image-20220824171151681

4.3.2、常用命令

增:lpushrpushlinsertrpoplpush

删:lpoprpoplrem

改:lset

查:lrangelindexllen

  • lpush/rpush <key> <value1> <value2> ...:从 左边 / 右边 插入一个或多个值

    解释:起始对于单边来所,和栈差不多。但是是双边的,所以需要指定左或右。

    image-20220824171526999

    注意点:

    • lpush 使得值都是从左边插入,也就是后面的值显示在前面,比如添加v1 v2 v3。但是查出来的结果是v3 v2 v1,顺序是反的
    • 查询列表不能使用字符串的查询方式(get
    • 查询列表使用lrang命令(后几个命令中有)
  • lpop/rpop <key>:从 左边 / 右边 pop出一个值。值在键在,值光键亡

    image-20220824172207743

  • rpoplpush <key1> <key2>:从<key1>列表右边吐出一个值,插入到<key2>列表右边

    image-20220824172436971

    注意没有lpoplpush等其他组合了,只有这一个


  • lrange <key> <startIndex> <stopIndex>:按照索引,从左往右获得列表中的元素

    索引和Python类似,有两套索引机制,正数是左往右,负数是倒数几个,一般获取所有值:

    lrange <key> 0 -1:0 —— -1就是所有值

    注意,没有rrange。不掩饰了,很常用,前面也用到了的。

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

    image-20220824173306113

    没有rindex

  • llen <key>:获得<key>对应列表的长度

    image-20220824173431479

    没有rlen


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

    image-20220824173948479

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

    image-20220824174355558

    如果没有n个就是有多少个删除多少个,不会报错。

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

    image-20220824174636024

4.3.3、数据结构

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

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist ,即压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。

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

image-20220824175213730

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

4.4、Redis 集合(Set)

4.4.1、简介

Redis 的 Set 对外提供的功能与 List 类似是一个列表的功能,特殊之处在于Set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择,并且set提供了判断某个成员是否在一个Set集合内的重要接口,这个也是List所不能提供的。

Redis 的 Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)

一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

4.4.2、常用命令

  • sadd <key> <value1> <value2> ...:将一个或多个 member 元素加入到集合key中,已经存在的 member 元素将被忽略,不会报错。

    image-20220824180622340

  • smembers <key>:取出 <key> 对应集合中的所有值

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

    image-20220824181058159

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

    image-20220824181203313

  • srem <key> <value1> <value2>...:删除<key>对应的集合中的一个或多个元素

  • spop <key>随机<key>对应的集合中 pop 出一个值

  • srandmember <key> <n>随机<key>对应的集合中取出 n 个值不会从集合中删除

  • smove <sourcekey> <destinationkey> <value>:把<sourcekey>对应的集合中的<value>移动到<destinationkey>对应的集合中

  • sinter <key1> <key2>:返回<key1><key2>对应的集合的交集元素

  • sunion <key1> <key2>:返回<key1><key2>对应的集合的并集元素

  • sdiff <key1> <key2>:返回<key1><key2>对应的集合的差集元素(包含key1中的,不包含key2中的,也就是key1 - key2)

4.4.3、数据结构

Set数据结构是dict字典,字典是用哈希表实现的。

Java中 HashSet 的内部实现使用的是 HashMap ,只不过所有的 value 都指向同一个对象。Redis 的 Set 结构也是一样,它的内部也使用 Hash 结构,所有的value都指向同一个内部值。

4.5、Redis 哈希(Hash)

4.5.1、简介

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

  • Redis hash 是一个string类型的 field 和 value 的映射表,hash 特别适合用于存储对象

  • 类似Java里面的Map

举例:

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储

主要有以下3种存储方式:

  1. image-20220824190207212

    每次修改用户的某个属性需要,先反序列化改好后再序列化回去。开销较大。

  2. image-20220824190219774

    用户ID数据冗余

  3. image-20220824190318307

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

4.5.2、常用命令

  • hset <key> <field> <value>:给<key>集合中的 <field>键赋值<value> (h set)

    image-20220824191722626

  • hget <key1> <field>:从<key1>集合<field>取出 value (h get)

    image-20220824191803414

  • hmset <key1> <field1> <value1> <field2> <value2>...:批量设置hash的值(h mset)

    image-20220824191852857

  • hexists <key1> <field>:查看哈希表 <key1> 中,给定属性 <field> 是否存在。 (h exists)

    image-20220824191951793

  • hkeys <key>:列出<key>对应的 hash集合 的所有 属性 field

    image-20220824192028668

  • hvals <key>:列出该hash集合的所有 属性对应的 值 value

    image-20220824192107456

  • hincrby <key> <field> <increment>:为哈希表 <key> 中的域 <field> 的值加上增量 <increment>(h incrby)

    image-20220824192202144

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

4.5.3、数据结构

Hash类型对应的数据结构是两种:

  • ziplist(压缩列表)
  • hashtable(哈希表)。

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

4.6、Redis 有序集合Zset (sorted set)

4.6.1、简介

Redis 有序集合 Zset 与 普通集合 Set 非常相似,是一个没有重复元素的字符串集合。

不同之处是 有序集合每个成员 都关联了一个 评分(score),这个评分(score)被用来 按照从最低分到最高分的方式 排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。==每个成员有一个评分,利用评分来排序==

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表

4.6.2、常用命令

  • zadd <key> <score1> <value1> <score2> <value2>…:将一个或多个 member 元素及其 score 值加入到有序集 key 当中。==注意,是key-socre-value对,不是key-value对了。==

    image-20220824193319810

  • zrange <key> <startIndex> <stopIndex> [WITHSCORES]:返回有序集 key 中,下标索引在<startIndex><stopIndex>之间的元素(带[WITHSCORES],可以让 分数score 一起和 值 返回到结果集。)

    image-20220824193454710

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

    image-20220824193820954

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

    image-20220824193937548

    注意:<max><min>

  • zincrby <key> <increment> <value>:为<key>对应的有序集合中<value>值对应的元素的score加上增量<increment> ==操作的是score==

    image-20220824194212768

  • zrem <key> <value>:删除该集合下,指定值的元素(<key>对应的有序集合中<value>元素)

  • zcount <key> <min> <max>:统计该集合,分数score 在区间内的 元素 个数==利用分数的区间统计元素个数==

    image-20220824194332862

  • zrank <key> <value>返回该<value>在有序集合<key>中的排名,==从 0 开始==。

    image-20220824194419193

4.6.3、数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

  1. hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

    hash的filed就是value,hash的value就是权重score。

  2. 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

4.6.4、跳跃表

  1. 简介

    应用场景:有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。

    对于有序集合的底层实现,可以用数组、平衡树、链表等。

    数组不便元素的插入、删除;

    平衡树或红黑树虽然效率高但结构复杂;

    链表查询需要遍历所有效率低。

    Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。

  2. 实例

    对比有序链表和跳跃表,从链表中查询出51

    • 有序链表:

      image-20220824194843636

      要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

    • 跳跃表:

      image-20220824194913189

      • 从第2层开始,1节点比51节点小,向后比较。21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层

      • 在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下

      • 在第0层,51节点为要查找的节点,节点被找到,共查找4次

      从此可以看出跳跃表比有序链表效率要高

五、Redis 的发布和订阅

5.1、什么是发布和订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

Redis 客户端可以订阅任意数量的频道。

5.2 Redis 的发布和订阅

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

    image-20220824200846002

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

    image-20220824200906950

5.3、发布和订阅命令行实现

  1. 打开一个客户端订阅 channel1 频道

    1
    subscribe channel1
  2. 打开另一个客户端,给 channel1 频道发送消息 hello

    1
    publish channel1 hello

    返回值是频道 channel1的订阅者数量

  3. 打开第一个客户端可以看到发送的消息

    注:发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息

六、Reidis 新数据类型

6.1、Bitmaps

6.1.1、简介

实际类型还是string,但是不能正常用string的命令(但是不会报错)

现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图

image-20220824204804442

合理地使用操作位能够有效地提高内存使用率和开发效率。

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

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

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

image-20220824204941917

6.1.2、常用命令

6.1.2.1、setbit

  1. 语法;

    1
    setbit <key> <offset> <value>

    设置<key>对应的Bitmaps中某个偏移量(<offset>)的值(<value>)(0 或 1)

    offset 偏移量从0开始

  2. 案例

    每个 独立用户是否访问过网站 存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。

    设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图

    image-20220824205450840

    实际操作:

    unique:users:20201106代表2020-11-06这天的独立访问用户的Bitmaps

    image-20220824205559494

    很多应用的用户id以一个指定数字(例如10000) 开头(有公共前缀), 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字,即去掉公共前缀

    在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。

6.1.2.2、getbit

  1. 语法

    1
    getbit <key> <offset>

    获取<key>对应的Bitmaps中的某个偏移量(<offset>)的值

  2. 案例:

    image-20220824210430544

6.1.2.3、bitcount

统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标索引数,二者皆包含。

==这里的单位是byte,1 byte = 8 bit==

  1. 语法

    1
    bitcount <key> [start end]

    统计字符串从 start字节 到 end字节 比特值为 1 的位的数量 ==一个字节是8位==

  2. 案例

    计算2022-11-06这天全天的独立访问用户数量

    image-20220824211250216

    start和end代表起始和结束字节数,下面操作计算用户 id 在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。==相当于是区间:[9,24]==

    image-20220824211512224

    结果数为3,对应的是11,15,19

    举例: K1 【01000001 01000000 00000000 00100001】,对应【0,1,2,3】

    bitcount K1 1 2 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000

    —》bitcount K1 1 2   —》1

    bitcount K1 1 3 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000 00100001

    —》bitcount K1 1 3  —》3

    bitcount K1 0 -2 : 统计下标0到下标倒数第2,字节组中bit=1的个数,即01000001 01000000 00000000

    —》bitcount K1 0 -2  —》3

6.1.2.4、bitop

  1. 语法

    1
    bitop <operation> <destkey> [key1 key2 ...]

    bitop是一个复合操作。<operation>的取值为:and(交集)、or(并集)、not(非)、xor(异或)。将操作结果保存在<destkey>中。被操作的对象就是[key1,key2...]所对应的Bitmaps。

  2. 案例

    • 先设置值:

      2020-11-04 这天访问网站的userid=1,2,5,9。

      1
      2
      3
      4
      setbit unique:users:20201104 1 1
      setbit unique:users:20201104 2 1
      setbit unique:users:20201104 5 1
      setbit unique:users:20201104 9 1

      2020-11-03 这天访问网站的userid=0,1,4,9。

      1
      2
      3
      4
      setbit unique:users:20201103 0 1
      setbit unique:users:20201103 1 1
      setbit unique:users:20201103 4 1
      setbit unique:users:20201103 9 1
    • 需求:

      计算出两天都访问过网站的用户数量

    • 实现

      1
      bitop and unique:users:and:20201104_03 unique:users:20201103 unique:users:20201104

      意思是,将unique:users:20201103对应的的Bitmaps与unique:users:20201104对应的Bitmaps进行and(与操作),将操作结果存到新的key:unique:users:and:20201104_03中,这个key对应的value就是前面两者进行与操作的结果。

      1
      bitcount unique:users:and:20201104_03

      最后再对其计数。

      对于不同需求使用不同操作就可以了,这里只是演示一种操作。

6.1.3、Bitmaps 与 set 对比

对比基础条件:

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

  • set和Bitmaps存储一天活跃用户对比:

    | 数据 类型 | 每个用户id占用空间 | 需要存储的用户量 | 全部内存量 |
    | ————— | ————————— | ———————— | ——————————— |
    | Set | 64位 | 50000000 | 64位50000000 = 400MB |
    | Bitmaps | 1位 | 100000000 | 1位
    100000000 = 12.5MB |

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

  • set和Bitmaps存储独立用户空间对比

    | 数据类型 | 一天 | 一个月 | 一年 |
    | ———— | ——— | ——— | ——- |
    | Set | 400MB | 12GB | 144GB |
    | Bitmaps | 12.5MB | 375MB | 4.5GB |

    但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(有大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。

  • set和Bitmaps存储一天活跃用户对比(独立用户比较少)

    | 数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
    | ———— | ————————— | ———————— | ——————————— |
    | Set | 64位 | 100000 | 64位100000 = 800KB |
    | Bitmaps | 1位 | 100000000 | 1位
    100000000 = 12.5MB |

6.2、HyperLogLog

6.2.1、简介

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的 incrincrby轻松实现。

但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题

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

  1. 数据存储在MySQL表中,使用distinct count计算不重复个数

  2. 使用Redis提供的hash、set、bitmaps等数据结构来处理

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

能否能够降低一定的精度来平衡存储空间?

Redis推出了HyperLogLog

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

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

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

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

6.2.2、常用命令

6.2.2.1、pfadd

  1. 语法

    1
    pfadd <key> <element> [element ...]

    添加指定元素(一次可以添加多个元素)到HyperLogLog中

  2. 案例

    将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。

    image-20220824214739257

6.2.2.2、pfcount

  1. 语法

    1
    pfcount <key> [key ...]

    计算<key>的近似基数,可以多个<key>合并起来然后求基数。例如hll1存储周一的UV,hll2存储周二的UV….。那么求一周的近似基数就是把hll1,hll2….hll7合并起来 然后 求基数

  2. 案例

    计算hll1与hll2合并之后的的基数

    image-20220824215157445

6.2.2.3、pfmerge

  1. 语法

    1
    pfmerge <destkey> <sourcekey> [sourcekey ...]

    将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

  2. 案例

    将hll1与hll2合并,将结果存储到hll3中,然后求hll3的基数

    image-20220824220034112

6.3、Geospatial

6.3.1、简介

Redis 3.2 中增加了对GEO类型的支持。

GEO,Geographic,地理信息的缩写。

该类型,就是元素的2维坐标,在地图上就是经纬度。

redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

6.3.2、常用命令

6.3.2.1、geoadd

  1. 语法

    1
    geoadd <key> <longitude> <latitude> <member> [longitude latitude member ...]

    添加地理位置(经度<longitude>,纬度<latitude>,名称<member>)(一次可以添加多组)

  2. 案例

    添加上海,重庆,深圳,北京的经纬度:

    image-20220824220726865

    两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。

    • 有效的经度从 -180 度到 180 度。

    • 有效的纬度从 -85.05112878 度到 85.05112878 度。

      当坐标位置超出指定范围时,该命令将会返回一个错误。

      已经添加的数据,是无法再次往里面添加的。

6.3.2.2、geopos

  1. 语法

    1
    geopos <key> <member> [member ...]

    获取<key>对应的geo中的指定地区(member)的坐标值(一次可获取多个)

  2. 案例

    获取上海,重庆,深圳,北京的经纬度:

    image-20220824220957511

6.3.2.3、geodist

  1. 语法

    1
    geodist <key> <member1> <member2> [m|km|ft|mi]

    获取两个位置之间(需要两个位置元素)的直线距离。

    单位:

    • m 表示单位为米[默认值]

    • km 表示单位为千米。

    • mi 表示单位为英里。

    • ft 表示单位为英尺。

  2. 案例

    获取上海和北京之间的距离,使用千米作为单位

    image-20220824221252456

6.3.2.4、georadius

  1. 语法

    1
    georadius <key> <longitude> <latitude> radius m|km|ft|mi

    以给定的经度(<longitude>)和纬度(<latitude>)为中心,找出某一半径(radius)内的元素。需要指定单位。单位和上面一样。

  2. 案例

    查找以经度为110,纬度30为中心,半径为1000 km的元素

    image-20220824221705337

七、Jedis

和JDBC类似,是使用java操作Redis的工具

7.1、引入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.3</version>
</dependency>

7.2、连接Redis注意事项

  • 禁用Linux的防火墙

  • redis.conf中注释调bind 127.0.0.1

    image-20220825104626995

    69行左右的位置

    image-20220825104733766

  • redis.conf中设置protected-mode 为no

    image-20220825104529894

    88行左右的位置

    image-20220825104600113

设置好了之后可以用xshell试试是否真的可以连接。

7.3、Jedis测试

创建JedisTest01类

写入代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import redis.clients.jedis.Jedis;

public class JedisTest01 {

public static void main(String[] args) {
// 1. 创建Jedis对象
Jedis jedis = new Jedis("192.168.10.104",6379); // 参数一:host,参数二:port
// 2. 测试
String result = jedis.ping();
System.out.println(result);
jedis.close();
}

}

测试能否ping通

正确运行结果:

image-20220825110113644

SLF4J提示的东西可以不用管,输出了PONG就是对的。

7.4、测试相关数据类型

7.4.1、Key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import redis.clients.jedis.Jedis;

import java.util.Set;

public class JedisTestKey {

public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.10.104",6379);
jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
Set<String> keys = jedis.keys("*");
System.out.println(keys.size());
for (String key : keys) {
System.out.println(key);
}
System.out.println(jedis.exists("k1"));
System.out.println(jedis.ttl("k1"));
System.out.println(jedis.get("k1"));
jedis.close();
}

}

运行效果:

image-20220825110638605

7.4.2、String

1
2
3
4
5
6
7
8
9
10
11
12
import redis.clients.jedis.Jedis;

public class JedisTestString {

public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.10.104",6379);
jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));
jedis.close();
}

}

运行效果:

image-20220825111113377

7.4.3、List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import redis.clients.jedis.Jedis;

import java.util.List;

public class JedisTestList {

public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.10.104",6379);
jedis.lpush("myList", "List1","List2","List3");
List<String> list = jedis.lrange("myList",0,-1);
for (String element : list) {
System.out.println(element);
}
jedis.close();
}

}

运行效果:

image-20220825111354720

7.4.4、Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import redis.clients.jedis.Jedis;

import java.util.Set;

public class JedisTestSet {

public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.10.104", 6379);
jedis.sadd("orders", "order01");
jedis.sadd("orders", "order02");
jedis.sadd("orders", "order03","order04");
Set<String> smembers = jedis.smembers("orders");
for (String order : smembers) {
System.out.println(order);
}
System.out.println("-------------------------");
jedis.srem("orders", "order02");
smembers = jedis.smembers("orders");
for (String order : smembers) {
System.out.println(order);
}
jedis.close();
}

}

运行效果:

image-20220825111742469

Set无序

7.4.5 Hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JedisTestHash {

public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.10.104", 6379);

jedis.hset("hash1","userName","lisi");
System.out.println(jedis.hget("hash1","userName"));

System.out.println("-----------------------");

Map<String,String> map = new HashMap<String,String>();
map.put("telPhone","13810169999");
map.put("address","chongqing");
map.put("email","abc@163.com");
jedis.hmset("hash2",map);
List<String> result = jedis.hmget("hash2", "telPhone","email");
for (String element : result) {
System.out.println(element);
}
jedis.close();
}

}

运行效果:

image-20220825112309330

7.4.6 Zset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import redis.clients.jedis.Jedis;

import java.util.List;

public class JedisTestZset {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.10.104", 6379);
jedis.zadd("zset01", 100d, "z3");
jedis.zadd("zset01", 90d, "l4");
jedis.zadd("zset01", 80d, "w5");
jedis.zadd("zset01", 70d, "z6");

List<String> zrange = jedis.zrange("zset01", 0, -1);
for (String e : zrange) {
System.out.println(e);
}
jedis.close();
}
}

运行效果:

image-20220825112659378

7.4.7 总结

最终运行结果:

image-20220825112811478

Linux下可以查到keys 的变化,所以以后要使用Jedis,需要Linux是打开状态。

jedis可以调用常用方法,包括Bitmaps等新数据类型的命令都能调用。

八、Jedis案例

8.1、需求

模拟验证码发送:

  1. 输入手机号,点击发送后随机生成6位数字码,2分钟有效

    • 随机生成6位数字码:利用Random
    • 2分钟有效:Redis设置key的有效时间
  2. 输入验证码,点击验证,返回成功或失败

    • 从Redis中获取验证码,和输入的验证码作比较
  3. 每个手机号每天只能输入3次

    • 用 incr 命令

image-20220825113358925

8.2、实现

小案例,就不写页面了,主要写Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import redis.clients.jedis.Jedis;

import java.util.Random;
import java.util.Scanner;

public class JedisTestVerificationCode {
public static void main(String[] args) {
// 模拟验证码发送:
Scanner scanner = new Scanner(System.in);
while(true){
System.out.print("请输入手机号: ");
String phoneNum = scanner.next();
String result = verifyCode(phoneNum);
System.out.println(result);
System.out.print("请输入验证码: ");
String inputCode = scanner.next();
checkCode(phoneNum,inputCode);
System.out.println("----------------");
}
}

/**
* 生成验证码数字
* @return 生成的验证码
*/
public static String getVerificationCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
for(int i = 0 ; i < 6 ; i++){
code.append(random.nextInt(10));
}
return code.toString();
}

/**
* 初始化验证码(产生真正验证码),需要判断24小时内是否产生了三次验证码,也需要设置验证码过期时间是120秒
* @param phoneNUm 手机号
* @return 真正生成的验证码
*/
public static String verifyCode(String phoneNUm){
// 连接Jedis
Jedis jedis = new Jedis("192.168.10.104", 6379);

// 拼接key
// 手机发送次数 key
String countKey = phoneNUm + ":count";
// 验证码的key
String codeKey = phoneNUm + ":code";

// 每个手机每天只能发送三次验证码
String count = jedis.get(codeKey);
if(count == null){
// 还没有发送过一次
// 存入key-value
jedis.setex(countKey, 24*60*60, "1");
} else if(Integer.parseInt(count) <= 2){
// 发送过,但没有达到三次
if(jedis.exists(codeKey)){
return "目前还有有效的验证码";
}
// 发送次数加一
jedis.incr(countKey);
} else {
// 发送超过三次
jedis.close();
return "今天发送次数已经超过三次了";
}

// 生成并获取验证码,将验证码放到redis中,设置存在时间为120秒
String verificationCode = getVerificationCode();
jedis.setex(codeKey, 120, verificationCode);
jedis.close();
return "产生的验证码为: " + verificationCode;
}

/**
* 验证输入验证码是否正确
* @param phoneNum 手机号
* @param inputCode 用户输入的验证码
*/
public static void checkCode(String phoneNum,String inputCode) {
// 连接Jedis
Jedis jedis = new Jedis("192.168.10.104", 6379);
// 拼接该手机号对应的验证码的key
String codeKey = phoneNum + ":code";
// 获取Redis中保存的手机号
String code = jedis.get(codeKey);
// 比较验证码是否一致
if(code.equals(inputCode)) {
// 验证码输入正确
System.out.println("验证码输入正确");
} else {
// 验证码输入错误
System.out.println("验证码输入错误");
}
}
}

九、Redis与SpringBoot整合

9.1、引入依赖

使用脚手架:

image-20220825154908544

产生的默认依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

还需要添加;

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>

9.2、相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
redis:
# Redis 服务器地址
host: 192.168.10.104
# Redis 端口位置
port: 6379
# 数据库索引(默认为0)
database: 0
# 连接超时时间,单位毫秒
timeout: 1800000
# 配置连接池
lettuce:
pool:
# 连接池最大连接数 (使用负值表示没有影响)
max-active: 8
# 最大阻塞等待时间(负数表示没有限制)单位毫秒
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 5
# 连接池终端最小空闲连接
min-idle: 0

9.3、配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();

Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);

template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);

// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}

9.4、测试

编写一个RedisTestController方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {

@Autowired
private RedisTemplate redisTemplate;

@GetMapping
public String testRedis(){
// 设置值到redis
redisTemplate.opsForValue().set("k1","v1");
// 获取值
return redisTemplate.opsForValue().get("k1").toString();
}

}

image-20220825171423590

十、事务、锁机制

10.1、Redis的事务定义

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

Redis事务的主要作用就是串联多个命令防止别的命令插队

10.2、Multi、Exec、discard

  • 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,等待按顺序执行(相当于开启事务)

  • 直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。(相当于提交事务)

  • 组队的过程中可以通过discard来放弃组队。 (相当于事务回滚)

image-20220826105919953

案例:

exec:

image-20220826111113774

discard:

image-20220826111253308

组队阶段出错(如果组队阶段出错,所有命令都不执行):

image-20220826111500581

组队阶段成功,执行阶段出错(除了出错的命令都会成功):

image-20220826111840955

10.4、悲观锁

image-20220826141543634

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

10.5、乐观锁

image-20220826141623532

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

10.5.1、watch

使用watch监视一个key

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

10.5.2、unwatch

取消watch命令对所有key的监视

10.6、Redis事务三特性

  • 单独的隔离操作

    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念

    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

  • 不保证原子性

    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

十一、秒杀案例

看视频吧,我没动手写。

十二、Redis持久化之RDB

12.1、总体介绍

官网介绍:http://www.redis.io

image-20220826154814431

Redis 提供了2个不同形式的持久化方式。

  • RDB(Redis DataBase)

  • AOF(Append Of File)

12.2、RDB(Redis DataBase)

12.2.1、官网介绍

image-20220826154850036

12.2.2、是什么

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

12.2.3、备份是如何执行的

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个==临时文件==中,待持久化过程都结束了,再用这个==临时文件替换上次持久化好的文件==。 整个过程中,主进程是不进行任何IO操作的,这就 确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

12.2.4、Fork

  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux中引入了“写时复制技术

  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

12.2.5、持久化流程

image-20220826155347759

12.2.6、dump.rdb

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

image-20220826155622341

12.2.7、配置位置

rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下

image-20220826155744603

12.2.8、触发RDB快照;保持策略

12.2.8.1、配置文件中默认的快照配置

image-20220826155850234

解释一个就行了:

save 3600 1:表示在 3600 秒内,有 1 个key发生变化就执行替换操作。执行替换操作之后又继续记录时间和发生变化的key的个数。

12.2.8.2、save 命令 VS bgsave 命令

  • save:save时只管保存,其它不管,全部阻塞。手动保存。不建议
  • bgsaveRedis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

可以通过 lastsave 命令获取最后一次成功执行快照的时间

12.2.8.3、flushall 命令

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

12.2.8.4、save 命令

格式:save 秒钟 写操作次数

RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件,

默认是 1 分钟内改了 1 万次,或5 分钟内改了 10 次,或15 分钟内改了1次

不适用save(禁用):不设置save指令,或者给save传入空字符串

12.2.8.5、stop-writes-on-bgsave-error

image-20220826161400040

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

12.2.8.6、rdbcompression 压缩文件

image-20220826161439478

对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。

如果不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes.

12.2.8.7、rdbchecksum 检查完整性

image-20220826162236514

在存储快照后,还可以让redis使用CRC64算法来进行数据校验,

但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。推荐yes.

12.2.8.8、rdb的备份

  1. 先通过 config get dir 查询 rdb文件的目录

  2. *.rdb 的文件拷贝到别的地方

  3. rdb 的恢复:

    • 关闭Redis
    • 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
    • 启动Redis, 备份数据会直接加载

12.2.9、优势

  • 适合大规模的数据恢复

  • 对数据完整性和一致性要求不高更适合使用

  • 节省磁盘空间

  • 恢复速度快

image-20220826162526604

12.2.10、劣势

  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑

  • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。

  • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

12.2.11、如何停止

动态停止RDB:redis-cli config set save ""

save后给空值,表示禁用保存策略

12.2.12、总结

image-20220826162732185

十三、Redis持久化之AOF

13.1 AOF(Append Only File)

13.1.1 是什么

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

13.1.2、AOF持久化流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内;

  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

image-20220826163405116

13.1.3、AOF默认不开启

可以在redis.conf中配置文件名称,默认为 appendonly.aof

AOF文件的保存路径,同RDB的路径一致。

13.1.4、AOF 和 RDB 同时开启,redis执行哪一个?

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

13.1.5、AOF 启动、修复、恢复

  • AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

  • 正常恢复

    • 修改默认的appendonly no,改为yes

    • 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)

    • 恢复:重启redis然后重新加载

  • 异常恢复

    • 修改默认的appendonly no,改为yes

    • 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof—fix appendonly.aof进行恢复

      1
      redis-check-aof --fix appendonly.aof
    • 备份被写坏的AOF文件

    • 恢复:重启redis,然后重新加载

13.1.6 AOF同步频率设置(持久化策略)

  • appendfsync always

    始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

  • appendfsync everysec

    每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

  • appendfsync no

    redis不主动进行同步,把同步时机交给操作系统。

13.1.7、Rewrite 压缩

image-20220826170710759

  1. 是什么

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

  2. 重写原理,如何实现重写

    AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把 rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。

    • no-appendfsync-on-rewrite:

      • 如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
      • 如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
    • 触发机制,何时重写

      Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发

      重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

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

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

      例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

      系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,

      如果Redis的AOF当前大小 >= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

  3. 重写流程

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

13.1.8、优势

image-20220826170959394

  • 备份机制更稳健,丢失数据概率更低。
  • 可读的日志文本,通过操作AOF稳健,可以处理误操作。

13.1.9、劣势

  • 比起RDB占用更多的磁盘空间。

  • 恢复备份速度要慢。

  • 每次读写都同步的话,有一定的性能压力。

  • 存在个别Bug,造成恢复不能。

13.1.10、总结

image-20220826171116796

十二——十三、用哪个好

官方推荐两个都启用。

如果对数据不敏感,也就是允许部分数据丢失,可以选单独用RDB。

不建议单独用 AOF,因为可能会出现Bug。

如果只是做纯内存缓存,可以都不用。

官网建议:

  • RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储

  • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.

  • Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
  • 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
  • 同时开启两种持久化方式
  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
  • 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

性能建议:

因为RDB文件只用作后备用途,建议只在 Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。

如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。

代价,一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。

只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值 64M太小 了,可以设到 5G 以上。

默认超过原大小100%大小时重写可以改到适当的数值。

十四、主从复制

14.1、是什么

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

14.2、能干嘛

  • 读写分离,性能扩展

  • 容灾快速恢复

image-20220826182302989

14.3、怎么用

看视频,看文档,看笔记

网络下显示:

<iframe src="//player.bilibili.com/player.html?aid=247670776&bvid=BV1Rv41177Af&cid=326382019&page=32" scrolling="no" border="1px" width=100% height=550px frameborder="no" framespacing="0" allowfullscreen="true"></iframe>

14.4、常用3招

14.4.1、一主二仆

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

    不可以

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

    不可写

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

    原地待命

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

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

    恢复之后能跟上之后的数据,down的这段时间的数据就没有了

image-20220826184632972

14.4.2、薪火相传

就像是N叉树一样

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

slaveof <ip> <port>

中途变更转向:会清除之前的数据,重新建立拷贝最新的

风险是一旦某个slave宕机,后面的slave都没法备份

主机挂了,从机还是从机,无法写数据了

14.4.3、反客为主

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

slaveof no one 将从机变为主机。如果不用命令就不会反客为主

但是如果输入命令也需要人力,还不如快速再把主机重启。

自动化的反客为主:14.6、哨兵模式

image-20220826214212693

14.5、复制原理

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

  • Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步

  • 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

  • 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
  • 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

image-20220826214304724

14.6、哨兵模式

14.6.1、是什么

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

==哨兵不是从服务器,就只是监听主机状态,还有选择合适从机做主机(原本主机down了的情况下)==

之后即使是原本的主机再次上线,新主机也不会再变成从机,而是让原本的主机做新主机的从机

image-20220826215924396

14.6.2、使用步骤

看文档

14.6.3、故障修复

image-20220826220041510

  1. 优先级在redis.conf中默认:slave-priority 100,可以自己设置。值越小优先级越高

  2. 偏移量是指获得原主机数据最全的服务器

  3. 每个redis实例启动后都会随机生成一个40位的runid

14.7、Java代码实现

写一个获得哨兵的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static JedisSentinelPool jedisSentinelPool=null;

public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.10.104:26379");

JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong

jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}

十五、集群

集群搭建具体可看我博客的另一篇文章,更加详细。

15.1、问题

容量不够,redis如何进行扩容?

并发写操作, redis如何分摊?

另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。

之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。

15.2、什么是集群

Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。

Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。

15.3、删除持久化数据

将rdb,aof文件都删除掉。

15.4、搭建模拟集群

看视频看文档

主要步骤:

  1. 先配置全局配置文件:

    开启 daemonize yes

    Appendonly 关掉或者换名字

  2. 配置局部配置

    • cluster-enabled yes 打开集群模式

    • cluster-config-file nodes-6379.conf 设定节点配置文件名

    • cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。

      最终配置文件:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      include /home/bigdata/redis.conf
      port 6379
      pidfile "/var/run/redis_6379.pid"
      dbfilename "dump6379.rdb"
      dir "/home/bigdata/redis_cluster"
      logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
      cluster-enabled yes
      cluster-config-file nodes-6379.conf
      cluster-node-timeout 15000
  3. 然后拷贝几份这个局部配置文件,对端口等做修改。

    可以利用vim快捷方式(例如将6379

    改成6380)::%s /6379 /6380

  4. 启动这几个Redis服务

    例如启动 redis6379 :redis-server redis6379.conf

    可通过进程命令查看是否成功启动:ps -ef | grep redis

  5. 将节点合成一个集群

    组合之前,请确保所有redis实例启动后,nodes-xxxx.conf文件都生成正常。

    image-20220827103308069

    合体:

    1. 进入redis安装目录下的src目录

      1
      cd  /opt/redis-7.0.4/src
    2. 集群化启动

      1
      redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391

      此处不要用127.0.0.1, 请用真实IP地址

      —replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。

  6. 登录集群

    可能直接进入读主机,存储数据时,会出现MOVED重定向操作。所以,应该以集群方式登录。

  7. 切换到相应的写主机

    使用 -c 选项

    1
    redis-cli -c -p 6379

    image-20220827153712835

  8. 查看集群信息

    通过 cluster nodes 命令


redis cluster 如何分配这六个节点?

一个集群至少要有三个主节点。

选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。

分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上


什么是插槽?

[OK] All nodes agree about slots configuration.

>>> Check for open slots…

>>> Check slots coverage…

[OK] All 16384 slots covered.

一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,

集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:

节点 A 负责处理 0 号至 5460 号插槽。

节点 B 负责处理 5461 号至 10922 号插槽。

节点 C 负责处理 10923 号至 16383 号插槽。


在集群中 录入

在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口。

redis-cli客户端提供了 –c 参数实现自动重定向。

如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。

不在一个slot下的键值,是不能使用mget,mset等多键操作。

image-20220827154211315

可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。

image-20220827154225033


在集群中 查询

CLUSTER GETKEYSINSLOT <slot><count> 返回 <count><slot> 槽中的键。

image-20220827154333884


故障恢复

如果主节点下线?从节点能否自动升为主节点?注意:15秒超时

image-20220827154409693

主节点恢复后,主从关系会如何?主节点回来变成从机。

image-20220827154445110

如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?

如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉

如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。

redis.conf中的参数 cluster-require-full-coverage

15.5、集群的Jedis开发

即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。

==无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。==

1
2
3
4
5
6
7
8
9
10
11
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.10.104",6379));

// new 的是JedisCluster
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}

15.6、集群优势

实现扩容

分摊压力

无中心配置相对简单

15.7、集群不足

多键操作是不被支持的

多键的Redis事务是不被支持的。lua脚本不被支持

由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。