Redis入门到进阶

1.安装

官网下载

image-20200907133410948

解压

tar -zxvf redis-5.0.7.tar.gz

移动

image-20200907133618798

编译 & 安装、

在此之前确定安装了gcc-c++,如果为安装先安装 yum install -y gcc-c++

编译

image-20200907133856278

安装

安装的话,我们现在/usr/local下面创建新的redis安装目录

image-20200907134024031

开始 安装

Redis入门到进阶_第1张图片

此时 redis服务和客户端安装在了/usr/local/redis/bin下面

image-20200907134340299

我们将 配置文件复制到此处

image-20200907134452253

我们可以删除我们编译的 Redis文件夹了

image-20200907134517545

2.单机配置

修改redis.conf

设置后台运行

Redis入门到进阶_第2张图片

将 daemonize no 改为yes

以配置文件形式 运行redis服务

image-20200907134902658

查看 后台运行是否生效

image-20200907135145503

启动redis 客户端

启动redis客户端 测试

Redis入门到进阶_第3张图片

配置完成

3.redis 性能测试

redis中自带了 性能测试工具-- redis-benchmark

image-20200907140343025

性能测试参数

Redis入门到进阶_第4张图片

我们测试一下 100并发连接 每个连接100000 请求数

image-20200907140646790

Redis入门到进阶_第5张图片

4.redis 基础知识

redis 默认 16个数据库

查看配置文件

image-20200907141051766

默认使用的是第 0 个数据库

我们可以使用select来切换数据库

 [root@centos-docker bin]# ./redis-cli  ##进入客户端
127.0.0.1:6379> select 3  ##切换索引为3 的数据库 索引编号默认是 0 - 15
OK
127.0.0.1:6379[3]> DBSIZE  ## 查询当前数据库的大小
(integer) 0
127.0.0.1:6379[3]> 

127.0.0.1:6379[3]> set name xkuna
OK
127.0.0.1:6379[3]> get name
"xkuna"
127.0.0.1:6379[3]> keys *  ##查看所有键
1) "name"
127.0.0.1:6379[3]> flushdb  ## 清空当前库 ;清空所有库 使用flushall
OK
127.0.0.1:6379[3]> keys *
(empty list or set)
127.0.0.1:6379[3]> 

Redis为什么单线程还这么快?

redis的数据是写在内存中的

redis是单线程的,多线程CPU上下文切换比较耗时,对于内存系统来说,如果没有上下文切换效率就是最高的!

5.Redis-Key

127.0.0.1:6379> keys *   #查询当前库下所有的键
1) "counter:__rand_int__"
2) "mylist"
3) "key:__rand_int__"
4) "name"
127.0.0.1:6379> flushall  #清除所有库下的数据
OK
127.0.0.1:6379> keys *   
(empty list or set)
127.0.0.1:6379> set name xkuna  # 添加string 类型 的键,并赋值
OK
127.0.0.1:6379> set age 21 
OK
127.0.0.1:6379> keys *  
1) "age"
2) "name"
127.0.0.1:6379> get name  #获取string 类型 的键的值
"xkuna"
127.0.0.1:6379> move age 1 #将该键 移动到 索引为 1 的 库
(integer) 1
127.0.0.1:6379> keys *    
1) "name"
127.0.0.1:6379> EXISTS name  #查看该键是否存在
(integer) 1
127.0.0.1:6379> EXISTS age    
(integer) 0
127.0.0.1:6379> EXPIRE name 10    #设置该键 的超时时间为 10 s
(integer) 1
127.0.0.1:6379> ttl name  #查看该键剩余的超时时间
(integer) 6
127.0.0.1:6379> ttl name
(integer) 4
127.0.0.1:6379> ttl name
(integer) 4
127.0.0.1:6379> ttl name
(integer) 0
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name hahaha  
OK
127.0.0.1:6379> type name  #查看该键的 类型
string

127.0.0.1:6379> get name
"hahaha"
127.0.0.1:6379> del name #删除指定key
(integer) 1
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> 



6.五种数据类型

String

## string基本命令
127.0.0.1:6379> keys *    #全部key
(empty list or set)
127.0.0.1:6379> set name xkuna #设置值
OK
127.0.0.1:6379> get name   #获取值
"xkuna"
127.0.0.1:6379> APPEND name 2020  #追加值
(integer) 9
127.0.0.1:6379> get name
"xkuna2020"
127.0.0.1:6379> STRLEN name #获取长度
(integer) 9
127.0.0.1:6379>  keys *
1) "name"
127.0.0.1:6379> APPEND aoligei aoligeihahaha #如果不存在该 键  直接创建该键 并且赋值
(integer) 13
127.0.0.1:6379> keys *
1) "name"
2) "aoligei"
127.0.0.1:6379> get aoligei
"aoligeihahaha"
127.0.0.1:6379> 


# 数值 自增 与 自减
127.0.0.1:6379> set age 21  
OK
127.0.0.1:6379> get age
"21"
127.0.0.1:6379> incr age  #自增 1
(integer) 22
127.0.0.1:6379> incr age 
(integer) 23
127.0.0.1:6379> incr age 
(integer) 24
127.0.0.1:6379> get age
"24"


127.0.0.1:6379> decr age #自减 1
(integer) 23
127.0.0.1:6379> decr age
(integer) 22
127.0.0.1:6379> decr age
(integer) 21
127.0.0.1:6379> get age
"21"


127.0.0.1:6379> incrby age 10 #自增 并且设置增加值
(integer) 31
127.0.0.1:6379> get age
"31"

127.0.0.1:6379> decrby age 10 # 自减 并且设置自减值
(integer) 21
127.0.0.1:6379> get age
"21"


#字符串操作
127.0.0.1:6379> set name xkuna
OK
127.0.0.1:6379> getrange name 0 2  #截取字符串 区间[0,2]
"xku"
127.0.0.1:6379> getrange name 0 -1 #截取整个字符串
"xkuna"



127.0.0.1:6379> setrange name 1 ee  # 修改从下标 1 开始的字符
(integer) 5
127.0.0.1:6379> get name
"xeena"
127.0.0.1:6379> 


# set 高阶操作
# setex 在赋值时设置过期时间
# setnx 该键存在 赋值失败, 该键不存在 赋值成功 (分布式锁中会用到)

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> setex name 30 xkuna  #
OK
127.0.0.1:6379> ttl name
(integer) 28
127.0.0.1:6379> ttl name
(integer) 26
127.0.0.1:6379> ttl name
(integer) 26
127.0.0.1:6379> setnx my haha
(integer) 1
127.0.0.1:6379> get my
"haha"
127.0.0.1:6379> setnx my hehe
(integer) 0
127.0.0.1:6379> get my
"haha"



# 多个赋值 mset k v ....
# 多个取值 mget k ....
# msetnx 多个赋值 一个失败 全部失败 
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK

127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 h1 k4 h4 # msetnx 多个赋值 一个失败 全部失败 ,此时 k1 存在
(integer) 0
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
127.0.0.1:6379> 


# 先get后set
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongo
"redis"
127.0.0.1:6379> get db
"mongo"
127.0.0.1:6379> 

List

# lpush 在list 左边 即最 前面添加值 可以一次性添加 一个 或者多个值
# rpush 在list 右边 即最 后面添加值 可以一次性添加 一个 或者多个值
# lrange 查看list 区间内的 值
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> lpush list a
(integer) 1
127.0.0.1:6379> lpush list b
(integer) 2
127.0.0.1:6379> lpush list c
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> rpush list r1
(integer) 4
127.0.0.1:6379> rpush list r2
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "b"
3) "a"
4) "r1"
5) "r2"

# lpop 删除第一个元素
# rpop删除最后一个元素
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "b"
3) "a"
4) "r1"
5) "r2"
127.0.0.1:6379> lpop list  # 删除第一个元素
"c"
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "r1"
4) "r2"
127.0.0.1:6379> rpop list  #删除最后一个元素
"r2"
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "r1"

#lindex 获取指定下标的 元素
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "r1"
127.0.0.1:6379> lindex list 0
"b"
127.0.0.1:6379> lindex list  1
"a"

#llen list长度
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "r1"
127.0.0.1:6379> llen list
(integer) 3


#list 的值 是可以重复的
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "r1"
127.0.0.1:6379> lpush list b
(integer) 4
127.0.0.1:6379> lpush list b
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "b"
3) "b"
4) "a"
5) "r1"

# list 指定删除 几个 相同的 值     lrem [key] [count] [value]
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "b"
3) "b"
4) "a"
5) "r1"
127.0.0.1:6379> lrem list 2 b # lrem [key] [count] [value]
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "r1"
127.0.0.1:6379> 



# ltrim 保留list中的指定区间 元素   ltrim [key] [start] [stop]
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> rpush list hello hello1 hello2 hello3
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim list 1 2
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello1"
2) "hello2"
# 取出 源list 中的最后一个元素 放到 新list的最前面    rpoplpush [sourceList] [newList]

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> llist 1 2 3 4 5
(error) ERR unknown command `llist`, with args beginning with: `1`, `2`, `3`, `4`, `5`, 
127.0.0.1:6379> lpush list  1 2 3 4 5
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> rpoplpush list newList
"1"
127.0.0.1:6379> lrange newList 0 -1
1) "1"
127.0.0.1:6379> 

# 给list 指定 下标更新赋值 如果该list 或者 该list的该下标 不存在 会报错  lset [key] [index] [value]
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> exists list
(integer) 0
127.0.0.1:6379> lset list 0 haha
(error) ERR no such key
127.0.0.1:6379> lpush list hello
(integer) 1
127.0.0.1:6379> lrange list 0 - 1
(error) ERR wrong number of arguments for 'lrange' command
127.0.0.1:6379> lrange list 0 -1
1) "hello"
127.0.0.1:6379> lset list 0 haha
OK
127.0.0.1:6379> lrange list 0 -1
1) "haha"
127.0.0.1:6379> 

# 在list指定 值 的前面 或 后面 添加 值    linsert [key] before|after [value] [newValue]
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> lpush list hello1 hello2
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
2) "hello1"
127.0.0.1:6379> linsert list before hello1 hello3
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
2) "hello3"
3) "hello1"
127.0.0.1:6379> linsert list after hello2 hello4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
2) "hello4"
3) "hello3"
4) "hello1"

set

set中的元素是无序且不可重复的

# sadd [key] [values].... 添加一个或者多个元素
# smembers [key] 读取该set所有的元素
# sismember [key] [value] 判断该set 是否含有该 value 
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> sadd myset a b c d e f g
(integer) 7
127.0.0.1:6379> smembers myset
1) "c"
2) "a"
3) "g"
4) "b"
5) "d"
6) "f"
7) "e"
127.0.0.1:6379> sadd myset a
(integer) 0
127.0.0.1:6379> smembers myset
1) "g"
2) "b"
3) "d"
4) "a"
5) "c"
6) "f"
7) "e"
127.0.0.1:6379> sismember myset a
(integer) 1

# scard [key] 查询set中元素的数量
# srem [key] [value] 删除set中的 值
127.0.0.1:6379> scard myset
(integer) 7
127.0.0.1:6379> srem myset a
(integer) 1
127.0.0.1:6379> smembers myset
1) "g"
2) "b"
3) "d"
4) "c"
5) "f"
6) "e"

#随机抽出 set 中 指定数目的 元素  srandmember [key] [count]
127.0.0.1:6379> srandmember myset 3
1) "g"
2) "b"
3) "d"
127.0.0.1:6379> srandmember myset 3
1) "d"
2) "e"
3) "f"
127.0.0.1:6379> srandmember myset 3
1) "b"
2) "c"
3) "e"


#随机移除 指定个数 的元素   spop [key] [count]
127.0.0.1:6379> smembers myset
1) "g"
2) "b"
3) "d"
4) "c"
5) "f"
6) "e"
127.0.0.1:6379> spop myset 2
1) "f"
2) "e"
127.0.0.1:6379> smembers myset
1) "g"
2) "b"
3) "d"
4) "c"

#smove [source] [newSet] [value] 将源set中的value 放入 另一个 set
127.0.0.1:6379> smembers myset
1) "g"
2) "b"
3) "d"
4) "c"
127.0.0.1:6379> keys *
1) "myset"
127.0.0.1:6379> smove myset myset2 c
(integer) 1
127.0.0.1:6379> keys *
1) "myset"
2) "myset2"
127.0.0.1:6379> smembers myset
1) "g"
2) "b"
3) "d"
127.0.0.1:6379> smembers myset2
1) "c"


# sdiff [k1] [k2]  set集合 k1 中 k2没有的元素
# sinter [k1] [k2] set集合中 k1 和 k2 都有的元素
# sunion [k1] [k2] set集合中 k1 和 k2 全部的元素
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> sadd k1 a b c
(integer) 3
127.0.0.1:6379> sadd k2 c d e
(integer) 3
127.0.0.1:6379> sdiff k1 k2
1) "b"
2) "a"
127.0.0.1:6379> sinter k1 k2
1) "c"
127.0.0.1:6379> sunion k1 k2
1) "d"
2) "a"
3) "c"
4) "b"
5) "e"

hash

hash类型 其实是 key-map 结构

# hset [key] [map-key] [map-value] 添加该键下 一个map中的一对 k v
# hget [key] [map-key]  获取该键下的 该map-key 的map-value
# hmset [key] [map-key] [map-value] [map-key] [map-value] ..... 添加该键下 一个map中的多对 k v
# hmget [key] [map-key] [map-key] ....  获取该键下的 多个map-key 对应的 map-value
# hgetall [key] 获取该键下 所有的 kv 键值对
127.0.0.1:6379> hset user uid 1
(integer) 1
127.0.0.1:6379> hget user uid
"1"
127.0.0.1:6379> hmset user name xuna age 21
OK
127.0.0.1:6379> hmget user name age
1) "xuna"
2) "21"
127.0.0.1:6379> hgetall user
1) "uid"
2) "1"
3) "name"
4) "xuna"
5) "age"
6) "21"


# hlen [key] 该键下 的map 含有多少对 键值对
# hexists [key] [map-key]  该键下 的map 是否含有 该键
# hkeys [key] 该键下 的map中所有的键 
# hvals [key] 该键下 的map中所有的值
127.0.0.1:6379> keys *
1) "user"
127.0.0.1:6379> hgetall user
1) "uid"
2) "1"
3) "name"
4) "xuna"
5) "age"
6) "21"
127.0.0.1:6379> hlen user
(integer) 3
127.0.0.1:6379> hexists user name
(integer) 1
127.0.0.1:6379> hkeys user
1) "uid"
2) "name"
3) "age"
127.0.0.1:6379> hvals user
1) "1"
2) "xuna"
3) "21"


#hincrby [key] [map-key] [map-value] [count] 增加值
#hdecrby [key] [map-key] [map-value] [count] 减少值
#hsetnx [key] [map-key] [map-value] 如果不存在该key 或者 map-key 赋值成功,存在 则 赋值失败
127.0.0.1:6379> hincrby user age 1
(integer) 22
127.0.0.1:6379> hsetnx user name xkunaa
(integer) 0
127.0.0.1:6379> hsetnx user username xkunaa
(integer) 1


hash 适合对象存储, string适合存储 字符串

zset

zset 是 有序且不重复的 set集合

# 添加一个会或者多个 元素   zadd [key] [score] [value] [score] [value] ....
# 按照 下标 查询元素   zrange [key] [index1] [index2]  查询出的元素按照 score 升序排列
# 按照 score 升序 且 符合 socre 区间 查询元素  srangebyscore [key] [min] [max]  必须是先 min 后 max ,因为这是区间
# 负无穷到正无穷 -inf +inf
# (a,b) --- (a (b   [a,b] --- a b    [a,b) a (b  
#  按照 score 升序 且 符合 socre 区间 查询元素和对应的 score       srangebyscore [key] [min] [max] withscores
127.0.0.1:6379> zadd gongzi 2000 zhangsan 5000 wangwu 3000 lisi
(integer) 3
127.0.0.1:6379> zrange gongzi 0 -1 #查询所有 元素
1) "zhangsan"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> zrangebyscore gongzi -inf +inf #查询 score在 正无穷 到 负无穷 的元素
1) "zhangsan"
2) "lisi"
3) "wangwu"

127.0.0.1:6379> zrangebyscore gongzi 2000 3000 # 查询 score在 [2000, 3000] 的元素
1) "zhangsan"
2) "lisi"

127.0.0.1:6379> zrangebyscore gongzi (2000 (5000 # 查询 score在 (2000, 5000) 的元素
1) "lisi"
127.0.0.1:6379> zrangebyscore gongzi (2000 (5000 withscores  # 查询  score在 (2000, 5000) 元素和对应的 score
1) "lisi"
2) "3000"
127.0.0.1:6379> 

# zrem [key] [value] 删除zset中的一个值
# zcard [key] 查询该 zset的大小
# zrevrange [key] [index1] [index2] 按照 下标区间 倒叙输出
# zcount [key] [min] [max]  查询  score在 区间内的 元素数量
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> zadd gongzi 3000 zhangsan 5000 wangwu 4000 lisi
(integer) 3
127.0.0.1:6379> zrange gongzi  0 -1
1) "zhangsan"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> zrem gongzi zhangsan
(integer) 1
127.0.0.1:6379> zrange gongzi  0 -1
1) "lisi"
2) "wangwu"
127.0.0.1:6379> zcard gongzi
(integer) 2
127.0.0.1:6379> zrevrange gongzi 0 -1
1) "wangwu"
2) "lisi"
127.0.0.1:6379> zcount gongzi 1000 3000
(integer) 0
127.0.0.1:6379> zcount gongzi 1000 5000
(integer) 2

7. 三种特殊数据类型

geospatial地理位置详解

geoadd 添加一个 或 多个地理位置信息

# geoadd [key] [经度] [纬度] [名称] 添加地理位置 可以一次性 添加多个

127.0.0.1:6379> geoadd cn:city 116.405285 39.904989 beijing
(integer) 1
127.0.0.1:6379> geoadd cn:city 121.472644 31.231706 shanghai
(integer) 1
127.0.0.1:6379> geoadd cn:city 113.280637 23.125178 guangzhou
(integer) 1
127.0.0.1:6379> 

geopos 获取一个或者多个地理位置信息

# geopos [key] [名称1] [名称2] 获取key中 一个或多个 地理位置信息

127.0.0.1:6379> geopos cn:city beijing 
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
127.0.0.1:6379> geopos cn:city beijing shanghai
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
2) 1) "121.47264629602432251"
   2) "31.23170490709807012"
127.0.0.1:6379> geopos cn:city beijing shanghai guangzhou
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
2) 1) "121.47264629602432251"
   2) "31.23170490709807012"
3) 1) "113.28063815832138062"
   2) "23.12517743834835215"

geodist 获取两个 地理位置之间的距离

单位

  • m 米 (默认)
  • km 千米
  • mi 英里
  • ft 英尺
# geodist [key] [名称1] [名称2] [单位(不写默认为 m)]  获取key中 两个地理位置之间的 直线距离

127.0.0.1:6379> geodist cn:city shanghai beijing
"1067597.9668"
127.0.0.1:6379> geodist cn:city shanghai beijing km
"1067.5980"

georadius 获取以一个 经纬度 为中心的 半径内的 其他元素

# georadius [key] [经度] [纬度] [半径] [单位] [条件...(可省略)] 

127.0.0.1:6379> georadius cn:city 110.21 30.21 1000 km 
1) "guangzhou"
127.0.0.1:6379> georadius cn:city 110.21 30.21 4000 km 
1) "guangzhou"
2) "shanghai"
3) "beijing"

#条件 withcoord 经纬度

127.0.0.1:6379> georadius cn:city 110.21 30.21 4000 km  withcoord
1) 1) "guangzhou"
   2) 1) "113.28063815832138062"
      2) "23.12517743834835215"
2) 1) "shanghai"
   2) 1) "121.47264629602432251"
      2) "31.23170490709807012"
3) 1) "beijing"
   2) 1) "116.40528291463851929"
      2) "39.9049884229125027"

# 条件 withdist       中心的距离
127.0.0.1:6379> georadius cn:city 110.21 30.21 4000 km  withdist
1) 1) "guangzhou"
   2) "844.9321"
2) 1) "shanghai"
   2) "1082.4051"
3) 1) "beijing"
   2) "1216.1386"
# 条件 count [number]     查询数量
127.0.0.1:6379> georadius cn:city 110.21 30.21 4000 km  withdist withcoord count 1
1) 1) "guangzhou"
   2) "844.9321"
   3) 1) "113.28063815832138062"
      2) "23.12517743834835215"


georadiusbymember 获取以一个 地理位置元素 为中心的 半径内的 其他元素

# georadius [key] [地理位置元素] [半径] [单位] [条件...(可省略)] 
127.0.0.1:6379> georadiusbymember cn:city beijing 4000 km  withdist withcoord count 2
1) 1) "beijing"
   2) "0.0000"
   3) 1) "116.40528291463851929"
      2) "39.9049884229125027"
2) 1) "shanghai"
   2) "1067.5980"
   3) 1) "121.47264629602432251"
      2) "31.23170490709807012"

其实 geo 底层就是 zset, 我们可以直接适应 zset 直接操作 geo

127.0.0.1:6379> zrange cn:city 0 -1
1) "guangzhou"
2) "shanghai"
3) "beijing"
127.0.0.1:6379> zrem cn:city beijing
(integer) 1
127.0.0.1:6379> zrange cn:city 0 -1
1) "guangzhou"
2) "shanghai"

hyperloglog 基数统计

什么是基数

集合 A{1 , 3 ,5 ,5}, 那么集合A 基数为 3

基数 是 一个集合中不重复元素的个数(可以理解为 set 的size() ),可以接受误差

用途:

举个栗子:假如我要统计网页的UV(浏览用户数量,一天内同一个用户多次访问只能算一次),传统的解决方案是使用Set来保存用户id,然后统计Set中的元素数量来获取页面UV。但这种方案只能承载少量用户,一旦用户数量大起来就需要消耗大量的空间来存储用户id。我的目的是统计用户数量而不是保存用户,这简直是个吃力不讨好的方案!而使用Redis的HyperLogLog最多需要 12k 就可以统计大量的用户数,尽管它大概有0.81%的错误率,但对于统计UV这种不需要很精确的数据是可以忽略不计的。

# pfadd [key] [value1] [value2] ... 将 多个元素添加到 key中
# pfcount [key] 
# pfmerge [distkey] [sourcekey1] [sourcekey2] ..... 将多个 key中的元素 添加到 新的key中
127.0.0.1:6379> pfadd mykey a b c d a
(integer) 1

127.0.0.1:6379> pfcount mykey  # 判断该 key中的基数
(integer) 4

127.0.0.1:6379> pfadd mykey2 a e f g h
(integer) 1

127.0.0.1:6379> pfcount mykey2
(integer) 5

127.0.0.1:6379> pfmerge mykey4 mykey mykey2
OK

127.0.0.1:6379> pfcount mykey4
(integer) 8

如果允许容错,一定要使用hyperloglog ,否则使用 set 或者自己定义的数据类型

bitmaps 位图场景详解

Redis允许使用二进制数据的Key(binary keys) 和二进制数据的Value(binary values)。Bitmap就是二进制数据的value。Redis的 setbit(key, offset, value)操作对指定的key的value的指定偏移(offset)的位置1或0,时间复杂度是O(1)。

应用场景

统计用户 登录 未登录, 员工打卡 等等,表示两种状态的,都可以使用 bitmaps

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

模拟打卡

0 - 6周一到周日(offset), 0 未打卡, 1打卡 (value)

# setbit [key] [offset] [value]  
# getbit [key] [offset]
# bitcount [key] 获取 value = 1 的数量
127.0.0.1:6379> setbit sign 0 1 
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 0
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
127.0.0.1:6379> getbit sign 1
(integer) 1
127.0.0.1:6379> getbit sign 5
(integer) 0
127.0.0.1:6379> bitcount sign
(integer) 3

8.事务

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

一次性,顺序性,排他性

redis事务没有隔离级别的概念!

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

redis的单条命令是原子性的,但 redis的事务 不是原子性的。

----入队 set set set 执行----

redis的事务:

  • 开启事务(multi)
  • 命令入队(…)
  • 执行事务(exec)

正常执行事务

# multi 开始事务
# 命令入队
# exec 执行事务 
127.0.0.1:6379> multi    # 开始事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec  # 执行事务 
1) OK
2) OK
3) "v1"
4) "v2"

# discard  放弃事务

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get k3 # 事务中的命令队列 都不会执行
(nil)


编译异常(代码 或者 命令 错误,事务中的所有命令都不会执行)

127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v1
QUEUED
127.0.0.1:6379> getset k1  # 命令错误
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors. # 编译异常

运行时异常,在事务队列中存在 语法性 异常, 错误命令抛异常, 其他命令 正常执行

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 
QUEUED
127.0.0.1:6379> set k2 v2 
QUEUED
127.0.0.1:6379> incr k1  # 自增 1
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range #运行时异常
4) "v1"

watch 监视 (可以理解为乐观锁)

在这里我们启用两个客户端 操作我们的redis,客户端 分别命名为 A B

A先执行

127.0.0.1:6379> set money 1000 # A客户端 设置 money为 1000
OK

B再执行

127.0.0.1:6379> watch money # 监控 money
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decrby money 100 # money减 100
QUEUED

A 再执行

127.0.0.1:6379> set money 2000  # B事务未执行前 A修改了money 为 2000
OK

B再执行

127.0.0.1:6379> exec #执行事务 失败, 因为 监视的 money 在事务前后 发生了改变
(nil)

如果此时执行 B 的事务,则需要重新 watch money

B 执行

127.0.0.1:6379> unwatch  # 取消监视
OK
127.0.0.1:6379> watch money # 再次监视 
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> decrby money 100
QUEUED
127.0.0.1:6379> get money
QUEUED 
127.0.0.1:6379> exec # 执行事务
1) (integer) 1900
2) "1900"

9.Jedis

什么是Jedis

jedis是redis官方推荐使用的java连接开发工具

测试

在连接我们的linux redis之前,要做两处修改,使得可以远程连接redis

  • 首先是 开放 6379端口

  • 找到redis的配置文件 redis.confbind 127.0.0.1 改为 0.0.0.0 ,之后重启redis服务

    Redis入门到进阶_第6张图片

然后创建maven项目

依赖

		
		<dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>3.3.0version>
        dependency>

创建测试类

import redis.clients.jedis.Jedis;

/**
 * @author Xkuna
 * @date 2020/9/13 10:51.
 */
public class TestPing {
    public static void main(String[] args) {
        //连接 redis
        Jedis jedis = new Jedis("192.168.52.129", 6379);
        //执行命令
        jedis.set("k1", "v1") ;

        String k1 = jedis.get("k1");

        //关闭连接
        jedis.close();
        
        System.out.println(k1);
    }
}

控制台结果

Redis入门到进阶_第7张图片

redis客户端

127.0.0.1:6379> get k1
"v1"

测试成功

jedis的操作redis方法与 我们redis的命令一致!

事务

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

/**
 * @author Xkuna
 * @date 2020/9/13 11:13.
 */
public class TestTx {
    public static void main(String[] args) {
        //连接 redis
        Jedis jedis = new Jedis("192.168.52.129", 6379);
        //开启事务
        Transaction multi = jedis.multi();

        try{
            // 事务命令 入队
            multi.set("name", "xkuna") ;

            //执行事务 命令
            multi.exec() ;
        }catch (Exception e){
            //执行失败 放弃事务
            multi.discard() ;
        }finally {
            //关闭连接
            jedis.close();
        }
        System.out.println(jedis.get("name"));
    }
}

Redis入门到进阶_第8张图片

10.springboot 整合

说明:在springboot 2.x 之后,原来的jedis被替换成了 lettuce

两者区别:

  • jedis: 采用直连,多个线程操作的话,是不安全的,如果是想要避免不安全的,使用jedis pool 连接池! 类似于BIO模式

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

源码分析

 	@Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
	// 默认的redisTemplate没有过多的设置,redis对象都是需要序列化的
	// 泛型是 ,我们使用 需要强制转换为 类型
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
	//由于 string是redis中最常用的类型,所以单独设置了一个Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

整合测试

pom依赖

	 	<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

配置文件

spring.redis.host=192.168.52.129
spring.redis.port=6379

测试类

package top.xkuna;

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.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class Redis02BootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate ;

    @Test
    void contextLoads() {
        // opsForValue 操作字符串类型
        // opsForList  操作字List类型
        // opsForHash  操作Hash类型
        // opsForSet   操作Set类型
        // opsForZSet  操作Zset类型
        //还有其他直接操作key 以及 事务, 监控 的方法 keys() delete() expire()    multi()  exec()  discard() watch() unwatch()等等
        // 然后再调用相应的命令api, 跟命令相同

        redisTemplate.opsForValue().set("name", "xkuna");

        //如果对数据库进行操作,那么需要获取连接

//        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//        connection.flushDb();
//        connection.flushAll();

    }

}

因为我们直接调用 自带的redisTempalte 没有序列化,所以乱码了,但是 写入redis成功

存储对象

pojo类

package top.xkuna.pojo;

/**
 * @author Xkuna
 * @date 2020/9/13 16:17.
 */
public class User {
    private String name ;
    private int age ;
    
	public User() {}
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

  • 第一种方式: 将对象转为json字符串 存储到redis的string类型中
 	@Test
    public void test() throws JsonProcessingException {
        User user = new User("xkuna", 21) ;
        String s = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user", s);
        Object user1 = redisTemplate.opsForValue().get("user");
        System.out.println(user1);
    }

Redis入门到进阶_第9张图片

  • 第二种方式,序列化对象 直接存储

上面的User类 没有序列化,我们尝试直接存储

 @Test
    public void test() throws JsonProcessingException {
        User user = new User("xkuna", 21) ;
        redisTemplate.opsForValue().set("user", user);
        Object user1 = redisTemplate.opsForValue().get("user");
        System.out.println(user1);
    }

image-20200913162501265

直接报 序列化错误

我们需要 User类实现 序列化–Serializable接口即可

image-20200913162612103

再次运行上面的测试代码

Redis入门到进阶_第10张图片

直接存储成功

自定义redisTeplate(解决序列化)

package top.xkuna.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;

/**
 * @author Xkuna
 * @date 2020/9/13 16:13.
 */
@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        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("xkuna", 21) ;
        redisTemplate.opsForValue().set("user", user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

客户端

127.0.0.1:6379> keys *
1) "user"
127.0.0.1:6379> get user
"[\"top.xkuna.pojo.User\",{\"name\":\"xkuna\",\"age\":21}]"

完美~~

工具类!!!!


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

11.redis.conf详解

单位

Redis入门到进阶_第11张图片

(单位大小写不敏感)

include 包含

Redis入门到进阶_第12张图片

网络

Redis入门到进阶_第13张图片

bind 0.0.0.0  # 绑定 IP

protected-mode yes # 保护模式

port 6379 # 端口设置

通用配置 GENERAL

daemonize yes  # 守护进程(后台) 默认为no 

pidfile /var/run/redis_6379.pid # 如果后台运行,则需要指定一个 pid 文件

# 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 yes # 是否总是显示logo

快照 SNAPSHOTTING

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

redis是内存数据库,如果没有持久化,数据就会断电即失!

save 900 1 # 如果在 900s 内,至少 1 个key进行了操作,那么则会进行持久化操作
save 300 10 # 如果在 300s 内,至少 10 个key进行了操作,那么则会进行持久化操作
save 60 10000 # 如果在 60s 内,至少 10000 个key进行了操作,那么则会进行持久化操作

stop-writes-on-bgsave-error yes # 如果持久化失败 是否关闭持久化

rdbcompression yes # 是否压缩 rdb文件 会消耗 cpu资源

rdbchecksum yes # 是否检查 rdb文件

dir ./  # rdb文件保存的目录

REPLICATION 复制

后面主从复制 再配置

Redis入门到进阶_第14张图片

SECURITY 安全

默认 redis 是没有密码的

Redis入门到进阶_第15张图片

127.0.0.1:6379> config get requirepass # 获取密码
1) "requirepass"
2) ""         # 当前没有设置密码
127.0.0.1:6379> 

设置密码(在客户端内设置时, 重新启动会 失效,建议配置文件内修改)

127.0.0.1:6379> config set requirepass 123456 # 设置密码 为  123456
OK
127.0.0.1:6379> config get requirepass
(error) NOAUTH Authentication required.      # 未认证
127.0.0.1:6379> keys *
(error) NOAUTH Authentication required.      # 未认证
127.0.0.1:6379> auth 123456					# 认证登录 
OK
127.0.0.1:6379> config get requirepass     # 获取密码
1) "requirepass"
2) "123456"                                # 当前密码为 123456

此外 还可以 在配置文件中 设置密码

Redis入门到进阶_第16张图片

此种方式设置密码 需要 重新通过 配置文件启动 redis服务

客户端限制

客户端最大数量

Redis入门到进阶_第17张图片

内存限制

内存最大限制

Redis入门到进阶_第18张图片

内存满了 处理策略

Redis入门到进阶_第19张图片

APPEND ONLY MODE aof配置

appendonly no  # 默认不开启 aof模式


appendfilename "appendonly.aof"  # aof 模式 持久化的文件

# appendfsync always  # 每次修改都会 sync,消耗性能
appendfsync everysec  # 每秒修改都会 sync,容易丢失这 1 秒的数据
# appendfsync no      # 不即时同步,由操作系统控制何时刷写到磁盘上,这种模式速度最快


12.持久化

Redis是内存数据库,如果不将内存中的数据库状态 保存到磁盘中,那么一旦服务器进程退出,服务器中的Redis数据库状态也会消失,所以Redis提供了持久化功能!

RDB(Redis DataBase)

什么是RDB

Redis入门到进阶_第20张图片

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。Redis会单独创建( fork ) 一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是 最后一次持久化后的数据可能丢失。我们默认的就是RDB ,一般情况下不需要修改这个配置!

RDB持久化保存文件是 dump.rdb,在配置文件中的 SNAPSHOTTING(快照配置)可以设置

image-20200914144016069

持久化规则的修改

配置文件中的 SNAPSHOTTING(快照配置)

Redis入门到进阶_第21张图片

dump.rdb文件生成的条件

  • 触发 save 规则(dump文件中存在数据备份)
  • 执行flushall命令(但dump文件中 没有数据)
  • 退出redis(shutdown)

dump.rdb文件的存放位置

配置文件 内查看修改

Redis入门到进阶_第22张图片

默认是 bin目录

如何恢复rbd文件数据

放在配置文件配置好的 存放位置即可,在redis启动时,会自动恢复 RDB文件的数据

绝大多数情况下,RDB默认的配置已经满足需求

优点:

  • 适合大规模数据恢复
  • 对数据的完整性要求不高

缺点:

  • 需要一定的时间间隔进程操作!如果redis宕机了,最后一次修改的数据就没了
  • fork进程运行的时候,会占用一定的内存!

AOF(Append Only File)

原理

将我们的所有命令都记录下来, 恢复的时候就把这个文件全部在执行一遍!

Redis入门到进阶_第23张图片

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

AOF的配置在 配置文件中的 APPEND ONLY MODE部分

AOF 的文件是 appendonly.aof

Redis入门到进阶_第24张图片

AOF开启

image-20200914152354921

此处将no改为yes

测试

127.0.0.1:6379> set name xkuna
OK
127.0.0.1:6379> shutdown 
not connected> exit

此时我们已经关机

我们查看 appendonly.aof文件

vim appendonly.aof

Redis入门到进阶_第25张图片

此时发现,appendonly.aof文件内是我们执行过的命令

我们启动redis查看 数据 是否可以恢复

[root@centos-docker bin] ./redis-server ./redis.conf 
2857:C 14 Sep 2020 20:10:01.873 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2857:C 14 Sep 2020 20:10:01.873 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=2857, just started
2857:C 14 Sep 2020 20:10:01.873 # Configuration loaded
[root@centos-docker bin] ./redis-cli
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"xkuna"

数据恢复了

我们再次将 redis关机,我们手动修改appendonly.aof文件,看 会有什么结果

修改前

Redis入门到进阶_第26张图片

修改后

Redis入门到进阶_第27张图片

我们再次启动redis

[root@centos-docker bin] ./redis-server ./redis.conf 
2864:C 14 Sep 2020 20:11:49.551 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2864:C 14 Sep 2020 20:11:49.551 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=2864, just started
2864:C 14 Sep 2020 20:11:49.551 # Configuration loaded
[root@centos-docker bin] ./redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused    #连接失败
not connected> 

此时因为appendonly.aof文件被修改,redis启动失败

AOF自检工具

为防止上面的情况发生,AOF自带 检测工具 redis-check-aof

image-20200914201554053

输入下面的命令即可 自检恢复

[root@centos-docker bin]# ./redis-check-aof --fix appendonly.aof 
0x              39: Expected prefix '*', got: 's'
AOF analyzed: size=82, ok_up_to=57, diff=25
This will shrink the AOF from 82 bytes, with 25 bytes, to 57 bytes
Continue? [y/N]: y
Successfully truncated AOF

此时我们恢复成功了

我们可以查看下appendonly.aof文件

image-20200914201831676

和修改之前一样,我们再启动redis看是否能正常启动并且数据是否还在

[root@centos-docker bin]# ./redis-server ./redis.conf
2902:C 14 Sep 2020 20:19:35.748 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2902:C 14 Sep 2020 20:19:35.748 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=2902, just started
2902:C 14 Sep 2020 20:19:35.748 # Configuration loaded
[root@centos-docker bin]# ./redis-cli
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"xkuna"

ok~

规则

image-20200914202120318

# appendfsync always  # 每次修改都会 sync,消耗性能
appendfsync everysec  # 每秒修改都会 sync,容易丢失这 1 秒的数据
# appendfsync no      # 不即时同步,由操作系统控制何时刷写到磁盘上,这种模式速度最快

默认是每秒 保存下执行的命令到 appendonly.aof 文件

如果服务器宕机,那么最后一秒的数据可能会丢失

优缺点

优点:

  • 同步的规则保证文件的完整性比RDB更好

缺点:

  • 对于数据文件来说,AOF文件远大于RDB文件,修复的速度也比RBD慢
  • AOF运行效率比RDB慢,所以redis的默认持久化模式就是RDB

总结

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

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

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

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

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

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

5、性能建议

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

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

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

13.Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!

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

订阅/发布消息图:

Redis入门到进阶_第28张图片

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

Redis入门到进阶_第29张图片

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

Redis入门到进阶_第30张图片

命令

这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。

Redis入门到进阶_第31张图片

测试

我们启动两个客户端,一个订阅端,一个发送端

订阅端

127.0.0.1:6379> SUBSCRIBE xkuna  #订阅消息通道 xkuna
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "xkuna"
3) (integer) 1

发送端

127.0.0.1:6379> PUBLISH xkuna hello,redis # 在xkuna消息通道内 发送"hello,redis" 消息
(integer) 1
127.0.0.1:6379> 

查看 订阅端是否接收到消息

image-20200914212706676

原理

Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis的理解。

Redis通过PUBLISH、SUBSCRIBE 和PSUBSCRIBE等命令实现发布和订阅功能。

通过SUBSCRIBE命令订阅某频道后, redis-server 里维护了一个字典,字典的键就是一个个channel , 而字典的值则是一个链
表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定channel的订阅链表中。

通过PUBLISH命令向订阅者发送消息, redis-server 会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这
个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

Pub/Sub从字面上理解就是发布( Publish )与订阅( Subscribe ) , 在Redis中,你可以设定对某一个key值进行消息发布及消息订
阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系
统,比如普通的即时聊天,群聊等功能。

使用场景

1、 实时消息系统!

2、事实聊天! (频道当做聊天室,将信息回显给所有人即可! )

3、 订阅,关注系统都是可以的!

稍微复杂的场景我们就会使用消息中间件MQ

14.搭建集群

概念

主从复制,是指将一台Redis服务器的数据 ,复制到其他的Redis服务器。前者称为主节点(master/leader) ,后者称为从节点.

(slave/follower) ;数据的复制是单向的,只能由主节点到从节点。Master以写为主, Slave 以读为主。

默认情况下,每台Redis服务器都是主节点;且-个主节点可以有多个从节点(或没有从节点) ,但一个从节点只能有一个主节点。

主从复制的作用主要包括:

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

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

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

4、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说 ,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:

1、从结构上,单个Redis服务器会发生单点故障,并且一 台服务器需要处理所有的请求负载,压力较大;

2、从容量上,单个Redis服务器内存容量有限,就算一 台Redis服务器内存容量为256G ,也不能将所有内存用作Redis存储内存,
一般来说 ,单台Redis最大使用内存不应该超过20G。

电商网站上的商品, -般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。

对于这种场景,我们可以使如下这种架构:

Redis入门到进阶_第32张图片

环境配置

只配置从库,不配置主库

我们先启动我们的主库,查看主库信息

127.0.0.1:6379> info replication  # 查看 当前库的信息
# Replication
role:master  # 角色 master
connected_slaves:0 # 没有从机
master_replid:9de388bc2ead191118f3b5f6dff61fe85af60120
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

开始配置从库,此时我们启需要配置2个 redis作为从机,先关闭主机

主机端口 6379, 从机端口 6380, 6381

将三个配置文件 copy到新的文件夹中

[root@centos-docker bin]# mkdir cluster
[root@centos-docker bin]# cp redis.conf ./cluster/redis-6379.conf
[root@centos-docker bin]# cp redis.conf ./cluster/redis-6380.conf
[root@centos-docker bin]# cp redis.conf ./cluster/redis-6381.conf
[root@centos-docker bin]# cd cluster/
[root@centos-docker cluster]# ls
redis-6379.conf  redis-6380.conf  redis-6381.conf

修改 配置文件

修改 6379配置文件

port 6379  # 端口
pidfile /var/run/redis_6379.pid # pid文件
logfile "redis-6379.log"  # 日志文件名称
dbfilename dump-6379.rdb # dump文件名称

6380 6381 配置文件 只需将上面修改位置的6379 分别替换为6380 和 6381 即可

分别启动 三个 redis

[root@centos-docker cluster]# cd ../
[root@centos-docker bin]# ./redis-server cluster/redis-6379.conf 
[root@centos-docker bin]# ./redis-server cluster/redis-6380.conf 
[root@centos-docker bin]# ./redis-server cluster/redis-6381.conf 
[root@centos-docker bin]# ps -ef | grep redis
root       3583      1  0 01:19 ?        00:00:00 ./redis-server 0.0.0.0:6379
root       3588      1  0 01:20 ?        00:00:00 ./redis-server 0.0.0.0:6380
root       3593      1  0 01:20 ?        00:00:00 ./redis-server 0.0.0.0:6381
root       3598   1822  0 01:20 pts/1    00:00:00 grep --color=auto redis

一主二从

默认情况下,每台Redis服务器都是主节点,我们一般情况下,只用配置从机就好了!

认老大(6379老大,6380 6381 小弟)

配置6380

127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> 

配置6381

127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> 

查看 6379 主从信息

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2  # 两台从机
slave0:ip=127.0.0.1,port=6380,state=online,offset=84,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=84,lag=0
master_replid:57bfe8e6ebfd266ab1af6a9729112066df146ae6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:84
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:84

但是!在客户端用命令配置的主从关系,重启失效,只有在配置文件中配置才会永久生效

在配置文件的REPLICATION模块配置

Redis入门到进阶_第33张图片

注意

主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!

测试:主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!

复制原理

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

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

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

增量复制: Master继续将新的所有收集到的修改命令依次传给slave ,完成同步

但是只要是重新连接master , 一次完全同步(全量复制)将被自动执行I

链路模型

Redis入门到进阶_第34张图片

这种模型也可以完成主从复制

主机宕机怎么办?

谋朝篡位

在哨兵模式之前,主机宕机之后,都是手动修改主从关系,比如,从机改为主机 ,命令为 slaveof no one,此时从机就从slave变为master了。如果原来的主机正常启动了,没有修改主从关系,那么 从机 会重新连接到主机。

哨兵模式

(自动选举)

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一-种推荐的方式 ,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。

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

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

Redis入门到进阶_第35张图片

这里的哨兵有两个作用:

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

Redis入门到进阶_第36张图片

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

测试

一般情况下,每个redis都要配置一个sentinel,在这里三台redis只配置了一台sentinel(服务器 配置不

我们新创建一个 sentinel_conf文件夹 存放sentinel的配置文件

编写sentinel-6379.conf配置文件

下面是简单配置

port 26379  # sentinel端口
daemonize yes # 是否后台运行
sentinel monitor mymaster 127.0.0.1 6379 1 # 监控的redis信息
## 其他的配置 在启动sentinel之后 自动按照 默认配置 写入到该文件

启动sentinel

[root@centos-docker bin]# ./redis-sentinel sentinel_conf/sentinel-6379.conf 
1856:X 15 Sep 2020 10:55:46.229 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1856:X 15 Sep 2020 10:55:46.229 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=1856, just started
1856:X 15 Sep 2020 10:55:46.229 # Configuration loaded

登录sentinel 并查看 监控信息

[root@centos-docker bin]# ./redis-cli -p 26379
127.0.0.1:26379> sentinel master mymaster  # 查看监控主机名为  mymaster 的信息
 1) "name"
 2) "mymaster"
 3) "ip"
 4) "127.0.0.1"
 5) "port"
 6) "6379"
 7) "runid"
 8) "461c856fa56b91113b2ded3038166913a8ce3271"
 9) "flags"
10) "master"
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "397"
19) "last-ping-reply"
20) "397"
21) "down-after-milliseconds"
22) "30000"
23) "info-refresh"
24) "7240"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "77452"
29) "config-epoch"
30) "0"
31) "num-slaves"
32) "2"
33) "num-other-sentinels"
34) "0"
35) "quorum"
36) "1"
37) "failover-timeout"
38) "180000"
39) "parallel-syncs"
40) "1"

此时我们把 6379的 主机下线

[root@centos-docker bin]# ./redis-cli -p 6379
127.0.0.1:6379> shutdown 
not connected> exit

查看 6380 6381 信息

# 6380
[root@centos-docker bin]# ./redis-cli -p 6380
127.0.0.1:6380> info replication
# Replication
role:slave  # 角色为从机
master_host:127.0.0.1
master_port:6381
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:36435
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:ee296f79ccb66c1a0e0c56dc15cad8d0f1896d06
master_replid2:fa6f421dce64972a6b8fe40739865eee4a08e9a1
master_repl_offset:36435
second_repl_offset:34922
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:36435




# 6381

[root@centos-docker bin]# ./redis-cli -p 6381
127.0.0.1:6381> info replication
# Replication
role:master  # 角色为 主机
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=36981,lag=1
master_replid:ee296f79ccb66c1a0e0c56dc15cad8d0f1896d06
master_replid2:fa6f421dce64972a6b8fe40739865eee4a08e9a1
master_repl_offset:37114
second_repl_offset:34922
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:37114
127.0.0.1:6381> 


那么 sentinel 自动选举成功了,6381变为了主机

注意!此时6379再次上线,那么它将变为6381的从机!

优缺点

优点:

  • 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
  • 主从可以切换,故障可以转移,系统的可用性就会更好
  • 哨兵模式就是主从模式的升级,手动到自动,更加健壮!

缺点:

  • Redis不好在线扩容的.集群容量- -旦到达上限,在线扩容就十分麻烦!
  • 实现哨兵模式的配置其实是很麻烦的。里面有很多选择!

sentinel全部配置

#sentinel 配置

#端口
port 26379

#目录
dir /tmp

#日志文件
logfile /var/log/redis/redis-sentinel.log

#是否在后台执行,yes:后台运行;no:不是后台运行
daemonize yes

bind 127.0.0.1   #监听的ip地址,根据节点不同进行调整


# sentinel auth-pass  
#设置连接master和slave时的密码,注意的是sentinel不能分别为master和slave设置不同的密码,因此master和slave的密码应该设置相同。
# 配置示例:
sentinel auth-pass mymaster 0123passw0rd


#是否开启保护模式,默认开启。开启后,只能根据配置的bind地址和密码进行访问。
protected-mode no


#主节点信息,格式:sentinel    
# 自定义主节点名称;
#  主节点的ip和端口;
# 多少个主节点检测到主节点有问题就进行故障转移
sentinel monitor mymaster 127.0.0.1 6379 1

#sentinel与master的心跳时间(毫秒),默认30秒。
sentinel down-after-milliseconds mymaster 30000

#故障转移时,最多可以有多少个slave同时对新的master进行数据同步,该值越小,完成故障转移的时间越长,但可用slave数量越多,该值越大,越多slave因为replication而不可用。建议设置为1。
sentinel parallel-syncs mymaster 1

#故障转移超时时间(毫秒),默认180秒。
sentinel failover-timeout mymaster 180000

#master和slaves密码。
#sentinel auth-pass mymaster password

#当sentinel有警告级别的事件发生时执行(也有的资料说failover时触发)的脚本。
#sentinel notification-script  

#故障转移之后执行的脚本,并传递7个参数:      
# 表示 master名字
# 表示的是 每个redis实力的角色,如leader、observer
# 表示状态
# 原来的redis master
# 
# 故障迁移后的redis master
#
#sentinel client-reconfig-script  

15.缓存穿透和雪崩

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

另外的一-些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

Redis入门到进阶_第37张图片

缓存穿透(查不到)

概念

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀! ) , 于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

解决方案

布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;

Redis入门到进阶_第38张图片

存储空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间 ,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

Redis入门到进阶_第39张图片

但是这种方法会存在两个问题:

  • 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
  • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不- 致,这对于需要保持一致性的业务会有影响。

缓存击穿(并发大,缓存过期)

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一-个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一 个屏障上凿开了一一个洞。当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据 ,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期

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

加互斥锁

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

Redis入门到进阶_第40张图片

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!

产生雪崩的原因之一 ,比如在写本文的时候,马上就要到双十二零点,很快就会迎来-波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候 ,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

Redis入门到进阶_第41张图片

解决方案

redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis ,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

限流降级

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

数据预热

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

你可能感兴趣的:(redis,微服务,redis,分布式,分布式存储,数据库)