目录
频道首页
Redis基础教程
收藏
1
Rocky-BCRJ 最近修改于 2023-04-27 00:25:51

@[toc]

\

一、Redis 的概述

Redis 是一个开源(BSD 许可)的内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(String),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)与范围查询,bitmaps,hyperloglogs 和地理空间 geospatial 索引半径查询。Redis 内置了复制(replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(transactions)和不同级别的磁盘持久化(persistence),并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

二、Redis 的五大基本数据类型及常用命令

String 字符串

List 列表

Set 集合

Hash 哈希

Zset 有序集合

Geospatial 地理位置

Hyperloglogs 基数统计

Bitmap 位图

1、关于 RedisKey 的基本命令

| 命令 | 解释 | 案例 | | :------- | :-------------------------- | :------------- | | set | 设置一个 key 的 value 值 | set name chian | | get | 返回 key 的 value 值 | get name | | keys * | 展示库中所有的 key | keys * | | select | 切换数据槽 | select 1 | | FLUSHALL | 清空所有槽内的数据 | FLUSHALL | | FLUSHDB | 清空当前槽内的数据 | FLUSHDB | | EXISTS | 判断某个 key 是否存在 返回 1 存在 0 不存在 | EXISTS name | | MOVE | 移除某个 key 以及其对应的 value 值 | MOVE name 1 | | EXPIRE | 为某个 key 设置过期时间 | EXPIRE name 10 | | ttl | 查看某个过期 key 的剩余的时间 | ttl name | | type | 查看某个 key 的数据类型 | type name |

// 官网命令链接
http://redis.cn/commands.html

2、String 字符串类型详解

| 命令 | 解释 | 案例 | | :------- | :---------------------------------------------------- | :--------------------- | | APPEND | 在某个已存在的 key 的 value 值后面拼接新的字符;如果 key 不存在,就 set 这个 key | APPEND name liudieyier | | STRLEN | 获取某个 key 的长度 | STRLEN name | | INCR | 将 key 对应的 value 值+1;不可用于字符型,可用于字符型数字 | INCR AGE | | DECR | 将 key 对应的 value 值-1;不可用于字符型,可用于字符型数字 | DECR age | | INCRBY | 在 key 对应的 value 的值的基础上增加 n;设定增长量 | INCRBY age 10 | | DECRBY | 在 key 对应的 value 的值的基础上减少 n;设定减少量 | DECRBY age 10 | | GETRANGE | 返回给定字符串范围内的字符 | GETRANGE name 0 3 | | SETRANGE | 从给定下表位置进行替换 | SETRANGE name 1 hello | | SETEX | 设置过期时间 | SETEX name 30 "hello" | | SETNX | 不存在再设置;如果存在,将设置失败,返回 0 | SETNX myname "mysql" | | MSET | 批量设置 | MSET k1 v1 k2 v2 k3 v3 | | MGET | 批量获取 | MGET k1 k2 k3 | | MSETNX | 保证原子性,如果有任何一个 key 存在,该操作涉及到的所有 key 都失败 | MSETNX k1 v1 k2 v2 | | GETSET | 先 get 出来然后再 set 值 | getset name liudieyi |

APPEND

起到拼接的作用

1、在某个已存在的 key 的 value 值后面拼接新的字符,类似 java 中的 append api

2、如果 key 不存在,就 set 这个 key

127.0.0.1:6379> set name Hello
OK
127.0.0.1:6379> APPEND name World!
(integer) 11
127.0.0.1:6379> get name
"HelloWorld!"
127.0.0.1:6379
​
​
127.0.0.1:6379> set name Hello
OK
127.0.0.1:6379> get name
"Hello"
127.0.0.1:6379> APPEND name ",World!"
(integer) 12
127.0.0.1:6379> get name
"Hello,World!"
127.0.0.1:6379>
​
## 如果key不存在,就set这个key
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> APPEND name "Hello World"
(integer) 10
127.0.0.1:6379> get name
"Hello World"
127.0.0.1:6379>
STRLEN

获取某个 key 的长度

127.0.0.1:6379> STRLEN name
(integer) 11
127.0.0.1:6379>
INCR

将 key 对应的 value 值+1;不可用于字符型,可用于字符型数字

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379>  incr views
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> set name liudieyi
OK
127.0.0.1:6379> INCR name
(error) ERR value is not an integer or out of range
127.0.0.1:6379>
DECR

将 key 对应的 value 值-1;不可用于字符型,可用于字符型数字

127.0.0.1:6379> get views
"1"
127.0.0.1:6379> DECR views
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> INCR views
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> DECR view
(integer) -1
127.0.0.1:6379> get views
"2"
127.0.0.1:6379>
INCRBY

自定义增长量

在 key 对应的 value 的值的基础上增加 n

127.0.0.1:6379> clear
127.0.0.1:6379> set age 0
OK
127.0.0.1:6379> get age
"0"
127.0.0.1:6379> INCRBY age 10
(integer) 10
127.0.0.1:6379> get age
"10"
127.0.0.1:6379>
DECRBY

自定义减少量

在 key 对应的 value 的值的基础上减少 n

127.0.0.1:6379> set age 0
OK
127.0.0.1:6379> get age
"0"
127.0.0.1:6379> INCRBY age 10
(integer) 10
127.0.0.1:6379> get age
"10"
127.0.0.1:6379> DECRBY age 5
(integer) 5
127.0.0.1:6379> get age
"5"
127.0.0.1:6379>
GETRANGE

返回给定字符串范围内的字符

GETRANGE name 0 3 返回已 name 为可以的 value 的值,从开始下标为 0,到下标为 3 的字符

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name china
OK
127.0.0.1:6379> GETRANGE name 0 3
"chin"
127.0.0.1:6379>
​
## 查看全部的字符串
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name china
OK
127.0.0.1:6379> GETRANGE name 0 3
"chin"
127.0.0.1:6379> GETRANGE name 0 -1  # 获取全部的字符串
"china"
127.0.0.1:6379>
SETRANGE

替换指定下标位置的字符串

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name liudieyi
OK
127.0.0.1:6379> SETRANGE name 1 1111 #从下表1开始完后替换,结果应该是l1111eyi
(integer) 8
127.0.0.1:6379> get name
"l1111eyi"
127.0.0.1:6379>
SETEX(set with expire)

设置过期时间

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> setex name 30 redis
OK
127.0.0.1:6379> ttl name
(integer) 26
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"redis"
127.0.0.1:6379> ttl name
(integer) 15
127.0.0.1:6379> ttl name
(integer) 2
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>
SETNX(set if not exist)

不存在再设置

如果当前 key 存在,将会设置失败,返回 0;成功则返回 1;常用于分布式锁中

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> SETNX name liudieyi
(integer) 1      #不存在:返回1,设置成功
127.0.0.1:6379> get name
"liudieyi"
127.0.0.1:6379> SETNX name china
(integer) 0      #存在:返回0,设置失败
127.0.0.1:6379> get name
"liudieyi"    #此时value还是liudieyi
127.0.0.1:6379>
MSET

批量设置

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k4"
4) "k1"
127.0.0.1:6379>
MGET

批量获取

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k4"
4) "k1"
127.0.0.1:6379> MGET k1 k2 k3 k4
1) "v1"
2) "v2"
3) "v3"
4) "v4"
127.0.0.1:6379>
MSETNX

批量设置(保证原子性)

保证原子性,如果有任何一个 key 存在,该操作涉及到的所有 key 都失败

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k4"
4) "k1"
127.0.0.1:6379> MGET k1 k2 k3 k4
1) "v1"
2) "v2"
3) "v3"
4) "v4"
127.0.0.1:6379> MSETNX k1 v1 k5 v5
(integer) 0
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k4"
4) "k1"
127.0.0.1:6379>
MGET and MSET

存储对象案例

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> mset name_1 "{name:liudieyi,age:25}" name_2 "{name:liudehua,age:30}"  #设计一个json来保存对象信息
OK
127.0.0.1:6379> mget name_1 name_2
1) "{name:liudieyi,age:25}"
2) "{name:liudehua,age:30}"
127.0.0.1:6379> mset name_1_name "liudieyi" name_1_age 30 name_2_name "liudehua" name_2_age 30
OK
127.0.0.1:6379> MGET name_1_name name_1_age
1) "liudieyi"
2) "30"
127.0.0.1:6379> MGET name_2_name name_2_age
1) "liudehua"
2) "30"
127.0.0.1:6379>
GETSET

先 get 再 set

如果不存在值,返回 null,如果存在值,返回原来的值并设置新的值

127.0.0.1:6379> getset name liudieyi
(nil)                # get name == null  set name liudieyi
127.0.0.1:6379> get name
"liudieyi"          #get name liudieyi
127.0.0.1:6379> getset name liudehua
"liudieyi"        #get name liudieyi  set name liudehua
127.0.0.1:6379> get name
"liudehua"     #get name liudehua
127.0.0.1:6379>
案例

String 的类似的使用场景

· 计数器

· 统计多单位的数量 uid:4008823823:follow 0

· 粉丝数

· 对象存储

3、List 列表类型解析

基本数据类型,列表

1615968725(1)

如果可以从左边进,右边出,就变成了一个队列,先进先出。

如果只能一边进,另外一边不能出,就变成了一个栈,先进后出。

如果两边都可以出,就可以变成一个阻塞队列。

总结:Redis 里面,可以把 list 玩成栈、队列、阻塞队列!

所有的 list 有关的命令都是以 L 开头的

| 命令 | 解释 | 案例 | | :-------- | :------------------------------- | :-------------------------- | | LPUSH | 将一个值或者多个值,插入到列表的头部--左 | LPUSH list one | | LRANGE | 获取队列的值,从下表开始到某个下表结束 | LRANGE list 0 5 | | RPUSH | 将一个值或者多个值,插入到列表的尾部--右 | RPUSH name lisansan | | LPOP | 移除列表的值--从左边开始 | LPOP name | | RPOP | 移除列表的值--从右边开始 | RPOP name | | LINDEX | 获取列表中下表为 n 的值 | LINDEX name 1 | | LLEN | 返回列表的长度 | LLEN name | | LREM | 移除列表中的值 | LREM name 1 liudieyi | | LTRIM | 截取固定长度之内的值 | LTRIM name 1 2 | | RPOPLPUSH | 移除列表的最后一个元素,并且将这个元素添加到一个新的列表中 | RPOPLPUSH name newname | | LSET | 根据列表的小标将新值添加进相应下标对应的值,如果下标不存,会报错 | LSET num 1 5 | | LINSERT | 往列表中的元素前面或者后面插入指定的值 | LINSERT num after 3 "hello" |

LPUSH

从头部插入

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> LPUSH list1 one
(integer) 1
127.0.0.1:6379> LPUSH list1 tow
(integer) 2
127.0.0.1:6379> LPUSH list1 three
(integer) 3
127.0.0.1:6379> keys *
1) "list1"
127.0.0.1:6379>

#批量
127.0.0.1:6379> LPUSH num 0 1 2 3 4
(integer) 5
127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"
RPUSH

从尾部插入

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> LPUSH num 1   #将一个或者多个值放到列表的头部
(integer) 1
127.0.0.1:6379> LPUSH num 2
(integer) 2
127.0.0.1:6379> LPUSH num 3
(integer) 3
127.0.0.1:6379> LPUSH num 4
(integer) 4
127.0.0.1:6379> LPUSH num 5
(integer) 5
127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> RPUSH num 0      #将一个或者多个值放在列表的尾部
(integer) 6
127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "0"
127.0.0.1:6379>
LRANGE

1、获取从某个下标开始,到某个下标结尾的数据

2、LANGE key 0 -1 获取 key 中的所有的数据

#### 情况一
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> LPUSH list1 one
(integer) 1
127.0.0.1:6379> LPUSH list1 tow
(integer) 2
127.0.0.1:6379> LPUSH list1 three
(integer) 3
127.0.0.1:6379> keys *
1) "list1"
127.0.0.1:6379> LRANGE list1 0 2  #从下表0开始到下表3结束
1) "three"
2) "tow"
3) "one"
127.0.0.1:6379> LRANGE list1 0 -1
1) "three"
2) "tow"
3) "one"
127.0.0.1:6379>

####情况二
127.0.0.1:6379> LPUSH name liudieyi
(integer) 1
127.0.0.1:6379> LPUSH name liudehua
(integer) 2
127.0.0.1:6379> LPUSH name zhangsanfeng
(integer) 3
127.0.0.1:6379> LPUSH name yangguo
(integer) 4
127.0.0.1:6379> LPUSH name xiaolongnv
(integer) 5
127.0.0.1:6379> LRANGE name 0 2  #最后塞进去的值,反而最先被取出来--先进后出
1) "xiaolongnv"
2) "yangguo"
3) "zhangsanfeng"
127.0.0.1:6379>

127.0.0.1:6379> LRANGE name 0 -1
1) "xiaolongnv"
2) "yangguo"
3) "zhangsanfeng"
4) "liudehua"
5) "liudieyi"
127.0.0.1:6379>
LPOP

移除列表的值 --从左边开始

127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "0"
127.0.0.1:6379> LPOP num
"5"
127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"
127.0.0.1:6379>
RPOP

移除列表的值--从右边开始

127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"
127.0.0.1:6379> RPOP num
"0"
127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
127.0.0.1:6379>
LINDEX

获取列表中下标为给定下标的值

127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
127.0.0.1:6379> LINDEX num 2
"2"
127.0.0.1:6379> LINDEX num 1
"3"
127.0.0.1:6379>
LLEN

返回列表的长度

127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
127.0.0.1:6379> LLEN num
(integer) 4
127.0.0.1:6379>
LREM

移除列表中的值,可以指定移除的个数

127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
127.0.0.1:6379> LPUSH num 1
(integer) 5
127.0.0.1:6379> LPUSH num 1
(integer) 6
127.0.0.1:6379> LRANGE num 0 -1
1) "1"
2) "1"
3) "4"
4) "3"
5) "2"
6) "1"
127.0.0.1:6379> LREM num 1 1  #移除列表中1个值为1的的值;存在,返回移除的值的个数
(integer) 1
127.0.0.1:6379> LRANGE num 0 -1
1) "1"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> LREM num 1 2 #移除列表中1个值为2的值;存在,返回移除的值的个数
(integer) 1
127.0.0.1:6379> LRANGE num 0 -1
1) "1"
2) "4"
3) "3"
4) "1"
127.0.0.1:6379> LREM num 2 1 #移除列表中2个值为1的值;存在,返回移除的值的个数
(integer) 2
127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
127.0.0.1:6379> LREM num 2 1 #移除列表中2个值为1的值;不存在,返回0
(integer) 0
127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
127.0.0.1:6379>
LTRIM

通过下标截取指定的长度;

注意:会改变原有队列的数据,只剩下了截断后的元素

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> RPUSH num 1
(integer) 1
127.0.0.1:6379> RPUSH num 2
(integer) 2
127.0.0.1:6379> RPUSH num 3
(integer) 3
127.0.0.1:6379> RPUSH num 4
(integer) 4
127.0.0.1:6379> RPUSH num 5
(integer) 5
127.0.0.1:6379> LTRIM num 1 3  #取下标1-3之间的值
OK
127.0.0.1:6379> LRANGE num 0 -1
1) "2"
2) "3"
3) "4"
127.0.0.1:6379
RPOPLPUSH

移除列表中最后一个元素,并且将这个元素添加到一个新的列表中;如果新的列表不存,就创建

127.0.0.1:6379> LRANGE num 0 -1
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> RPOPLPUSH num newnum  #移除num列表中尾部的一个元素,并且把它添加                                        进新的newnum列表
"4"
127.0.0.1:6379> LRANGE newnum 0 -1 #新列表中的元素
1) "4"
127.0.0.1:6379> LRANGE num 0 -1  #旧列表中的元素
1) "2"
2) "3"
127.0.0.1:6379>
LSET

根据列表的小标将新值添加进相应下标对应的值,如果下标不存,会报错

127.0.0.1:6379> LPUSH num 0 1 2 3 4
(integer) 5
127.0.0.1:6379> LRANGE num 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"
127.0.0.1:6379> LSET num 0 5  #将下标为0的元素替换成5
OK
127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "3"
3) "2"
4) "1"
5) "0"
127.0.0.1:6379>

#报错案列
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> exists num
(integer) 0
127.0.0.1:6379> LSET num 0 1
(error) ERR no such key
LINSERT

将某个具体的 value 插入到列表中任意元素的前面或者后面

127.0.0.1:6379> LPUSH num 0
(integer) 1
127.0.0.1:6379> LPUSH num 0
(integer) 2
127.0.0.1:6379> LPUSH num 2
(integer) 3
127.0.0.1:6379> LPUSH num 3
(integer) 4
127.0.0.1:6379> LPUSH num 4
(integer) 5
127.0.0.1:6379> LPUSH num 5
(integer) 6
127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "0"
6) "0"
127.0.0.1:6379> LINSERT num before 3 "hello" #往元素3的q前面插入值
(integer) 7
127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "4"
3) "hello"
4) "3"
5) "2"
6) "0"
7) "0"
127.0.0.1:6379> LINSERT num after 3 "world"  #往元素3的后面插入值
(integer) 8
127.0.0.1:6379> LRANGE num 0 -1
1) "5"
2) "4"
3) "hello"
4) "3"
5) "world"
6) "2"
7) "0"
8) "0"
127.0.0.1:6379>
小结

· list 实际上是一个链表,before Node after,left,right 都可以插入值

· 如果 key 不存在,创建新的链表

· 如果 key 存在,新增内容

· 如果移除了所有的值,空链表,也代表不存在!

· 在两边插入或改动之,效率最高!中间元素,相对来说效率会第一点

应用场景

消息队列! 消息队列(Lpush Rpop) 栈(Lpush Lpop)

4、Set 集合类型解析

set 中的值是不能重复的;无序不重复集合

| 命令 | 解释 | 案例 | | :---------- | :------------------------------- | :-------------------- | | SADD | 往集合中添加元素;不能重复,重复失败返回 0 | sadd myset "hello" | | SMEMBERS | 查看集合中的元素 | SMEMBERS myset | | SISMEMBER | 查看集合中是否存在指定的元素,存在返回 1;不存在返回 0 | SISMEMBER myset hello | | SCARD | 返回集合中元素的个数 | SCARD myset | | SREM | 移除集合中的值,成功返回 1,失败返回 0 | SREM myset hello | | SRANDMEMBER | 在集合中随机获取一个元素 | SRANDMEMBER myset | | SPOP | 随机移除指定集合中的一个元素;可指定元素的个数 | SPOP myset | | SMOVE | 将一个集合中的元素移动到另外一个集合中;如果新集合没有,将会创建 | SMOVE num newnum 1 | | SDIFF | 取目标集合中的元素的差集 | SDIFF num num1 num2 | | SINTER | 返回目标集合的元素交集 | SINTER num1 num2 | | SUNION | 返回目标集合的元素并集 | SUNION num1 num2 |

SADD

不能添加重复元素,不然会返回 0 失败,成功返回 1

127.0.0.1:6379> SADD myset "hello"
(integer) 1
127.0.0.1:6379> keys *
1) "myset"
127.0.0.1:6379> SADD myset "World"
(integer) 1
127.0.0.1:6379> SADD myset "liudieyi"
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379>

# 不能添加重复元素
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379> SADD myset hello
(integer) 0
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379>
SMEMBERS
127.0.0.1:6379> SADD myset "hello"
(integer) 1
127.0.0.1:6379> keys *
1) "myset"
127.0.0.1:6379> SADD myset "World"
(integer) 1
127.0.0.1:6379> SADD myset "liudieyi"
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379>
SISMEMBER

查看集合中是否存在指定的元素,存在返回 1;不存在返回 0

127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379> SISMEMBER myset hello
(integer) 1
127.0.0.1:6379> SISMEMBER myset hhhh
(integer) 0
127.0.0.1:6379>
SCARD

返回集合中元素的个数

127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379> SCARD myset
(integer) 3
127.0.0.1:6379>
SREM

移除集合中的值,成功返回 1,失败返回 0

127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "hello"
127.0.0.1:6379> srem myset hello
(integer) 1       #移除成功返回1
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
127.0.0.1:6379> srem myset hellasda
(integer) 0      #移除失败返回0
127.0.0.1:6379>
SRANDMEMBER

在集合中随机获取指定个数的元素 默认为 1 个

127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "2"
4) "3"
5) "1"
127.0.0.1:6379> SRANDMEMBER myset #随机取一个
"1"
127.0.0.1:6379> SRANDMEMBER myset
"World"
127.0.0.1:6379> SRANDMEMBER myset 2  #随机取两个
1) "2"
2) "3"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "liudieyi"
2) "2"
127.0.0.1:6379>
SPOP

随机移除指定集合中的一个元素;可指定元素的个数;返回已经移除的元素

127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "2"
4) "3"
5) "1"
127.0.0.1:6379> SPOP myset
"3"
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "liudieyi"
3) "2"
4) "1"
127.0.0.1:6379> SPOP myset 2
1) "liudieyi"
2) "1"
127.0.0.1:6379> SMEMBERS myset
1) "World"
2) "2"
127.0.0.1:6379>
SMOVE

将一个集合中的指定元素移动到另外一个集合中;如果新集合未创建,将创建

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> sadd num 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd newnum a b c d e
(integer) 5
127.0.0.1:6379> SMEMBERS num
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> SMEMBERS newnum
1) "c"
2) "b"
3) "d"
4) "a"
5) "e"
127.0.0.1:6379> SMOVE num newnum 1 #将num集合中的元素1移动到newnum集合中
(integer) 1
127.0.0.1:6379> SMEMBERS num
1) "2"
2) "3"
3) "4"
4) "5"
127.0.0.1:6379> SMEMBERS newnum
1) "d"
2) "a"
3) "b"
4) "c"
5) "1"
6) "e"
127.0.0.1:6379> SMOVE num newnum 1 #如果源集合中不存在元素,返回0失败
(integer) 0
127.0.0.1:6379> SMOVE num newnum2 2 #如果目标集合不存在,将创建
(integer) 1
127.0.0.1:6379> SMEMBERS newnum2
1) "2"
127.0.0.1:6379> SMOVE num newnum2 3 4 #不可从源集合中移动多个元素到目标集合
(error) ERR wrong number of arguments for 'smove' command
127.0.0.1:6379>
SDIFF

-- 差集

127.0.0.1:6379> sadd num1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd num2 2 4 6 8 10
(integer) 5
127.0.0.1:6379> SDIFF num1 num2 #返回num1数组的差集
1) "1"
2) "3"
3) "5"
127.0.0.1:6379>
SINTER

共同好友可以实现

返回目标集合的元素的交集

127.0.0.1:6379> sadd num1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd num2 2 4 6 8 10
(integer) 5
127.0.0.1:6379> SINTER num1 num2
1) "2"
2) "4"
127.0.0.1:6379>
SUNION

返回目标集合之间的并集

127.0.0.1:6379> SUNION num1 num2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "8"
8) "10"
127.0.0.1:6379> SMEMBERS num1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> SMEMBERS num2
1) "2"
2) "4"
3) "6"
4) "8"
5) "10"
127.0.0.1:6379>
案列

微博,B 站 共同关注功能!(并集)

数字集合类:

-- 差集

-- 交集

-- 并集

微博,A 用户将所有关注的人放在一个 set 集合中,通过用户唯一 id 等,将他的粉丝也放在一个集合中,就可以实现 共同关注、共同爱好、二度好友、可能认识的人等功能(六度分割理论)

5、Hash 哈希类型详解

类似 Map\集合,key-Map 集合

| 命令 | 解释 | 案例 | | :------ | :------------------------------------- | :----------------------------------- | | HSET | 给列表添加元素 | HSET myhash field1 java | | HGET | 取出列表中的元素 | HGET myhash field1 | | HMSET | 批量添加元素 | HMSET myhash field java field2 redis | | HMGET | 批量获取元素 | HMGET myhash field field1 | | HGETALL | 获取指定 key 中得所有元素 | HGETALL myhash | | HDEL | 删除 hash 指定得 key 指端,对应得 value 值就消失了 | HDEL myhash field | | HLEN | 获取长度 | HLEN myhash | | HEXISTS | 判断 hash 中得 key 是否存在,存在返回 1,不存在返回 0 | HEXISTS myhash field1 | | HKEYS | 只获取指定 hash 中得所有 key | HKEYS myhash | | HVALS | 只获取指定 hash 中得所有 value | HVALS myhash | | HINCRBY | 给指定 hash 中得 key 对应得 value 一个自增量 | HINCRBY myhash field5 10 | | HSETNX | 给指定的 hash 中的 put 值,如果存在,返回 0 失败,成功返回 1 | HSETNX myhash field4 mqtt |

HSET

给定义 key 值,往列表中添加元素

127.0.0.1:6379> clear
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> hset myhash field java
(integer) 1
127.0.0.1:6379> hget myhash field
"java"
127.0.0.1:6379>
​
## 添加相同key值得元素,值会被覆盖
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> hset myhash field java
(integer) 1
127.0.0.1:6379> hget myhash field
"java"
127.0.0.1:6379> HSET myhash field mysql
(integer) 0
127.0.0.1:6379> HGET myhash field
"mysql"
127.0.0.1:6379>
HGET

根据 key 以及 hash 的 key 值取出元素

127.0.0.1:6379> clear
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> hset myhash field java
(integer) 1
127.0.0.1:6379> hget myhash field
"java"
127.0.0.1:6379>
HMSET

批量添加元素

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> hset myhash field java
(integer) 1
127.0.0.1:6379> hget myhash field
"java"
127.0.0.1:6379> HSET myhash field mysql
(integer) 0
127.0.0.1:6379> HGET myhash field
"mysql"
127.0.0.1:6379> hset myhash field1 redis
(integer) 1
127.0.0.1:6379> HMSET myhash field hello field1 world
OK
127.0.0.1:6379> HMGET myhash field field1
1) "hello"
2) "world"
127.0.0.1:6379>
HMGET

批量获取元素

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> hset myhash field java
(integer) 1
127.0.0.1:6379> hget myhash field
"java"
127.0.0.1:6379> HSET myhash field mysql
(integer) 0
127.0.0.1:6379> HGET myhash field
"mysql"
127.0.0.1:6379> hset myhash field1 redis
(integer) 1
127.0.0.1:6379> HMSET myhash field hello field1 world
OK
127.0.0.1:6379> HMGET myhash field field1
1) "hello"
2) "world"
127.0.0.1:6379>
HGETALL

获取指定 key 对应得所有元素

127.0.0.1:6379> HGETALL myhash
1) "field"
2) "hello"
3) "field1"
4) "world"
127.0.0.1:6379>
HDEL

删除 hash 指定得 key 指端,对应得 value 值就消失了

127.0.0.1:6379> HGETALL myhash
1) "field"
2) "hello"
3) "field1"
4) "world"
127.0.0.1:6379> HDEL myhash field
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "field1"
2) "world"
127.0.0.1:6379>
HLEN

获取指定 key 得 value 得长度

127.0.0.1:6379> HGETALL myhash
1) "field1"
2) "world"
127.0.0.1:6379> HLEN myhash
(integer) 1
127.0.0.1:6379> keys *
1) "myhash"
127.0.0.1:6379>

#######################################
127.0.0.1:6379> HMSET myhash field1 redis field2 mysql field3 redis field4 jvm
OK
127.0.0.1:6379> HGETALL
(error) ERR wrong number of arguments for 'hgetall' command
127.0.0.1:6379> HGETALL myhash
1) "field1"
2) "redis"
3) "field2"
4) "mysql"
5) "field3"
6) "redis"
7) "field4"
8) "jvm"
127.0.0.1:6379> HLEN
(error) ERR wrong number of arguments for 'hlen' command
127.0.0.1:6379> HLEN myhash
(integer) 4
127.0.0.1:6379>
HEXISTS

判断 hash 中指定 key 是否存在,存在返回 1,不存在返回 0

127.0.0.1:6379> HGETALL myhash
1) "field1"
2) "redis"
3) "field2"
4) "mysql"
5) "field3"
6) "redis"
7) "field4"
8) "jvm"
127.0.0.1:6379> HEXISTS myhash field1  #存在
(integer) 1
127.0.0.1:6379> HEXISTS myhash field   #不存在
(integer) 0
127.0.0.1:6379>
HKEYS

只获取指定 hash 中得所有 key

127.0.0.1:6379> CLEAR
127.0.0.1:6379> HKEYS myhash
1) "field1"
2) "field2"
3) "field3"
4) "field4"
127.0.0.1:6379>
HVALS

只获取指定 hash 中得所有 value

127.0.0.1:6379> HKEYS myhash
1) "field1"
2) "field2"
3) "field3"
4) "field4"
127.0.0.1:6379> HVALS myhash
1) "redis"
2) "mysql"
3) "redis"
4) "jvm"
127.0.0.1:6379>
HINCRBY

给指定 hash 中得 key 对应得 value 一个自增量

127.0.0.1:6379> HSET myhash field5 10
(integer) 1
127.0.0.1:6379> hget myhash field5
"10"
127.0.0.1:6379> HINCR myhash field5
(error) ERR unknown command `HINCR`, with args beginning with: `myhash`, `field5`,
127.0.0.1:6379> HINCRBY myhash field5 5
(integer) 15
127.0.0.1:6379> hget myhash field5
"15"
127.0.0.1:6379> HINCRBY myhash field5 -5
(integer) 10
127.0.0.1:6379> hget myhash field5
"10"
127.0.0.1:6379>
HSENTNX

给指定的 hash 中的 put 值,如果存在,返回 0 失败,成功返回 1

如果有 ---------->失败

如果没有-------->成功

127.0.0.1:6379> HSETNX myhash field4 mqtt
(integer) 0
127.0.0.1:6379> HGET myhash field4
"jvm"
127.0.0.1:6379> HSETNX myhash field4 mqtt
(integer) 0
127.0.0.1:6379> HSETNX myhash field6 mqtt
(integer) 1
127.0.0.1:6379> HGET myhash field6
"mqtt"
127.0.0.1:6379>

6、Zset 有序集合详解

有序集合,在 set 基础上,增加了一个值

| 命令 | 解释 | 案例 | | :------------ | :--------------- | :---------------------------- | | ZADD | 给集合中添加元素;可以批量 | ZADD myset 1 one 2two | | ZRANGE | 查看集合中的元素 | ZRANGE myset | | ZRANGEBYSCORE | 排序;升序 | ZRANGEBYSCORE salary -inf +in | | ZREM | 移除元素 | ZREM salary xxx | | ZCARD | 查看集合中的元素个数 | ZCARD salary | | ZREVRANGE | 排序;降序 | ZREVRANGE salary -1 0 | | ZCOUNT | 返回指定集合中指定范围的元素数量 | ZCOUNT myset 1 3 |

ZADD

给集合中添加元素;可以批量

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> zadd myset 1 one 2 two 3 three
(integer) 3
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379>
ZRANGE

查看集合中的元素

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> zadd myset 1 one 2 two 3 three
(integer) 3
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379>
ZRANGEBYSCORE

排序

127.0.0.1:6379> ZRANGE salary 0 -1
1) "liudieyi"
2) "abai"
3) "alibaba"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf #现实全部用户从小到大
1) "liudieyi"
2) "abai"
3) "alibaba"
127.0.0.1:6379>

########## #现实全部用户从小到大 并附带附加成绩
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores
1) "liudieyi"
2) "5000"
3) "abai"
4) "7000"
5) "alibaba"
6) "9000"
127.0.0.1:6379>


##################获取从小到5000的数据  升序
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 5000 withscores
1) "liudieyi"
2) "5000"
127.0.0.1:6379>
ZREM

删除集合中的元素

127.0.0.1:6379> ZRANGE salary 0 -1
1) "liudieyi"
2) "abai"
3) "alibaba"
127.0.0.1:6379> ZREM salary liudieyi
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
1) "abai"
2) "alibaba"
127.0.0.1:6379>
ZCARY

查看集合中的元素个数

127.0.0.1:6379> ZCARD salary
(integer) 2
127.0.0.1:6379> ZRANGE salary 0 -1
1) "abai"
2) "alibaba"
127.0.0.1:6379>
ZREVRANGE

降序

127.0.0.1:6379> ZREVRANGE salary 0 -1
1) "alibaba"
2) "abai"
127.0.0.1:6379>
ZCOUNT

返回指定集合中指定范围的元素数量

127.0.0.1:6379> zadd myset 1 hello 2 world 3 liudieyi
(integer) 3
127.0.0.1:6379> ZCOUNT myset 1 2  #获取指定区间的成员数量
(integer) 2
127.0.0.1:6379> ZCOUNT myset 1 3
(integer) 3
127.0.0.1:6379>

三、Redis 中三种特殊的类型

1、Geospatial 地理位置

作用:朋友的定位,附近的人,打车距离计算

| 命令 | 解释 | 案例 | | :---------------- | :------------------------------------------------ | :-------------------------------------------- | | GEOADD | 添加一个或多个地理空间位置 | GEOADD china:city 116.397128 39.916527 beijin | | GEOPOS | 查询具体地理位置的经纬度 | GEOPOS china:city beijin | | GEODIST | 返回两个地理位置之间的距离 | GEODIST china:city beijin shanghai | | GEORADIUS | 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。 | GEORADIUS Sicily 15 37 200 km WITHDIST | | GEORADIUSBYMEMBER | 指定成员的位置被用作查询的中心。 | GEORADIUSBYMEMBER china:city beijin 1000 km | | Geohash | 返回一个或多个位置的 GEOHSAH 表示 | GEOHASH Sicily Palermo Catania |

GEOADD

添加一个或多个地理空间位置

规则:地球南北两极无法添加

参数 GEOADD key 经度 纬度 城市名称;

127.0.0.1:6379> GEOADD china:city 116.397128 39.916527 beijin
(integer) 1
127.0.0.1:6379> GEOADD china:city 121.48941 31.40527 shanghai
(integer) 1
127.0.0.1:6379> GEOADD china:city 113.27324 23.15792 guangzhou
(integer) 1
127.0.0.1:6379> GEOADD china:city 113.88308 22.55329 shenzhen
(integer) 1
127.0.0.1:6379> GEOADD china:city 106.54041 29.40268 chongqin 108.93425 34.23053 xian
(integer) 2
127.0.0.1:6379>
GEOPOS

查询具体地理位置的经纬度

127.0.0.1:6379> GEOADD china:city 116.397128 39.916527 beijin
(integer) 1
127.0.0.1:6379> GEOADD china:city 121.48941 31.40527 shanghai
(integer) 1
127.0.0.1:6379> GEOADD china:city 113.27324 23.15792 guangzhou
(integer) 1
127.0.0.1:6379> GEOADD china:city 113.88308 22.55329 shenzhen
(integer) 1
127.0.0.1:6379> GEOADD china:city 106.54041 29.40268 chongqin 108.93425 34.23053 xian
(integer) 2
127.0.0.1:6379> GEOPOS china:city beijin
1) 1) "116.39712899923324585"
   2) "39.91652647362980844"
127.0.0.1:6379> GEOPOS china:city xian
1) 1) "108.93425256013870239"
   2) "34.23053097599082406"
127.0.0.1:6379>
GEODIST

返回两个地理位置之间的距离

返回两个给定位置之间的距离。

如果两个位置之间的其中一个不存在, 那么命令返回空值。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。

  • km 表示单位为千米。

  • mi 表示单位为英里。

  • ft 表示单位为英尺。

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

GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。

127.0.0.1:6379> GEODIST china:city beijin shanghai  #默认单位 米
"1052105.5643"
127.0.0.1:6379> GEODIST china:city beijin shanghai km  #指定单位 千米 km
"1052.1056"
127.0.0.1:6379>
​
​
127.0.0.1:6379> GEODIST china:city shanghai shenzhen km
"1239.2104"
127.0.0.1:6379>
GEORADIUS

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

GEORADIUS key 经度 纬度 半径 单位

127.0.0.1:6379> GEORADIUS china:city 121.48789949 31.24916171 1000 km
1) "shanghai"
127.0.0.1:6379> GEORADIUS china:city 121.48789949 31.24916171 2000 km
1) "chongqin"
2) "xian"
3) "shenzhen"
4) "guangzhou"
5) "shanghai"
6) "beijin"
127.0.0.1:6379>
​
####直线距离
127.0.0.1:6379> GEORADIUS china:city 121.48789949 31.24916171 2000 km withdist
1) 1) "chongqin"
   2) "1448.5271"
2) 1) "xian"
   2) "1219.4397"
3) 1) "shenzhen"
   2) "1225.7200"
4) 1) "guangzhou"
   2) "1211.6406"
5) 1) "shanghai"
   2) "17.3639"
6) 1) "beijin"
   2) "1067.8745"
127.0.0.1:6379>
​
########经度和纬度
127.0.0.1:6379> GEORADIUS china:city 121.48789949 31.24916171 2000 km withcoord
1) 1) "chongqin"
   2) 1) "106.54040783643722534"
      2) "29.40268053517299762"
2) 1) "xian"
   2) 1) "108.93425256013870239"
      2) "34.23053097599082406"
3) 1) "shenzhen"
   2) 1) "113.88307839632034302"
      2) "22.55329111565713873"
4) 1) "guangzhou"
   2) 1) "113.27324062585830688"
      2) "23.1579209662846921"
5) 1) "shanghai"
   2) 1) "121.48941010236740112"
      2) "31.40526993848380499"
6) 1) "beijin"
   2) 1) "116.39712899923324585"
      2) "39.91652647362980844"
127.0.0.1:6379>
​
######获取半径内指定的人数以及其经纬度  --给人数限制
127.0.0.1:6379> GEORADIUS china:city 121.48789949 31.24916171 2000 km withdist withcoord count 2
1) 1) "shanghai"
   2) "17.3639"
   3) 1) "121.48941010236740112"
      2) "31.40526993848380499"
2) 1) "beijin"
   2) "1067.8745"
   3) 1) "116.39712899923324585"
      2) "39.91652647362980844"
127.0.0.1:6379> GEORADIUS china:city 121.48789949 31.24916171 2000 km withdist withcoord count 3
1) 1) "shanghai"
   2) "17.3639"
   3) 1) "121.48941010236740112"
      2) "31.40526993848380499"
2) 1) "beijin"
   2) "1067.8745"
   3) 1) "116.39712899923324585"
      2) "39.91652647362980844"
3) 1) "guangzhou"
   2) "1211.6406"
   3) 1) "113.27324062585830688"
      2) "23.1579209662846921"
127.0.0.1:6379>
GEORADIUSBYMEMBER

这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点

指定成员的位置被用作查询的中心。

#以北京为中心,查找周围1000km以内的城市  不需要写经纬度
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijin 1000 km
1) "beijin"
2) "xian"
127.0.0.1:6379>
GEOHASH

返回一个或多个位置的 GEOHSAH 表示

#将二维的经纬度转换为11位的一维的字符串
127.0.0.1:6379> GEOHASH china:city beijin shanghai
1) "wx4g0dtf9e0"
2) "wtw6st1uuq0"
127.0.0.1:6379>
案例

geo 底层所依赖的数据类型依然还是 Zset,可以使用 Zset 命令来操作 geo

127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqin"
2) "xian"
3) "shenzhen"
4) "guangzhou"
5) "shanghai"
6) "beijin"
127.0.0.1:6379> ZREM china:city beijin  #通过Zset中的命令移除北京
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqin"
2) "xian"
3) "shenzhen"
4) "guangzhou"
5) "shanghai"
127.0.0.1:6379>

2、Hyperloglogs 基数统计

Redis 2.8.9 版本更新了 Hyperloglog 数据结构!

Redis Hyperloglog 基数统计的算法

优点

占用的内存是固定的,2^64 不同的元素的技术,只需要消耗 12kb 内存;

如果从内存的角度来比较来说 Hyperloglog 首先

缺点

可能存在 0.81%的错误率;

如果不允许容错,就是用 set 或者自己的数据类型

网页的 UV(一个人访问一个网站的多次,但仍然是一个人!)

传统的方式,set 保存用户的 id,然后就可以统计 set 中元素的数量作为标准判断,可能在并发情况下存在误差(可忽略)

如果保存了大量的用户 id,就比较麻烦!我们的目的是计数,而不是保存用户 id

| 命令 | 解释 | 案例 | | :------ | :------------------------ | :---------------------- | | PFADD | 将指定元素添加到 Hyperloglog 中 | PFADD hll a b c d e f g | | PFCOUNT | 返回指定 key 的长度 | PFCOUNT num | | PFMERGE | 将指定 key 中的元素合并到一个目标 key 中 | PFMERGE num3 num num2 |

PFADD

将除了第一个参数以外的参数存储到以第一个参数为变量名的 HyperLogLog 结构中.

这个命令的一个副作用是它可能会更改这个 HyperLogLog 的内部来反映在每添加一个唯一的对象时估计的基数(集合的基数).

如果一个 HyperLogLog 的估计的近似基数在执行命令过程中发了变化, PFADD 返回 1,否则返回 0,如果指定的 key 不存在,这个命令会自动创建一个空的 HyperLogLog 结构(指定长度和编码的字符串).

如果在调用该命令时仅提供变量名而不指定元素也是可以的,如果这个变量名存在,则不会有任何操作,如果不存在,则会创建一个数据结构(返回 1).

了解更多 HyperLogLog 数据结构,请查阅PFCOUNT命令页面.

如果 HyperLogLog 的内部被修改了,那么返回 1,否则返回 0 .

127.0.0.1:6379> PFADD num 1 2 3 4 5 6 7 8 9 10
(integer) 1
PFCOUNT

返回指定 key 的长度

127.0.0.1:6379> PFCOUNT num
(integer) 10
PFMERGE

将指定 key 中的元素合并到一个目标 key 中

127.0.0.1:6379> PFMERGE num3 num num2
OK
127.0.0.1:6379> PFCOUNT num3
(integer) 20
127.0.0.1:6379>

3、Bitmap 位图场景详解

位存储,数据结构!都是操作二进制来进行记录,就只有 0 和 1 两个状态

| 命令 | 解释 | 案例 | | :------- | :----------- | :-------------- | | SETBIT | 塞 | SETBIT sign 0 1 | | GETBIT | 取 | GETBIT sign 3 | | BITCOUNT | 统计;返回为 1 的个数 | BITCOUNT SIGN |

SETBIT
#使用bitmap记录周一到周二的打卡  0未打卡 1已打卡  统计打卡次数 统计为1的个数
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1
(integer) 0
127.0.0.1:6379> SETBIT sign 4 0
(integer) 0
127.0.0.1:6379> SETBIT sign 5 1
(integer) 0
127.0.0.1:6379> SETBIT sign 6
(error) ERR wrong number of arguments for 'setbit' command
127.0.0.1:6379> SETBIT sign 6 1
(integer) 0
127.0.0.1:6379>
GETBIT
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 1
127.0.0.1:6379>
BITCOUNT

统计;返回为 1 的个数

127.0.0.1:6379> BITCOUNT sign
(integer) 4
127.0.0.1:6379>

四、Redis 基本的事务操作

Redis 事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中,都会按照顺序执行

一次性、顺序性、排他性(不允许被其他操作干扰)

Redis 事务没有隔离级别的概念

所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec

Redis 单条命令是保证原子性的,但是 Redis 事务不保证原子性

Redis 事务:

  • 开启事务(MULTI)

  • 命令入队(…)

  • 执行事务(EXEC)

127.0.0.1:6379> MULTI        #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1   #设置过程
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> EXEC   #执行事务
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379>
​
​
####################放弃事务
127.0.0.1:6379> MULTI  #开始事务
OK
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> set k7 v7
QUEUED
127.0.0.1:6379(TX)> DISCARD   #丢弃事务 事务队列中的命令都不会执行
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> get k5
(nil)
127.0.0.1:6379>

编译型异常(代码有问题!命令有错),事务中所有的命令都不会被执行,不会被提交

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> keys *  # 所有的命令都不会被执行
(empty array)
127.0.0.1:6379>

运行时异常:如果事务队列中存在语法性,那么执行命令的时候,会跳过错误的命令,其他命令可以正常运行以及提交事务,错误命令最终会导致抛出异常

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> get k4
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) OK
5) "v4"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379>

五、Redis 的乐观锁

悲观锁:很悲观,很保守,认为每一步操作都可能出问题,所以无论干啥都会加锁

乐观锁:认为任何时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人改动过这个诗句

获取 version

更新的时候比较 version

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> WATCH money #监视money对象
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>

测试多线程修改值,使用 watch 充当乐观锁操作的测试

#客户端一
127.0.0.1:6379> watch money   #开启检测
OK
127.0.0.1:6379> mult
(error) ERR unknown command `mult`, with args beginning with:
127.0.0.1:6379> MULTI     #开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 10   #更新操作
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10    #更新操作
QUEUED
127.0.0.1:6379(TX)> exec    #执行事务
(nil)                   #执行失败
127.0.0.1:6379>
#客户端二
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000  #修改了值
OK
127.0.0.1:6379>

在客户端一开启了事务但是还没有执行的时候,客户端二对 money 进行了修改,导致客户端一执行失败

1、如果发现事务执行失败,就先解锁

2、获取最新的锁,再次监视,select version

3、再次执行

4、比对监视的值是否发生了变化,如果没有变化,就可以执行成功,不然,就失败

127.0.0.1:6379> WATCH money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 1
QUEUED
127.0.0.1:6379(TX)> INCRBY out 1
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 999
2) (integer) 1
127.0.0.1:6379>

六、通过 Jedis 控制 Redis

什么是 Jedis? 是 Redis 官方推荐的 java 连接开发工具!使用 java 操作 Redis 中间件,如果要使用 java 操作 Redis,那么一定要对 Jedis 十分的熟悉

1、导入对应 pom 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ricardo</groupId>
    <artifactId>redis-01-jedis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--    导入jedis-->
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>
    </dependencies>

</project>

2、编码测试

---链接数据库

---操作命令

---断开连接

package com.ricardo;

import redis.clients.jedis.Jedis;

/**
 * 测试java连接redis客户端
 */
public class TestPing {

    private static final String HOST = "127.0.0.1";
    private static final String PASS_WORD = "liudieyi1206@";

    public static void main(String[] args) {
        // 创建Jedis连接对象
        Jedis jedis = new Jedis(HOST,6379);
        // 设置Jedis连接密码
        jedis.auth(PASS_WORD);
        System.out.println(jedis.ping());
    }
}

1615906542(1)

Jedis 里面的所有基本 api 都和 Redis 客户端的命令一模一样

3、通过 Redis 再次理解事物

正常提交的事务

package com.ricardo;

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

/**
 * 通过jedis测事务
 */
public class TxTest {
    private static final String HOST = "127.0.0.1";
    private static final String PASS_WORD = "liudieyi1206@";

    public static void main(String[] args) {
        // 创建Jedis连接对象
        Jedis jedis = new Jedis(HOST,6379);
        // 设置Jedis连接密码
        jedis.auth(PASS_WORD);

        // 准备测试的数据
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","mysql");
        jsonObject.put("age","40");

        // 开启事务
        Transaction multi = jedis.multi();
        try {
            multi.set("user1",JSONObject.toJSONString(jsonObject));
            multi.set("user2",JSONObject.toJSONString(jsonObject));
            // 执行事务
            multi.exec();
        } catch (Exception e) {
            // 发生异常,放弃事务
            multi.discard();
        } finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            // 关闭连接
            jedis.close();
        }

    }
}

结果

1615907683(1)

发生异常,抛弃事务

package com.ricardo;
​
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
​
/**
 * 通过jedis测事务
 */
public class TxTest {
 &nbsp; &nbsp;private static final String HOST = "127.0.0.0";
 &nbsp; &nbsp;private static final String PASS_WORD = "liudieyi1206@";
​
 &nbsp; &nbsp;public static void main(String[] args) {
 &nbsp; &nbsp; &nbsp; &nbsp;// 创建Jedis连接对象
 &nbsp; &nbsp; &nbsp; &nbsp;Jedis jedis = new Jedis(HOST,6379);
 &nbsp; &nbsp; &nbsp; &nbsp;// 设置Jedis连接密码
 &nbsp; &nbsp; &nbsp; &nbsp;jedis.auth(PASS_WORD);
​
 &nbsp; &nbsp; &nbsp; &nbsp;// 准备测试的数据
 &nbsp; &nbsp; &nbsp; &nbsp;JSONObject jsonObject = new JSONObject();
 &nbsp; &nbsp; &nbsp; &nbsp;jsonObject.put("name","mysql");
 &nbsp; &nbsp; &nbsp; &nbsp;jsonObject.put("age","40");
​
 &nbsp; &nbsp; &nbsp; &nbsp;// 清空数据
 &nbsp; &nbsp; &nbsp; &nbsp;jedis.flushDB();
​
 &nbsp; &nbsp; &nbsp; &nbsp;// 开启事务
 &nbsp; &nbsp; &nbsp; &nbsp;Transaction multi = jedis.multi();
 &nbsp; &nbsp; &nbsp; &nbsp;try {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;multi.set("user1",JSONObject.toJSONString(jsonObject));
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;multi.set("user2",JSONObject.toJSONString(jsonObject));
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int a = 1/0;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 执行事务
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;multi.exec();
 &nbsp; &nbsp; &nbsp;  } catch (Exception e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 发生异常,放弃事务
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;multi.discard();
 &nbsp; &nbsp; &nbsp;  } finally {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(jedis.get("user1"));
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(jedis.get("user2"));
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 关闭连接
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;jedis.close();
 &nbsp; &nbsp; &nbsp;  }
​
 &nbsp;  }
}
​

1615907803(1)

七、SpringBoot 整合 Redis

SpringBoot 操作数据:Spring-data jpa jdbc mongodb redis

在 SpringBoot 2.X 知乎,原来使用的 Jedis 被替换成了 lettuce

Jedis:底层采用的直连,如果有多个线程操作的化,是不安全的,如果想要避免不安全,须使用 jedis pool 连接池!类似 BIO 模式,阻塞

lettuce:底层采用 Netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数量,类似 NIO 模式

注意!SpringBoot 2.X 之后如果要配置 Jedis 连接池,建议使用 lettuce pool,因为在 RedisConnectionFactory 的实现类 JedisConnectionFactory 中,有一些类不存在了,导致有一些功能并不能生效;而在 LettuceConnectionFactory 并不存在该种情况

源码分析

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate") //我们可以自己定义RedisTemplate来替换
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
 &nbsp; &nbsp; &nbsp; &nbsp;// 默认的RedisTemplate没有过多的设置,redis对象都是需要序列化
 &nbsp; &nbsp; &nbsp; &nbsp;// 两个泛型都是Object,Object类型,我们使用需要强制转换
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
​
    @Bean
    @ConditionalOnMissingBean //常用使用类型,所以单独提出来的
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

1、整合测试

  • 导入依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.3</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.ricardo</groupId>
        <artifactId>redis-02-springboot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>redis-02-springboot</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <!--操作redis-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!--web支持-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    ​
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    ​
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    ​
    </project>
    ​
    
  • 配置属性

    spring.redis.host=106.14.28.105
    spring.redis.port=6379
    spring.redis.password=liudieyi1206@
    
  • 编码测试

package com.ricardo;
​
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
​
@SpringBootTest
class Redis02SpringbootApplicationTests {
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("redis","Hello!");
        System.out.println(redisTemplate.opsForValue().get("redis"));
    }
​
}

1615965252(1)

2、值的序列化

1615965362(1)

企业微信截图_16159654905094

3、解决 Redis 序列化问题以及自定义 RedisTemplate

测试序列化与无序列化的差距

未序列化带来的问题

package com.ricardo;
​
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ricardo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
​
@SpringBootTest
class Redis02SpringbootApplicationTests {
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("redis","Hello!");
        System.out.println(redisTemplate.opsForValue().get("redis"));
    }
​
    @Test
    public void test() throws JsonProcessingException {
        User user = new User("刘叠祎",18);
        // 测试经过了序列化后传json
        //String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }
​
}
​

1615966263(1)

序列化之后

package com.ricardo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ricardo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("redis","Hello!");
        System.out.println(redisTemplate.opsForValue().get("redis"));
    }

    @Test
    public void test() throws JsonProcessingException {
        User user = new User("刘叠祎",18);
        // 测试经过了序列化后传json
        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

}

1615966344(1)

结论:没有序列化的对象,JdkSerializationRedisSerializer 识别不了,故无法传递对象,会报一个序列化错误;所以,所有的对象都需要序列化

测试序列化
package com.ricardo.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author ldy
 * @date 2021-03-17 15:22
 **/
@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User implements Serializable {
    private String name;
    private Integer age;
}
package com.ricardo;
​
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ricardo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
​
@SpringBootTest
class Redis02SpringbootApplicationTests {
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("redis","Hello!");
        System.out.println(redisTemplate.opsForValue().get("redis"));
    }
​
    @Test
    public void test() throws JsonProcessingException {
        User user = new User("刘叠祎",18);
        // 测试经过了序列化后传json
        //String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }
​
}

测试结果

1615966679(1)

传输成功

在企业中,我们所有 POJO 类都需要序列化

自定义序列化配置

在 RedisTemplate 底层支持的序列化方式有如下几种

1615967098(1)

编写自己的 RedisTemplate

package com.ricardo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;

/**
 * Redis的配置类
 * @author ldy
 * @date 2021-03-17 15:19
 **/
@Configuration
public class RedisConfig {

    /**
     * 编写我们的RedisTemplate
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 为了开发方便,一般使用String,Object类型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 添加连接工厂
        template.setConnectionFactory(redisConnectionFactory);
        // 序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 使用ObjectMapper进行转义
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // String序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也要采用string的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

测试代码

    @Test
    public void test() throws JsonProcessingException {
        User user = new User("刘叠祎",18);
        // 测试经过了序列化后传json
        //String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

测试结果

1615967934(1)

结果:key 与 value 皆无乱码

提供企业级 Redis 工具类:

RedisUtil.java

测试 RedisUtil
    @Test
    public void test1(){
        redisUtil.set("user_1",new User("张三丰",18));
        System.out.println(redisUtil.get("user_1"));
    }

结果

1615969219(1)

八、Redis 配置文件详解(Redis.conf)

我们启动的时候,都是通过配置文件来启动

1、配置文件 unix 单位对大小写不敏感

1615969852(1)

2、可以自定义多个配置文件

1615970016(1)

3、网络。指定的 ip

1615970144(1)

4、端口号

1615970227(1)

bind 127.0.0.1  #绑定的ip
protected-mode no  #保护模式   生产环境一般都是yes
port 6379      #端口号

通用配置

daemonize yes   #以守护线程的方式运行  默认是no 我们需要自己手动设置未yes让他后台运行
pidfile /var/run/redis_6379.pid  #如果是守护线程的方式运行,我们就需要执行一个pid文件

# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 部分重要信息 生产环境适用
# warning (only very important / critical messages are logged)
loglevel notice

logfile ""   #日志的文件名

databases 16 #数据库的数量,默认为16个库
always-show-logo no  #是否开启的时候总是显示Redis的logo

快照

持久化:在规定的时间内,执行了多少次操作,则会持久化到文件,.rdb 文件 .aof 文件

redis 是内存数据库,如果没有持久化,那么数据断电、或者 redis 宕机就会全部失去

# 如果3600秒内,如果至少有一个key进行了修改,我们就进行持久化操作
# save 3600 1
# 假设300秒内,至少有100个操作,我们就进行持久化操作
# save 300 100
# 假设60秒内,至少有1W个操作,我们就进行持久化操作
# save 60 10000

stop-writes-on-bgsave-error yes  #持久化如果出错了,是否还需要redis继续工作
rdbcompression yes  #是否压缩我们.rdb文件,需要消耗一些CPU资源
rdbchecksum yes #保存.rdb文件的时候,进行错误的检查校验,如果有误,会进行一些操作
dir ./   #.rdb文件保存的目录

REPLICATION 主从复制

SECURITY 安全

requirepass foobared   #设置redis连接密码  例如requirepass root

CLIENTS 限制

# maxclients 10000 &nbsp; #设置能连接Redis的最大客户端的数量
# maxmemory <bytes>  #redis配置设置最大的内存容量
# maxmemory-policy noeviction  #内存达到上限的处理策略
    #移除一些过期的key
    #报错
    #....
1、noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2、allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
3、allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
4、volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
5、volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
6、volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
​

APPEND ONLY MODE 模式 aof 配置 持久化方式

appendonly no &nbsp;#默认是不开启aof模式,因为默认是使用rdb方式持久化,在大部分所有的场景下,rdb完全够用
appendfilename "appendonly.aof" &nbsp;#持久化的文件的名字
​
# appendfsync always  #每次修改都会同步  消耗性能
appendfsync everysec &nbsp; #每秒执行一次sync,可能会丢失这1秒的数据
# appendfsync no &nbsp; &nbsp;  #不执行同步,这个时候操作系统自己同步数据,速度最快,一般也不要

九、Redis 持久化

Redis 是内存数据库,如果不把内存中的数据及时保存到磁盘中,那么服务器进程一旦因为异常情况退出,那么内存中的数据将丢失。所以 Redis 提供了持久化功能

1、RDB(Redis DataBase)

1615973630(1)

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

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

rdb 保存的文件是 dump.rdb 都是在配置文件中的快照模块配置

# The filename where to dump the DB
dbfilename dump.rdb

测试,修改持久化配置

# save 3600 1
# save 300 100
# save 60 10000
save 60 5

当前文件目录下,无 dump.rdb 文件

1615974297(1)

此时我来操作数据库多次

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 k2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> set k4 v4
OK
127.0.0.1:6379> set k5 v5
OK
127.0.0.1:6379> set k6 k6
OK
127.0.0.1:6379> set k7 v7
OK
127.0.0.1:6379>

目录下已经多出来 dump.db 文件

1615974520(1)

此时,我们将 redis 关机

127.0.0.1:6379> shutdown
not connected>
​
###redis已被关闭,进程已消失
[root@iZuf643fe16d8rmoz485dgZ bin]# ps -ef|grep redis
root &nbsp; &nbsp; &nbsp; 40234 &nbsp; 37964 &nbsp;0 17:54 pts/2 &nbsp; &nbsp;00:00:00 grep --color=auto redis
[root@iZuf643fe16d8rmoz485dgZ bin]#

开启 redis,查看数据

[root@iZuf643fe16d8rmoz485dgZ bin]# redis-server config/redis.conf
[root@iZuf643fe16d8rmoz485dgZ bin]# redis-cli -p 6379
127.0.0.1:6379> auth liudieyi1206@
OK
127.0.0.1:6379> keys * &nbsp; &nbsp; ##数据仍然存在
1) "k6"
2) "user_1"
3) "k5"
4) "k2"
5) "k3"
6) "k1"
7) "k7"
8) "k4"
9) "user"
127.0.0.1:6379>

快速产生 dump.rdb 文件

有一些敏感操作会立即生成 dump.rdb 文件,例如 FLUSHALL 命令

触发机制

1、save 的规则满足的情况下,会自动触发 rdb 规则

2、执行 FLUSHALL 命令,也会触发 rdb 规则

3、推出 Redis,也会产生 rdb 文件

备份就自动生成一个 dump.rdb 文件

如何恢复 rdb 文件

1、只需要将 rdb 文件放在 redis 的启动目录下,redis 启动的时候会自动扫描该目录检查 dump.rdb 文件,恢复 rdb 中的数据

2、查看我们 rdb 文件需要存放的文件

127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/bin"     #如果在这个目录下存在 dump.rdb文件,启动时会自动扫描并恢复文件中记载的数据
127.0.0.1:6379>

3、在生产环境中,我们会将 dump.rdb 文件进行备份

优点

1、适合大规模的数据恢复!

2、如果对数据完整性的要求不高!即可使用该规则

缺点

1、需要一定的时间间隔进程操作,如果 redis 意外宕机了,这个最后一次修改数据就没有了

2、fork 进程的时候,会占用一点的内存空间。这里需要考虑内存的情况

2、AOF(Append Only File)

将我们的所有命令都记录下来,类似于记录所有历史操作的文件,恢复的时候就相当于把这个文件里面的记载的命令都重新执行一遍。

以日志的形式来记录每一个写操作,将 Redis 执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动的时候读取这些数据重新构建数据,相当于,redis 重启之后根据日志文件的内容将指令从前到后执行一次以完成数据的恢复工作。

Aof 保存的文件名称是 appendonly.aof

############################## APPEND ONLY MODE ###############################

# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.

appendonly no   #默认是no,不开启;我们手动设置为yes 开启

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof"  #生成的日志文件的名称
# appendfsync always
appendfsync everysec   #默认记录每一秒中记录一次
# appendfsync no

##################################重写规则说明####################################

no-appendfsync-on-rewrite no  #是否重写 默认是no 保证数据的安全性

auto-aof-rewrite-percentage 100
#aof文件增长比例,指当前aof文件比上次重写的增长比例大小。aof重写即在aof文件在一定大小之后,重新将整个内存写到aof文件当中,以反映最新的状态(相当于bgsave)。这样就避免了,aof文件过大而实际内存数据小的问题(频繁修改数据问题)

auto-aof-rewrite-min-size 64mb
#aof文件重写最小的文件大小,即最开始aof文件必须要达到这个文件时才触发,后面的每次重写就不会根据这个变量了(根据上一次重写完成之后的大小).此变量仅初始化启动redis有效.如果是redis恢复时,则lastSize等于初始aof文件大小.

aof-load-truncated yes
#指redis在恢复时,会忽略最后一条可能存在问题的指令。默认值yes。即在aof写入时,可能存在指令写错的问题(突然断电,写了一半),这种情况下,yes会log并继续,而no会直接恢复失败.

测试

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379>

1616036748(1)

1616036782(1)

如果我们的 appendonly.aof 文件损坏了,比如说我们在这个文件里面写一些毫无相关的数据,那么他将会依赖文件目录下面的 redis-check-aof 来恢复 appendonly.aof 文件。

如果 appendonly.aof 文件损坏了,那么 redis 将启动不起来,我们需要修复 appendonly.aof 文件,运行 redis-check-aof 文件来进行修复,然后再次重启

redis-check-aof --fix appendonly.aof  #修复aof文件

#AOF配置文件有损坏的情况提示
[root@iZuf643fe16d8rmoz485dgZ bin]# redis-check-aof --fix appendonly.aof
0x               0: Expected prefix '*', got: 'a'
AOF analyzed: size=123, ok_up_to=0, diff=123
This will shrink the AOF from 123 bytes, with 123 bytes, to 0 bytes
Continue? [y/N]: y   #这边需要确认Y
Successfully truncated AOF
[root@iZuf643fe16d8rmoz485dgZ bin]#

#AOF文件无损坏的情况的提示
[root@iZuf643fe16d8rmoz485dgZ bin]# redis-check-aof --fix appendonly.aof
AOF analyzed: size=110, ok_up_to=110, diff=0
AOF is valid
[root@iZuf643fe16d8rmoz485dgZ bin]#

优点和缺点

优点:

1、可以通过配置将每次的修改都持久化,以便在出现一些不可预估的错误的时候,保留一些手段快速恢复数据

2、根据配置的不同,性能损耗不同,可根据实际业务场景以及吞吐量来规划

缺点:

1、相对于数据文件来说,aof 远远大于 rdb,修复速度要比 rdb 慢

2、Aof 运行效率要比 rdb 慢,因为涉及到所有的指令的读写,所以我们 redis 的默认配置的持久化方案是 rdb

扩展

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

2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 Redis 协议追加保存每次写的操作到文件末尾,Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大

3、只做缓存,如果只希望数据在服务器运行的时候存在,也可以不用任何持久化

4、同时开启两种持久化方式

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

  • RDB 的数据不实时,同时使用两者时服务器重启也只会招 AOF 文件,那要不要只使用 AOF 呢?建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段

5、性能建议

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

  • 如果 Enable AOF,,好处是在最恶劣的情况下也只会丢失不超过两秒的数据,启动脚本较为简单只 load 自己的 AOF 文件可以,代价一是带来了持续的 IO,二是 AOF rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的,只要硬盘许可,应该尽量减少 AOF rewirte 的频率,AOF 重写的基础大小默认值 64M 太小了,可以设置到 5G 以上,默认超过原大小 100%大小重写可以改到适当的数值,个人建议 80%,根据分配的磁盘大小以及业务吞吐量决定,不一定要 100%

  • 如果不 Enable AOF,仅靠 Master-Slave Repllcation 实现高可用也可以,能省掉一大笔 IO,也减少了 rewirte 时带来的系统波动。代价时如果 Master/Slave 同时宕机,比如说停电,会丢失几十分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件。载入较新的那个; 微博为该种架构

十、Redis 订阅发布

通信 队列 发送者(pub)把信息存到消息队列,订阅者(sub)接收信息

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

角色:

1、消息发送者

2、频道

3、消息订阅者

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

pubsub1

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

pubsub2

测试

#客户端一 订阅端 使用过命令订阅一个主题
127.0.0.1:6379> SUBSCRIBE yekongbujianxingchen
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "yekongbujianxingchen"
#客户端二 发送端 使用命令往主题上发布信息
127.0.0.1:6379> PUBLISH yekongbujianxingchen "hello world"  #发送者发送消息到频道
(integer) 1
127.0.0.1:6379> PUBLISH yekongbujianxingchen "hello redis"
(integer) 1
127.0.0.1:6379>
#查看客户端一 订阅单 的信息
127.0.0.1:6379> SUBSCRIBE yekongbujianxingchen
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "yekongbujianxingchen"
3) (integer) 1
1) "message"
2) "yekongbujianxingchen"
3) "hello world"
1) "message"
2) "yekongbujianxingchen"
3) "hello redis"

使用场景

1、实时消息系统!(全站广播)

2、实时聊天

3、订阅、关注(例如公众号推送消息)

十一、Redis 集群环境搭建(主从复制)

1、概念

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(Master/learder),后者称为从节点(Salve/follower);数据的复制时单向的,只能由主节点到从节点。Master 以写为主,Slave 以读为主。

Redis 集群最低需要三台

优点

1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式

2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余

3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量

4、高可用基石:上述作用,都是主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础

一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是不可以的,原因:

1、从结构上来说,单台 Redis 服务器会发生单点故障,如果故障,除非排除故障重启外,无弥补方案;所以需要集群方案来保证高可用

2、从容量来说,高并发高流量的项目中,只用一台 redis 是不够的,因为 redis 的数据是基于内存的,虽然说可以将热点数据运用在内存中,其他数据放在磁盘,或者数据到达一定量的时候在刷新到磁盘;但是对于高并发项目来说,不可能一台 redis 给他分配几百 G 甚至更多的内存。

故在企业中,使用 Redis 一定要搭建集群,最好是异地多节点

2、环境配置

只配置从库,不用配置主库;Redis 默认自己就是主库

#查看当前库的信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:2c6d811a845a4a1f2dc0c9922835ab6feb9e14a4
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>

1、将 redis.conf 文件复制三份

[root@iZuf643fe16d8rmoz485dgZ bin]# cd config/
[root@iZuf643fe16d8rmoz485dgZ config]# cp redis.conf redis79.conf
[root@iZuf643fe16d8rmoz485dgZ config]# cp redis.conf redis80.conf
[root@iZuf643fe16d8rmoz485dgZ config]# cp redis.conf redis81.conf

2、修改文件端口

###79的文件
logfile "6379.log" &nbsp;#日志文件命名,防止重复
dbfilename dump6379.rdb &nbsp;#rdb文件命名,防止重复
​
###80的文件
port 6380 &nbsp; &nbsp;#修改端口号
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dbfilename dump6380.rdb
​
###81的文件
port 6381 &nbsp; &nbsp;#修改端口号
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump6381.rdb

3、根据配置文件不同启动以上三台 redis 实例

1616058364(1)

十二、主从复制之复制原理

一主二从

以上第十一节配置完后,三台 redis 还是主机

1、现在将 80 和 81 配置为从机

#6380端口的机器
127.0.0.1:6380> SLAVEOF 106.14.28.105 6379
OK
127.0.0.1:6380>
​
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=28,lag=1
master_failover_state:no-failover
master_replid:a0428aec91a7f290046fbdf5b010605454593b1c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
127.0.0.1:6379>
​
#6381的端口的机器
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:98
slave_priority:100
slave_read_only:1
connected_slaves:0
master_failover_state:no-failover
master_replid:a0428aec91a7f290046fbdf5b010605454593b1c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:98
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:71
repl_backlog_histlen:28
127.0.0.1:6381>

2、查看 6379 端口的 redis 实例信息

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2   # 如果两个都配置成功了,是有两个从机的
slave0:ip=127.0.0.1,port=6380,state=online,offset=112,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=112,lag=0
master_failover_state:no-failover
master_replid:a0428aec91a7f290046fbdf5b010605454593b1c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:112
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:112
127.0.0.1:6379>

主机可以写,从机不能写只能读

#从机
127.0.0.1:6380> get name
"helloword"
127.0.0.1:6380> set name helloworld
(error) READONLY You can't write against a read only replica. &nbsp;  #报错
127.0.0.1:6380>
[root@iZuf643fe16d8rmoz485dgZ bin]#
​
#主机
127.0.0.1:6379> set name liudieyi
OK
127.0.0.1:6379> get name
"liudieyi"
127.0.0.1:6379>

如果主机断开了,从机仍然可以读,但是不能写;主机重新连接回来了,从机仍然可以获取到主机重新连接后写的数据

如果是使用命令行配置的主从,那么从机断开后,将读取不到在从机断开的这一段时间内,主机写的数据;必须要重新配置主机的地址后,便可重新获取到断开连接的这段时间内主机写的数据

注意:如果你在 Redis 的配置文件里面配置了 Redis 连接密码,那么一定要在从机的配置文件中加上 Redis 的密码 :

masterauth xxxxx

xxxx 为你的主机器的密码;不然你在 info replication 命令看从机和主机的状态中显示是机子的状态是关闭的;如果主机 有密码,必须要配这个

另外,真实环境的配置,是需要在配置文件里面配置的,不是通过命令

masterauth <master-password>  例如 masterauth 127.0.0.1 6379

复制原理

和 MySQL 主从复制的原因一样,Redis 虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis 支持主从复制,Redis 的主从结构可以采用一主多从或者级联结构,Redis 主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。

f892996953174d73a6f04a4f2510376d

1、全量同步

Redis 全量复制一般发生在 Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份。具体步骤如下:

  • 从服务器连接主服务器,发送 SYNC 命令;

  • 主服务器接收到 SYNC 命名后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;

  • 主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;

  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;

  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;

  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

20993eb5d0f640fdac9c48971ffb8783

完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

2、增量同步

Redis 增量复制是指 Slave 初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。

增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

3、 Redis 主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步

4、注意点

如果多个 Slave 断线了,需要重启的时候,因为只要 Slave 启动,就会发送 sync 请求和主机全量同步,当多个同时出现的时候,可能会导致 Master IO 剧增宕机。

十三、宕机后手动配置主机

1616121251(1)

1、如果一主二从配置,主机宕机了,只有两个从机实例在运行;相当于整个系统只有读的业务在成功运行,所有写的请求都会瘫痪。那么如何在主机宕机的情况下,从两个从机实例中,选举出一个从机将他的身份变成主机,来进行读写操作呢?

2、在排除哨兵模式的情况下,需要在设置为主机的从机的实例上执行以下命令

SLAVEOF no one &nbsp; #切换从机身份变成主机
127.0.0.1:6381> SLAVEOF no one &nbsp; ##重点
OK
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:7c37bf3f3f486674d6b84df86d677e9f1c899787
master_replid2:a0428aec91a7f290046fbdf5b010605454593b1c
master_repl_offset:77602
second_repl_offset:77603
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:71
repl_backlog_histlen:77532
127.0.0.1:6381>

在这个身份变换后的从机地址下的从机,都会默认为这个从机为老大。

如果在我们重新通过手动配置主机后,原先的主机重新连接上来了,但是结果是,这台恢复好的主机变成了一个光杆司令,因为他的从机已经断开和他的连接,自己成为了一个主机了。谋朝篡位,老皇帝回来了也只能是一个无政权的太上皇

十四、哨兵模式详解

自动选举 Master 的机制

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用,这不是一种推荐的方式,更多时候,我们希望,当主服务器宕机了,其下属从服务器中能自动切换身份为主服务器,起到主服务器的功能。所以,我们在实际开发中优先考虑哨兵模式。Redis 从 2.8 版本开始正式提供了 Sentinel(哨兵)架构来解决这个问题

哨兵模式能够监控主机是否有故障,如果故障了根据从机之间的投票自动将某个从库转换为主库

哨兵模式是一个特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待 Redis 服务器相应,从而监控运行多个 Redis 实例

11320039-57a77ca2757d0924

1616122604(1)

当然,我们这边的哨兵是一个独立的进程,如果这个线程也死了,那么就起不到哨兵的作用了,所以我们一般会给哨兵也配置一个从机,来保证他的高可用

11320039-3f40b17c0412116c

假设主服务器宕机,哨兵 1 先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵 1 三观的认为主服务器不可用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover【故障转移】操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

测试

我们现在的状态是一主二从

1、新建一个 conf 文件

cd /usr/local/bin/config/ &nbsp; #进入到一个配置文件的目录  目录无要求 看个人爱好
vim sentinel.conf &nbsp; &nbsp;#vim命令创建并且修改
###在sentinel.cong文件中配置如下
sentinel monitor myredis 127.0.0.1 6379 1

第一行配置指示 Sentinel 去监视一个名为 mymaster 的主 redis 实例, 这个主实例的 IP 地址为本机地址 127.0.0.1 , 端口号为 6379 , 而将这个主实例判断为失效至少需要 2 个 Sentinel 进程的同意,只要同意 Sentinel 的数量不达标,自动 failover 就不会执行。同时,一个 Sentinel 都需要获得系统中大多数 Sentinel 进程的支持, 才能发起一次自动 failover, 并预留一个新主实例配置的编号。而当超过半数 Redis 不能正常工作时,自动故障转移是无效的。

1616123673(1)

2、配置完成后,启动哨兵

[root@iZuf643fe16d8rmoz485dgZ bin]# redis-sentinel config/sentinel.conf
3035:X 19 Mar 2021 11:15:31.879 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
3035:X 19 Mar 2021 11:15:31.879 # Redis version=6.2.1, bits=64, commit=00000000, modified=0, pid=3035, just started
3035:X 19 Mar 2021 11:15:31.879 # Configuration loaded
3035:X 19 Mar 2021 11:15:31.879 * monotonic clock: POSIX clock_gettime
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  _._
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.-``__ ''-._
 &nbsp; &nbsp;  _.-`` &nbsp; &nbsp;`.  `_. &nbsp;''-._ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Redis 6.2.1 (00000000/0) 64 bit
  .-`` .-```.  ```\/ &nbsp;  _.,_ ''-._
 ( &nbsp; &nbsp;' &nbsp; &nbsp;  , &nbsp; &nbsp; &nbsp; .-`  | `, &nbsp;  ) &nbsp; &nbsp; Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'| &nbsp; &nbsp; Port: 26379
 | &nbsp; &nbsp;`-._ &nbsp; `._ &nbsp;  / &nbsp; &nbsp; _.-' &nbsp;  | &nbsp; &nbsp; PID: 3035
 &nbsp;`-._ &nbsp;  `-._ &nbsp;`-./  _.-' &nbsp;  _.-'
 |`-._`-._ &nbsp; &nbsp;`-.__.-' &nbsp;  _.-'_.-'|
 | &nbsp; &nbsp;`-._`-._ &nbsp; &nbsp; &nbsp;  _.-'_.-' &nbsp;  | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; http://redis.io
 &nbsp;`-._ &nbsp;  `-._`-.__.-'_.-' &nbsp;  _.-'
 |`-._`-._ &nbsp; &nbsp;`-.__.-' &nbsp;  _.-'_.-'|
 | &nbsp; &nbsp;`-._`-._ &nbsp; &nbsp; &nbsp;  _.-'_.-' &nbsp;  |
 &nbsp;`-._ &nbsp;  `-._`-.__.-'_.-' &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp;`-._ &nbsp;  `-.__.-' &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;`-._ &nbsp; &nbsp; &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;`-.__.-'
​
3035:X 19 Mar 2021 11:15:31.880 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
3035:X 19 Mar 2021 11:15:31.882 # Sentinel ID is d447823459c2e64cbc19ea9d4f0ceda58f13274d
3035:X 19 Mar 2021 11:15:31.882 # +monitor master myredis 127.0.0.1 6379 quorum 1

这是启动失败的情况,原因是我们在 sentinel 中给他配置密码;如果你得 redis 有密码的化,一定要配置密码

###在sentinel.cong文件中配置如下
sentinel monitor myredis 127.0.0.1 6379 1
sentinel auth-pass myredis liudieyi1206@

修改后启动效果

[root@iZuf643fe16d8rmoz485dgZ bin]# redis-sentinel config/sentinel.conf
3105:X 19 Mar 2021 11:30:23.956 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
3105:X 19 Mar 2021 11:30:23.956 # Redis version=6.2.1, bits=64, commit=00000000, modified=0, pid=3105, just started
3105:X 19 Mar 2021 11:30:23.956 # Configuration loaded
3105:X 19 Mar 2021 11:30:23.956 * monotonic clock: POSIX clock_gettime
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  _._
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.-``__ ''-._
 &nbsp; &nbsp;  _.-`` &nbsp; &nbsp;`.  `_. &nbsp;''-._ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Redis 6.2.1 (00000000/0) 64 bit
  .-`` .-```.  ```\/ &nbsp;  _.,_ ''-._
 ( &nbsp; &nbsp;' &nbsp; &nbsp;  , &nbsp; &nbsp; &nbsp; .-`  | `, &nbsp;  ) &nbsp; &nbsp; Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'| &nbsp; &nbsp; Port: 26379
 | &nbsp; &nbsp;`-._ &nbsp; `._ &nbsp;  / &nbsp; &nbsp; _.-' &nbsp;  | &nbsp; &nbsp; PID: 3105
 &nbsp;`-._ &nbsp;  `-._ &nbsp;`-./  _.-' &nbsp;  _.-'
 |`-._`-._ &nbsp; &nbsp;`-.__.-' &nbsp;  _.-'_.-'|
 | &nbsp; &nbsp;`-._`-._ &nbsp; &nbsp; &nbsp;  _.-'_.-' &nbsp;  | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; http://redis.io
 &nbsp;`-._ &nbsp;  `-._`-.__.-'_.-' &nbsp;  _.-'
 |`-._`-._ &nbsp; &nbsp;`-.__.-' &nbsp;  _.-'_.-'|
 | &nbsp; &nbsp;`-._`-._ &nbsp; &nbsp; &nbsp;  _.-'_.-' &nbsp;  |
 &nbsp;`-._ &nbsp;  `-._`-.__.-'_.-' &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp;`-._ &nbsp;  `-.__.-' &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;`-._ &nbsp; &nbsp; &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;`-.__.-'
​
3105:X 19 Mar 2021 11:30:23.957 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
3105:X 19 Mar 2021 11:30:23.957 # Sentinel ID is d447823459c2e64cbc19ea9d4f0ceda58f13274d
3105:X 19 Mar 2021 11:30:23.957 # +monitor master myredis 127.0.0.1 6379 quorum 1
3105:X 19 Mar 2021 11:30:23.958 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379 &nbsp; &nbsp; #成功显示从机6380
3105:X 19 Mar 2021 11:30:23.960 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379    #成功显示从机6381

3、我们来将 6379 端口的实例关闭,模拟宕机的情况

127.0.0.1:6379> SHUTDOWN
not connected>
[root@iZuf643fe16d8rmoz485dgZ bin]#

过了一会,redis 哨兵就会发出告警然后进行主从切换

[root@iZuf643fe16d8rmoz485dgZ bin]# redis-sentinel config/sentinel.conf
3105:X 19 Mar 2021 11:30:23.956 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
3105:X 19 Mar 2021 11:30:23.956 # Redis version=6.2.1, bits=64, commit=00000000, modified=0, pid=3105, just started
3105:X 19 Mar 2021 11:30:23.956 # Configuration loaded
3105:X 19 Mar 2021 11:30:23.956 * monotonic clock: POSIX clock_gettime
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  _._
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.-``__ ''-._
 &nbsp; &nbsp;  _.-`` &nbsp; &nbsp;`.  `_. &nbsp;''-._ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Redis 6.2.1 (00000000/0) 64 bit
  .-`` .-```.  ```\/ &nbsp;  _.,_ ''-._
 ( &nbsp; &nbsp;' &nbsp; &nbsp;  , &nbsp; &nbsp; &nbsp; .-`  | `, &nbsp;  ) &nbsp; &nbsp; Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'| &nbsp; &nbsp; Port: 26379
 | &nbsp; &nbsp;`-._ &nbsp; `._ &nbsp;  / &nbsp; &nbsp; _.-' &nbsp;  | &nbsp; &nbsp; PID: 3105
 &nbsp;`-._ &nbsp;  `-._ &nbsp;`-./  _.-' &nbsp;  _.-'
 |`-._`-._ &nbsp; &nbsp;`-.__.-' &nbsp;  _.-'_.-'|
 | &nbsp; &nbsp;`-._`-._ &nbsp; &nbsp; &nbsp;  _.-'_.-' &nbsp;  | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; http://redis.io
 &nbsp;`-._ &nbsp;  `-._`-.__.-'_.-' &nbsp;  _.-'
 |`-._`-._ &nbsp; &nbsp;`-.__.-' &nbsp;  _.-'_.-'|
 | &nbsp; &nbsp;`-._`-._ &nbsp; &nbsp; &nbsp;  _.-'_.-' &nbsp;  |
 &nbsp;`-._ &nbsp;  `-._`-.__.-'_.-' &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp;`-._ &nbsp;  `-.__.-' &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;`-._ &nbsp; &nbsp; &nbsp;  _.-'
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;`-.__.-'
​
3105:X 19 Mar 2021 11:30:23.957 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
3105:X 19 Mar 2021 11:30:23.957 # Sentinel ID is d447823459c2e64cbc19ea9d4f0ceda58f13274d
3105:X 19 Mar 2021 11:30:23.957 # +monitor master myredis 127.0.0.1 6379 quorum 1
3105:X 19 Mar 2021 11:30:23.958 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:30:23.960 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.616 # +sdown master myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.617 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
3105:X 19 Mar 2021 11:33:19.617 # +new-epoch 4
3105:X 19 Mar 2021 11:33:19.617 # +try-failover master myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.618 # +vote-for-leader d447823459c2e64cbc19ea9d4f0ceda58f13274d 4
3105:X 19 Mar 2021 11:33:19.618 # +elected-leader master myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.619 # +failover-state-select-slave master myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.674 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.674 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:19.745 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:20.545 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:20.545 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:20.642 * +slave-reconf-sent slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:21.594 * +slave-reconf-inprog slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:21.594 * +slave-reconf-done slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:21.652 # +failover-end master myredis 127.0.0.1 6379
3105:X 19 Mar 2021 11:33:21.652 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6381
3105:X 19 Mar 2021 11:33:21.652 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6381
3105:X 19 Mar 2021 11:33:21.652 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381

通过日志,我们看他将 6381 的实例设置为了主库,我们去 6381 看下信息

127.0.0.1:6381> info replication
# Replication
role:master &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 成功变成了Master
connected_slaves:1 &nbsp; &nbsp; # 有一个从库
slave0:ip=127.0.0.1,port=6380,state=online,offset=15828,lag=0 &nbsp; &nbsp;#从库的地址和端口
master_failover_state:no-failover
master_replid:5dd5040254ab91b6dbcdf85b1780c2f954358397
master_replid2:ca011be19d5a87dcc029b81645ced4536233f37d
master_repl_offset:15828
second_repl_offset:9886
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:15828
127.0.0.1:6381>

通过以上,我们就可知,我们的哨兵就配置成功了

如果此时主机回来了,只能成功 6381 实例的从机

这里记得!!!!我们当时将 6379 实例设置为主机的时候,我们是没有配置他的主机连接密码的;这边哨兵模式 6379 宕机了,重启后如果没有配置主机连接密码是无法重新称为 6381 的从机的!所以配置的时候我建议一定要配置好了!

masterauth liudieyi1206@
####6379实例重新连接上来后,就可以看到6381实例下有两个从机了
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=68477,lag=1
slave1:ip=127.0.0.1,port=6379,state=online,offset=68477,lag=1
master_failover_state:no-failover
master_replid:5dd5040254ab91b6dbcdf85b1780c2f954358397
master_replid2:ca011be19d5a87dcc029b81645ced4536233f37d
master_repl_offset:68609
second_repl_offset:9886
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:68609
1
127.0.0.1:6381>

优点:

1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全由

2、主从可以切换,故障可以转移,系统的可用性会更好

3、哨兵模式就是主从模式的升级,手动到自动,更加健壮

缺点:

1、Redis 不好在线扩容,集群容量一旦到达上线,在线扩容就十分麻烦

2、实现哨兵模式的配置其实很麻烦,里面有很多选择,一般最低是一主一丛,六台 redis

十五、缓存穿透和雪崩

Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最重要的问题就是数据一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,就不能使用缓存

另外一些经典问题就是,缓存穿透,缓存雪崩和缓存击穿。

1、缓存穿透

概念

通俗地说缓存穿透就是用户对不存在的数据发起请求.比如用户请求 id = -1 的数据,缓存中当然不会有这条数据, 那就只能去数据库查找,而数据库也不会有,只能全表扫描. 当用户不断发起该类请求,那就很可能是攻击者, 攻击会导致数据库压力过大.

解决方案:

布隆过滤器

对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;

20200509183125264

2、缓存击穿

概念

一个存在的 key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB,造成瞬时 DB 请求量大、压力骤增。

解决方案:

1、设置热点数据永不过期

从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题

2、加互斥锁

分布式锁:使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到分布式锁,因此对分布式锁的考研很大

在访问 key 之前,采用 SETNX(set if not exists)来设置另一个短期 key 来锁住当前 key 的访问,访问结束再删除该短期 key。

3、缓存雪崩

概念

缓存雪崩是指,由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

20200509183204853

4、高可用

redis 高可用

这个思想的含义是,既然 redis 有可能挂掉,那么我就多增加几台 redis,这样一台挂掉之后其他的还可以继续工,其实就是集群架构(异地多活!)

限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来兑取数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。

数据预热

数据加热的含义就是在正式部署之前,我先把可能得数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

内容大纲
批注笔记
Redis基础教程
ArticleBot
z
z
z
z
主页
Git管理
文章
云文档
留言墙
AI文档