Redis学习笔记

1、NoSQL

1.1、什么是NoSQL

相对于传统的关系型数据库(MySQL、Oracle等)的行列模式,在大数据时代(web2.0,尤其是超大规模的高并发社区)很难正常运行,所以产生了NoSQL的一种数据库用来存储访问量比较高的数据,常见的NoSQL数据库有MongoDB、Redis等。

NoSQL=> Not Only SQL(不仅仅是SQL)

泛指非关系型数据库,NoSQL在当前的大数据环境下发展的十分迅速,而Redis是发展最快的,也是当下需要掌握的技术。
很多数据类型比如用户个人信息,社交网络,地理位置等,这些数据存储不需要像关系型数据库那样需要一个固定的格式,不需要多余的操作就可以横向(集群)扩展的,其实在Java中有一个东西几乎可以存储万事万物,就是Map<String,Object>,其实这就是一种典型的NoSQL的体现使用键值对来管理数据

1.2、NoSQL的特点

解耦

1、方便扩展(数据之间没有关系,可以很好的扩展)
2、大数据量高性能(Redis 1s可以写8W次,可以读11W次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
3、数据类型是多样型的(不需要事先设计数据库,随取随用,如果是数据量十分大的表,很多人就无法去设计了)
4、传统RDBMS和NoSQL的区别

传统的RDBMS

  • 结构化存储
  • 固定化的查询语法SQL
  • 数据和关系都存储在单独的表中
  • 数据操作语言,数据定义语言
  • 基础的事务
  • ...

NoSQL

  • 不仅仅是SQL
  • 没有固定的查询语言
  • 键值对存储、列存储、文档存储(MongoDB)、图形数据库(社交关系拓扑图)等
  • 最终一致性
  • CAP定理 和 BASE定理 (异地多活!一个服务器崩了,其他服务器仍可以继续工作)
  • 高性能、高可用、高可扩展
  • ...

1.2.1、了解3V和3高

大数据时代的3V:主要是用来描述问题的

  1. 海量Voulme(用户多)
  2. 多样Varierty(每种数据都是不一样的)
  3. 实时Velocity(实时性,人是很难接受延迟的,要保证实时性)

大数据时代的3高:主要是对程序的要求

  1. 高并发
  2. 高可扩(扩展性必须要高,可以随时扩展机器)
  3. 高性能(保证用户体验和性能)

虽然比较的NoSQL和关系型数据库的区别,但是现在在公司中需要将NoSQL和关系型数据库结合使用

1.3、NoSQL四大分类

KV键值对:

  • 新浪:Redis
  • 美团:Redis+Tair
  • 阿里、百度:Redis+MemeCache

文档型数据库(bson和json一样):

  • MongoDB(一般必须要掌握):是NoSQL中功能最丰富,最像关系型数据库的中间产品

列存储

  • HBase
  • 分布式文件系统

图存储

  • 拓扑图,并不是用来存储图片的,是用来存储关系的(社交网络,广告推荐)
  • Neo4j,InfoGrid
分类 举例 应用场景 数据模型 优点 缺点
键值对 Redis 内容缓存,主要用于处理大量数据的高访问负载,也可用于一些日志系统等 key指向value的键值对,通常用hash table来实现 查找速度快 数据无结构化,通常只被当做字符串或者二进制数据
列存储 HBase 分布式的文件系统 以列簇式存储,将同一列的数据存在一起 查找速度快,可扩展性强,更容易实现分布式扩展 功能相对局限
文档型 MongoDB web应用 k-v键值对,Value为结构化数据 数据结构不严格,不需要先定义表结构 查询性能不高,而且缺乏统一的查询语言
图形存储 Neo4J 社交网络,推荐系统等,专注于构建关系图 拓扑图 利用图结构相关算法,比如最短路径寻址,N度关系查找等 很多时候需要对整个图做计算才能得出想要的信息,这种模式不好做集群分布式

2、Redis入门

2.1、概述

Redis是什么

Redis(Remote Dicionary Server),远程字典服务!
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
免费开源,是当下最热门的NoSQL技术之一!也被人们称为结构化数据库

Redis能干嘛?

  1. 内存存储、持久化,内存中是断电即失,所以持久化是很重要的(rdb、aof)
  2. 效率高,可以用于高速缓存
  3. 发布订阅系统,可以用来做简单的消息队列
  4. 地图信息分析
  5. 计时器、计数器(浏览量!)
  6. ...

Redis的特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务
  5. ...

学习中需要用到的东西

  1. Redis官网
  2. Redis中文网
  3. linux下载地址
  4. Windows下载地址(停更很久了,官方不建议使用windows开发)

常见的数据类型

  • String: 字符串
  • Hash: 散列
  • List: 列表
  • Set: 集合
  • Sorted Set: 有序集合

2.2、安装

PS:因为版本更新问题,在安装的时候需要将版本修改为自己下载的版本

2.2.1、Windows下安装

1、下载安装包
2、将解压好的压缩包解压到磁盘
image
3、文件内容
image
4、开启Redis,双击运行服务即可,服务端不能关闭,默认端口号6379
image
5、开启客户端,使用Redis
image
6、使用Redis
image
虽然Windows下使用Redis确实很简单,但是Redis官方推荐使用Linux开发

2.2.2、Linux下安装

使用的远程工具是Tabby
1、下载安装包
image

2、将下载好的安装包通过sftp传输到linux系统中,个人使用的是Tabby,在opt目录下新建文件夹redis,将下载好的压缩包上传到redis文件夹中
image
3、将上传的文件进行解压缩

> tar -zxvf redis-7.0.5.tar.gz

image
4、进入解压后的文件,可以看到redis的配置文件
image
5、环境安装

> yum install -y gcc-c++
> make #对所有的环境进行安装  此过程需要一些时间

image

> make install

image
6、redis默认的安装路径在/usr/local/bin/
image
7、将redis配置文件复制到当前目录下,以后就修改config下的配置文件就可以了,出问题之后可以重新复制

> mkdir config
>  cp /opt/redis/redis-7.0.5/redis.conf config/

image
8、redis默认不是从后台启动的,需要修改配置文件
image
9、启动redis服务!在启动的时候需要绑定配置文件

> redis-server config/redis.conf

image
10、使用redis客户端,连接redis服务

> redis-cli -h ip地址 -p 6379  #因为ip地址还是127.0.0.1即本机,所以-h ip地址可以不用写即
> redis-cli -p 6379

image
11、查看redis 的进程是否开启
需要重新打开一个Dos窗口

> ps -ef|grep redis

image
12、如何关闭redis服务shutdown

> shutdown #在客户端内输入
> exit

image
再去查看redis的进程是否存在
image

2.3、测试性能

redis-benchmark是一个压力测试工具
官方自带的性能测试工具
redis-benchmark 命令参数!
下图截取自菜鸟教程
image

来简单的测试一下,也可以按照上图参数自行进行性能测试

# 测试:100个并发连接,100000请求
> redis-benchmark -h localhost -p 6379 -c 100 -n 100000

截取一部分数据结果,其他的性能和SET的类似
image
image

2.4、基础知识

redis默认有16个数据库
image
默认使用的是第0个数据库,可以使用select进行切换数据库

127.0.0.1:6379> select 3  #选择数据库
OK
127.0.0.1:6379[3]> dbsize  #查看数据库大小
(integer) 0

image

127.0.0.1:6379[3]> keys *  #查看数据库中所有的key
1) "name"

清空数据库flushdb
清空全部数据库flushall

127.0.0.1:6379[3]> flushdb #清空当前数据库
OK
127.0.0.1:6379[3]> keys *
(empty array)
127.0.0.1:6379[3]> flushall #清空所有的数据库
OK

image

redis是单线程的

因为Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了

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

Redis是C语言写的,官方提供的数据为100000+的QPS,完全不比使用key-value的Memecache差!

  1. 误区1:高性能的服务器一定是多线程的
  2. 误区2:多线程(CPU上下文会切换 这个是耗时的操作)一定比单线程效率高

核心:redis是将所有的数据放在内存中的,所以说使用单线程操作效率就是最高的,对于内存来说,如果没有上下文切换,则效率就是最高的!多次读写都是在一个CPU上实现的,在内存里边,这个就是最佳的方案

2.5、Redis的数据类型

在Redis中常见的数据类型有8种,5种常用的,3种特殊的

官网文档

image

全段翻译:

Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库高速缓存消息队列(MQ)代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

2.5.1、Redis-Key

常用的命令,如果想查看其他命令的话可以从官网去查看,对应的是中文目录

命令 作用
set key value 向Redis数据库中使用key-value插入数据
get key 通过key从Redis中取出value值
keys * 查询当前数据库中有多少key,并列出来
select 0~15 在Redis中默认有16个数据库,下标对应的是0~15,选择对应的数据库
dbsize 返回当前数据库中有多少键值对
exists key 校验在当前库中该key是否存在,存在返回1,否则返回0
flushdb 清空当前数据库
flushall 清空所有数据库
remove key 0~15 将当前库中的key移动到其他库中
expire key time 设置key的过期时间,单位是秒
ttl key 查看key的剩余过期时间,如果key过期之后返回-2
type key 查看key的类型
127.0.0.1:6379> keys * # 查看当前数据库中所有的key
(empty array)
127.0.0.1:6379> set name porterdong # 存储一个key值
OK
127.0.0.1:6379> dbsize # 查看当前数据库的大小
(integer) 1
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> dbsize
(integer) 2
127.0.0.1:6379> exists name # 检测该key是否存在,如果返回1表示存在,0表示不存在
(integer) 1
127.0.0.1:6379> exists age
(integer) 1
127.0.0.1:6379> move name 1 # 将该key移动到下标为1的数据库
(integer) 1
127.0.0.1:6379> exists name
(integer) 0
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> exists name
(integer) 1
127.0.0.1:6379> set name porterdong
OK
127.0.0.1:6379> expire name 10 # expire设置当前key的过期时间,单位是秒(热点数据,用户的cookie,单点登录可以使用这个来做)
(integer) 1
127.0.0.1:6379> ttl name # 查看key的剩余过期时间,单位是秒,如果key过期之后ttl key返回的是-2
(integer) 8
127.0.0.1:6379> type age # type返回key的类型
string

2.6、五大基本数据类型

只是列举了常见的命令,其他命令可以从官网查看

2.6.1、String(字符串类型)

命令 作用
strlen key 返回字符串的长度
append key value 将value值拼接到key值的后边,如果key不存在,相当于set,返回新字符串的长度
127.0.0.1:6379> set name zhangsan #存值
OK 
127.0.0.1:6379> get name # 取值
"zhangsan"
127.0.0.1:6379> keys * # 查看所有的key
1) "name"
127.0.0.1:6379> append name porterdong # 对String类型的数据做拼接
(integer) 18 # 返回的是新字符串的长度
127.0.0.1:6379> get name
"zhangsanporterdong"
127.0.0.1:6379> append name "nihao"
(integer) 23
127.0.0.1:6379> get name
"zhangsanporterdongnihao"
127.0.0.1:6379> strlen name # 返回该key所对应的value的长度
(integer) 23
127.0.0.1:6379> append age 20 # 如果拼接的key值不存在,相当于set
(integer) 2
127.0.0.1:6379> keys *
1) "age"
2) "name"
命令 作用
incr key 对value值做自增的操作+1(类似于文章的观看)
decr key 对value值做自减的操作-1
incrby key num 对value值做步长增长操作
decrby key num 对value值做步长减小操作
127.0.0.1:6379> set size 1
OK
127.0.0.1:6379> incr size # 对value值做自增
(integer) 2
127.0.0.1:6379> incr size
(integer) 3
127.0.0.1:6379> get size
"3"
127.0.0.1:6379> decr size # 对value值做自减
(integer) 2
127.0.0.1:6379> get size
"2"
127.0.0.1:6379> incrby size 10 # 对vlaue值做步长增长 相当于java中的i+=
(integer) 12
127.0.0.1:6379> incrby size 10
(integer) 22
127.0.0.1:6379> get size
"22"
127.0.0.1:6379> decrby size 5 # 对vlaue值做步长减小 相当于java中的i-=
(integer) 17
127.0.0.1:6379> get size
"17"
命令 作用
getrange key start end 获取·value值中下标从start开始到end的字符串,如果end为-1则返回从start开始到结尾的全部字符串,start不能为负数,会返回空值
setrange key offset val 将下标为offset的字符以及后边的字符替换成val,具体替换的长度取决于val的长度
127.0.0.1:6379> set name hello,porterdong
OK
127.0.0.1:6379> getrange name 0 3 # 截取字符串,从下标0开始截取到下标为3的位置
"hell"
127.0.0.1:6379> getrange name 0 -1 # 在截取字符串的时候如果使用了-1则会返回从start开始到结尾全部的字符串
"hello,porterdong"
127.0.0.1:6379> set str abcdefg
OK
127.0.0.1:6379> get str
"abcdefg"
127.0.0.1:6379> setrange str 2 xx # 将下标为2的位置的以后的字符进行修改,修改的长度取决于修改后的值
(integer) 7
127.0.0.1:6379> get str
"abxxefg"
命令 作用
setex key time value 创建一个键值对,过期时间为times(set with expire)
setnx key value 判断key是否存在,如果不存在则创建,否则不会创建,在分布式锁中会经常使用(set if not exist)
127.0.0.1:6379> setex key1 30 porterdong # 在设置键值对的之后设置过期时间
OK
127.0.0.1:6379> ttl key1
(integer) 27
127.0.0.1:6379> setnx key2 porterdong # 如果key不存在则创建k-v,如果k存在不会有任何操作
(integer) 1
127.0.0.1:6379> keys *
1) "key2"
2) "str"
3) "name"
127.0.0.1:6379> setnx key2 zhangsan
(integer) 0
127.0.0.1:6379> get key2
"porterdong"
命令 作用
mset k1 v1 k2 v2 k3 v3 ... 同时设置多个k-v键值对
mget k1 k2 k3 ... 同时读取多个值
msetnx k1 v1 k2 v2 k3 v3 ... 同时设置多个值,如果有一组数据重复,则全部不会进行添加原子性的操作
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3 # 同时读取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # 同时设置多个值,如果有一组重复则全部添加失败
(integer) 0
127.0.0.1:6379> get k4
(nil)

进阶用法:存储json对象

命令 作用
set key 用来设置json格式的对象
set key:{id}:{filed} value 用来设置k-v,但是这种方式的可复用性非常高,比如在文件浏览的地方,使用这种方式只需要将文章编号传递进去就可以了
mset key1 {k1:v1,k2:v2,...} key2 {k1:v1,k2:v2,...} ... 用来设置多个json格式的对象
127.0.0.1:6379> set user:1 {name:zhangsan,age:5} # 设置一个json格式的对象  user:1还是key {}是value 
OK
127.0.0.1:6379> get user:1
"{name:zhangsan,age:5}"
127.0.0.1:6379> mset user:2 {name:lisi,age:10} user:3 {name:wangwu,age:20} # 设置多个json格式的对象
OK
127.0.0.1:6379> keys *
1) "user:1"
2) "user:2"
3) "user:3"
127.0.0.1:6379> get user:2
"{name:lisi,age:10}"
127.0.0.1:6379> get user:3
"{name:wangwu,age:20}"
#  另外一种方式  这里的key是一个巧妙地设计 user:{id}:{filed}  在redis中这样设计是可以的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
命令 作用
getset key value 取出值然后进行修改,如果key不存在则相当于set,返回的结果类似get
127.0.0.1:6379> getset db redis # 将db命令取出来进行修改,因为一开始不存在所以返回get查询的结果nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb # 将db命令取出来,因为已经执行一次getset了,相当于创建,所以返回的结果是get的结果,并修改新的值
"redis"
127.0.0.1:6379> get db
"mongodb"

String类型的使用场景:value除了是字符串还可以是数字

  • 计数器
  • 统计多单位的数量,比如粉丝数,文章阅读数
  • 对象存储缓存

2.6.2、List

基本的数据类型,列表类型的存储
在Redis中,可以把List当成栈(先进先出)、队列(先进后出)、阻塞队列(两头都可以进入)去使用
值的注意的是,List所有的命令都是L开头的

命令 作用
lpush list value [value1 value2 value3] 将一个或者多个value的值存入列表的头部(左侧)
lrange list start end 从列表中取出值,如果end为-1则返回从start开始到结尾的全部值,start不能为负数,会返回空,在list中存储数据采用的是先进后出的形式
rpush list value [value1 value2 value3] 将一个或者多个value的值存入列表的尾部(右侧)

image

127.0.0.1:6379> lpush list one # 向list头部(左侧)添加一条数据
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 从list中读取所有数据
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1
1) "three"
2) "two"
127.0.0.1:6379> lpush list four five six
(integer) 6
127.0.0.1:6379> lrange list 0 -1 # 向列表头部(左侧)添加多个数据
1) "six"
2) "five"
3) "four"
4) "three"
5) "two"
6) "one"
127.0.0.1:6379> rpush list seven eight # 向列表尾部(右侧)添加多个数据
(integer) 8
127.0.0.1:6379> lrange list 0 -1
1) "six"
2) "five"
3) "four"
4) "three"
5) "two"
6) "one"
7) "seven"
8) "eight"
命令 作用
lpop key 移除列表头部(左侧)的元素
rpop key 移除列表尾部(右侧)的元素
127.0.0.1:6379> lrange list 0 -1
1) "six"
2) "five"
3) "four"
4) "three"
5) "two"
6) "one"
7) "seven"
8) "eight"
127.0.0.1:6379> lpop list # 移除list列表中头部(左侧)的元素
"six"
127.0.0.1:6379> rpop list # 移除list列表中尾部(右侧)的元素
"eight"
127.0.0.1:6379> lrange list 0 -1
1) "five"
2) "four"
3) "three"
4) "two"
5) "one"
6) "seven"
命令 作用
lindex key index 通过下标查找list列表中该位置的元素
llen key 查询列表中一共有多少个元素
127.0.0.1:6379> lrange list 0 -1
1) "five"
2) "four"
3) "three"
4) "two"
5) "one"
6) "seven"
127.0.0.1:6379> lindex list 0 # 查找列表中0位置的元素
"five"
127.0.0.1:6379> llen list # 查看列表中一共有多少个元素
(integer) 6
命令 作用
lrem key count value 从列表中移除count个value,如果列表中value的值个数比count大,从左往右依次移除
ltrim list start end 只保留列表中start开始到end结束的元素,其他元素删掉

lrem常用的地方就是关注列表取关,因为关注列表中用户的id是确定的,所以只需要移除一个就好了

127.0.0.1:6379> lpush list one two three three # 向列表中添加多个元素
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one # 移除列表中的一个值为one的元素
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 1 three # 移除列表中从左往右第一个three元素
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrem list 2 three # 移除列表中2个three元素
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"

127.0.0.1:6379> rpush mylist hello0 hello1 hello2 hello3
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello0"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim mylist 1 2 # 将mylist中下标1到2的其他元素删掉
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
命令 作用
rpoplpush key key1 将key列表中的最后(右)一个元素挪到key1列表的左边(头部),如果key1不存在则会创建
127.0.0.1:6379> rpush mylist hello0 hello1 hello2
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello0"
2) "hello1"
3) "hello2"
127.0.0.1:6379> rpoplpush mylist myotherlist # 将mylist列表中的最后的元素移到myotherlist中
"hello2"
127.0.0.1:6379> lrang mylist 0 -1
127.0.0.1:6379> lrange mylist 0 -1 # 查看原来的列表
1) "hello0"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1 # 查看新的列表
1) "hello2"
命令 作用
lset key index value 将列表key中下标为index位置的元素修改为value,如果key不存在返回错误,如果对应的下标index不存在也会返回错误
127.0.0.1:6379> exists list # 判断列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 修改列表中的某个值,如果不存在返回错误
(error) ERR no such key
127.0.0.1:6379> lpush list redis
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "redis"
127.0.0.1:6379> lset list 0 item # 将列表list中第一个元素修改为item
OK
127.0.0.1:6379> lrange list 0 0
1) "item"
127.0.0.1:6379> lset list 1 zhangsan # 修改了不存在位置的元素,也会返回错误
(error) ERR index out of range
命令 作用
linsert key before/after pivot element 向列表key中pivot元素的前(左)/后(右)边插入元素element
127.0.0.1:6379> rpush list hello world
(integer) 2
127.0.0.1:6379> linsert list after world other # 向列表中world的后(右)边插入元素other
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
3) "other"
127.0.0.1:6379> linsert list before world , # 向列表world的前(左)边插入元素,
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) ","
3) "world"
4) "other"

小结:

  • 它实际上是一个链表,相当于LinkedList的一个Node节点,有三个属性before item after,left,right都可以插入值
  • 如果key不存在,创建新的列表
  • 如果key存在,新增内容
  • 如果移除了所有的值,空链表,也代表不存在
  • 在两边插入值,或者改动值,效率最高。如果链表过长,修改中间值,相对来说效率会低一点

List类型的使用场景:
消息排队,消息队列(lpush rpop),栈(lpush lpop)

2.6.3、Set(集合)

set中的值是无序不重复的
值得注意的是set中所有的命令都是s开头的

命令 作用
sadd key val [val1 val2 val3 ...] 向set集合中添加一个或多个元素,如果数据和集合中的数据不重复返回成功插入的数量
smembers key 返回set集合中的所有元素
sismember key value 判断value是否是集合中的元素,如果是返回1,如果不是返回0
scard key 获取集合中元素的个数
srem key val val1 val2 ... 移除key集合中的一个或者多个元素
127.0.0.1:6379> sadd myset hello # 向set集合中添加一个元素
(integer) 1
127.0.0.1:6379> sadd myset porter dong # 向set集合中添加多个元素
(integer) 2
127.0.0.1:6379> smembers myset # 返回set集合中所有元素的信息
1) "dong"
2) "porter"
3) "hello"
127.0.0.1:6379> sismember myset hello # 判断hello是否是set集合的元素
(integer) 1
127.0.0.1:6379> sismember myset world # 判断world是否是set集合的元素
(integer) 0
127.0.0.1:6379> scard myset # 获取当前集合中的元素个数
(integer) 4
127.0.0.1:6379> smembers myset
1) "zhangsan"
2) "dong"
3) "porter"
4) "hello"
127.0.0.1:6379> srem myset dong # 移除集合中的dong
(integer) 1
127.0.0.1:6379> smembers myset
1) "zhangsan"
2) "porter"
3) "hello"
指令 作用
srandmember key [count] 从key集合中随机抽取count个元素,count可以不写,默认随机抽取一个元素
spop key [count] 从key集合中随机移除count个元素,count可以不写,默认随机删除一个元素
127.0.0.1:6379> sadd myset zhangsan lisi wangwu zhaoliu
(integer) 4
127.0.0.1:6379> srandmember myset # 从set集合中随机抽取一个元素
"zhaoliu"
127.0.0.1:6379> srandmember myset
"zhangsan"
127.0.0.1:6379> srandmember myset 2 # 从set集合中随机抽取2个元素
1) "lisi"
2) "zhangsan"
127.0.0.1:6379> srandmember myset 2
1) "lisi"
2) "wangwu"
127.0.0.1:6379> spop myset
"zhaoliu"
127.0.0.1:6379> spop myset
"zhangsan"
指令 作用
smove key key1 member 将key集合中的member元素移动到key1集合中
127.0.0.1:6379> sadd myset zhangsan lisi wangwu zhaoliu
(integer) 4
127.0.0.1:6379> sadd myset2 porterdong
(integer) 1
127.0.0.1:6379> smove myset myset2 zhangsan # 将myset集合中的zhangsan移动到myset2集合中
(integer) 1
127.0.0.1:6379> smembers myset
1) "zhaoliu"
2) "wangwu"
3) "lisi"
127.0.0.1:6379> smembers myset2
1) "porterdong"
2) "zhangsan"

使用的是数学的集合类

  • 差集
  • 交集
  • 并集
指令 作用
sdiff key1 key2 返回key1集合和key2集合不一致的地方
sinter key1 key2 返回key1集合和key2集合一样的地方
sunion key1 key2 将key1集合和key2集合中的元素做并集
127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2 # 将两个集合做差集,相对于key2集合返回key1集合不一直的地方
1) "a"
2) "b"
127.0.0.1:6379> sinter key1 key2 # 将两个集合做交集  共同关注可以使用这个sinter
1) "c"
127.0.0.1:6379> sunion key1 key2 # 将两个集合做并集
1) "e"
2) "c"
3) "b"
4) "a"
5) "d"

set集合的使用

在微博中、B站、抖音中共同关注,共同爱好,二度好友(六度分隔理论,即好友推荐)

2.6.4、Hash(哈希)

可以将Hash想象成Map集合,key-map,这时候在Map中存储的数据是键值对 本质和String类型本质没有太大区别,只是value变成了hash值
值得注意的是,Hash集合中所有的命令都是h开头

指令 作用
hset key field value 将一条field-value键值对存储到集合key中
hmset key field value field2 value2 ... 将一条或者多条field-value键值对存储到集合key中
hget key field 从key集合中将field对应的value值取出来
hmget key field field2 ... 从key集合中将一个或者多个field对应的value值取出来
hgetall key 将key集合中所有的key-value对读取出来
hkeys key 获取key集合中所有的field值
hvals key 获取key集合中所有的value值
hexists key filed 判断key集合中filed key值是否存在,存在返回1,不存在返回0
hlen key 返回key集合中有多少对k-v
127.0.0.1:6379> hset myhash field1 porterdong # 将field1-porterdong k-v数据存储到hash集合中
(integer) 1
127.0.0.1:6379> hget myhash field1
"porterdong"
127.0.0.1:6379> hmset myhash field2 zhangsan field3 lisi # 将多条 k-v数据存储到hash集合中
(integer) 2
127.0.0.1:6379> hget myhash field2 # 根据field 将 hash集合中对应的数据读取出来
"zhangsan"
127.0.0.1:6379> hget myhash field3
"lisi"
127.0.0.1:6379> hmget myhash field1 field2 # 根据多个field值将hash集合中对应的多个值读取出来
1) "porterdong"
2) "zhangsan"
127.0.0.1:6379> hgetall myhash # 将hash集合中所有的key-value键值对读取出来
 1) "field1"
 2) "porterdong"
 3) "field2"
 4) "zhangsan"
 5) "field3"
 6) "lisi"
 127.0.0.1:6379> hlen myhash # 获取hash表的字段数量
(integer) 3
127.0.0.1:6379> hexists myhash field1 # 判断hash表中字段field1是否存在
(integer) 1
127.0.0.1:6379> hkeys myhash # 获取hash中所有的key值
1) "field1"
2) "field2"
3) "field3"
127.0.0.1:6379> hvals myhash # 获取hash中所有的value值
1) "porterdong"
2) "zhangsan"
3) "lisi"
指令 作用
hdel key field [field2 field3 ...] 将key集合中的一个或者多个键值对删掉
 127.0.0.1:6379> hdel myhash field1 # 将myhash集合中的key为field1的键值对删除掉
(integer) 1
127.0.0.1:6379> hgetall myhash
 1) "field2"
 2) "zhangsan"
 3) "field3"
 4) "lisi"
127.0.0.1:6379> hdel myhash field2 field3 # 将myhash集合中的多个键值对删除掉
(integer) 2
127.0.0.1:6379> hgetall myhash
(empty array)
指令 作用
hincrby key filed num 将key集合中的filed对应的value值以步长num做增加或减少
hsetnx key filed value 判断key集合中的field是否存在,如果不存在则进行创建,存在不做任何操作
127.0.0.1:6379> hset myhash age 10
(integer) 1
127.0.0.1:6379> hincrby myhash age 1 # 将hash中的age对应的值自增1
(integer) 11
127.0.0.1:6379> hincrby myhash age 10 # 将hash中age对应的值步长10增加
(integer) 21
127.0.0.1:6379> hincrby myhash age -5 # 将hash中age对应的值步长5减少
(integer) 16
127.0.0.1:6379> hsetnx myhash name zhangsan # 如果hash中的name不存在则进行创建,否则没有任何操作
(integer) 1
127.0.0.1:6379> hsetnx myhash name lisi
(integer) 0

hash使用情景

  • 用来存储对象 尤其是用户信息、经常变动信息的保存 hash更适合于对象的存储,String更加适合字符串存储

2.6.5、Zset(有序集合)

在set的基础之上,增加了一个值用来排序,用来排序的值最好是自己的序列或者是业务相关的内容
值得注意的是Zset中所有指令都是z开头

指令 作用
zadd key 字段 value [字段2 value2 字段3 value3 ...] 向key集合中存入1条或者n条数据,字段要有一定顺序
zrange key start end 从key集合中将排序之后的下标start到end 的值读取出来,如果end为-1则返回从start开始到结尾的所有数据
zrangebyscore key min max [withscores] 对set集合中的数据进行从小到大排序 可以使用-inf(无穷小),+inf(无穷大),也可以使用具体范围
zrevrangebyscore key min max [withsocres] 对set集合中的数据进行从大到小排序 可以使用+inf(无穷大),-inf(无穷小),也可以使用具体范围
zcount key start end 查询某个区间的元素个数
127.0.0.1:6379> zadd myset 1 one # 向myset集合中存入一条数据,排序值为1
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three 4 four # 向myset集合中存入n条数据,排序值递增
(integer) 3
127.0.0.1:6379> zrange myset 0 -1 # 查询myset集合中所有已经排完序的元素
1) "one"
2) "two"
3) "three"
4) "four"
127.0.0.1:6379> zadd myset 50000 zhangsan
(integer) 1
127.0.0.1:6379> zadd myset 45000 lisi
(integer) 1
127.0.0.1:6379> zadd myset 40000 wangwu
(integer) 1
127.0.0.1:6379> zadd myset 35000 zhaoliu
(integer) 1
127.0.0.1:6379> zrangebyscore myset -inf +inf # 将所有数据从小到大进行排序
1) "zhaoliu"
2) "wangwu"
3) "lisi"
4) "zhangsan"
127.0.0.1:6379> zrangebyscore myset -inf +inf withscores # 将排序后的数据的score排序值也显示出来
1) "zhaoliu"
2) "35000"
3) "wangwu"
4) "40000"
5) "lisi"
6) "45000"
7) "zhangsan"
8) "50000"
127.0.0.1:6379> zrangebyscore myset -inf 45000 withscores # 获取排序后某个范围的数据
1) "zhaoliu"
2) "35000"
3) "wangwu"
4) "40000"
5) "lisi"
6) "45000"
127.0.0.1:6379> zrevrangebyscore myset +inf -inf # 将zset排序从大到小进行排序
1) "zhangsan"
2) "wangwu"
3) "zhaoliu"
127.0.0.1:6379> zcount myset 35000 45000 # 查询两个区间的个数
(integer) 2
指令 作用
zrem key value 移除zset集合中某一个元素
zcard key 获取zset集合中的元素个数
127.0.0.1:6379> zrange myset 0 -1
1) "zhaoliu"
2) "wangwu"
3) "lisi"
4) "zhangsan"
127.0.0.1:6379> zrem myset lisi # 移除key中某一个元素比如lisi
(integer) 1
127.0.0.1:6379> zrange myset 0 -1
1) "zhaoliu"
2) "wangwu"
3) "zhangsan"
127.0.0.1:6379> zcard myset # 查询集合中所有的元素个数
(integer) 3

zset常见的使用场景

因为zset集合和set集合基本类似就是排序了

  • zset来存储班级成绩表,工资表排序等
  • 消息的权重判断
  • 排行榜应用实现 比如B站每日最热视频Top N测试

2.7三大特殊数据类型

2.7.1、geospatital地理位置

使用场景:朋友的定位,附近的好友,打车距离的计算
Redis的Geo在Redis3.2版本就已经推出来了,这个功能可以推算两地的位置信息,两地的间隔
可以查询一些测试用例

geoadd

指令 作用
geoadd key 经度 纬度 value[经度1 纬度1 value1 经度2 纬度2 value2 ...] 将一组或者多组value位置的经度和纬度存储到key集合中
# 规则:两极无法进行添加,一般会下载城市数据,直接通过java数据一次性导入
# 参数 key 值(经度) 值(纬度) 值(名称) 有效的经度是-180~180 有效的纬度是-85.05112878~85.05112878
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing # 将北京的位置存储到集合中
(integer) 1
127.0.0.1:6379> geoadd china:city 115.48 38.86 baoding # 将保定的位置存储到集合中
(integer) 1
127.0.0.1:6379> geoadd china:city 121.61 38.91 dalian
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai 114.08 22.54 shenzhen # 将多个位置存储到集合中
(integer) 2

geopos

获取当前定位,当前定位肯定是一个坐标值

指令 作用
geopos key value [value2 ...] 获取城市的经度和纬度
127.0.0.1:6379> geopos china:city beijing # 查看北京的经度和纬度
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city baoding shanghai # 查看多个城市的经度和纬度
1) 1) "115.48000127077102661"
   2) "38.85999893055183207"
2) 1) "121.47000163793563843"
   2) "31.22999903975783553"

geodist

获取两个位置之间的距离,如果两个位置有一个不存在,该命令就会返回空值
指定单位的参数unit必须是以下单位的其中一个

单位 含义
m 表示单位米
km 表示单位千米
mi 表示单位英里
ft 表示单位英尺
指令 作用
geodist key member1 member2 unit 获取两个地点之间的直线距离
127.0.0.1:6379> geodist china:city beijing baoding m # 查询从北京到保定的距离 米
"140129.3952"
127.0.0.1:6379> geodist china:city baoding shanghai km # 查询从保定到上海的距离 千米
"1008.2467"

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

比如查找附近的人,(获取附近所有的的地址,也就是获取当前定位)通过半径来查询!
指定单位的参数unit必须是以下单位的其中一个

单位 含义
m 表示单位米
km 表示单位千米
mi 表示单位英里
ft 表示单位英尺
指令 作用
georadius key 经度 纬度 r unit [withdist][withcoord][count num] 查询以当前经纬度为中心,半径为r单位unit范围的元素[并显示直线距离][并显示经纬度][选择需要显示的数量]
127.0.0.1:6379> georadius china:city 110 30 1200 km # 查询距离经度110纬度30半径1200千米的城市
1) "shenzhen"
2) "shanghai"
3) "baoding"
127.0.0.1:6379> georadius china:city 110 30 1100 km withdist # 查询距离经度110纬度30半径1100千米的城市 并显示到中心的直线距离
1) 1) "shenzhen"
   2) "923.9364"
127.0.0.1:6379> georadius china:city 110 30 1100 km withcoord # 查询距离经度110纬度30半径1100千米的城市 并显示该城市的经纬度
1) 1) "shenzhen"
   2) 1) "114.08000081777572632"
      2) "22.53999903789756587"
127.0.0.1:6379> georadius china:city 110 30 1300 km count 2 # 查询距离经度110纬度30半径1300千米的城市 并只显示2个
1) "shenzhen"
2) "baoding"

georadiusbymember

查找指定中心半径内的元素
指定单位的参数unit必须是以下单位的其中一个

单位 含义
m 表示单位米
km 表示单位千米
mi 表示单位英里
ft 表示单位英尺
指令 作用
- -
georadiusbymember key member r unit 查找指定位置半径的元素
127.0.0.1:6379> georadiusbymember china:city beijing 200 km # 查找以北京为中心半径200千米内的城市
1) "beijing"
2) "baoding"

geo底层的实现原理就是zset,可以使用zset命令来操作geo

127.0.0.1:6379> zrange china:city 0 -1 # 通过zset命令来查询当前geo中所有的元素
1) "shenzhen"
2) "shanghai"
3) "baoding"
4) "beijing"
5) "dalian"
127.0.0.1:6379> zrem china:city shanghai # 通过zrem移除geo中的某一个元素
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "shenzhen"
2) "baoding"
3) "beijing"
4) "dalian"

2.7.2、hyperloglog

什么是基数

A(1,3,5,7,9) => 5
B(1,3,5,7,5,7,9) => 5
基数(不重复的元素),可以接受误差

简介

在Redis 2.8.9版本更新了hyperloglog数据结构,是基于基数统计的算法
优点:占用内存是固定的,2^64次不同元素的基数,只需要占用12KB内存,所以如果是基于内存去考虑的话hyperloglog是首选
比如网页的UV(一个人访问一个网站多次,但是还是算作1次访问)
传统的方式,set集合来存储用户的id,然后就可以统计set中的元素数量来作为标准去判断
这个方式如果保存大量的id,就会比较麻烦,而UV的目的是为了计数,而不是保存用户id
但是值得注意的是hyperloglog官网说有0.81%的错误率,但是在统计UV来说,可以忽略不记

指令 作用
pfadd key v1 v2 v3 ... 向key集合中存储一个或多个值
pfcount key key2 ... 查询一个或多个key集合中元素个数(去重)
pfmerge key source source2 ... 将多个集合的数据存储到一个key集合中(会去重)
127.0.0.1:6379> pfadd m1 a b c d e f g h i j # 向m1集合中加入若干个元素
(integer) 1
127.0.0.1:6379> pfcount m1 # 查询m1集合中有多少基数
(integer) 10
127.0.0.1:6379> pfadd m2 i j b d e m w q w z
(integer) 1
127.0.0.1:6379> pfcount m2
(integer) 9
127.0.0.1:6379> pfmerge m3 m1 m2 # 将m1和m2集合的元素加入到m3集合中,并去重  相当于并集
OK
127.0.0.1:6379> pfcount m3
(integer) 14

如果允许容错的话,一定要使用hyperloglog,如果不允许容错,那么就是用set或者自己的数据类型即可

2.7.3、bitmap

位存储

比如统计用户的活跃度信息,是否登录,是否打卡,只要涉及到两个状态的都可以使用bigmaps
bitmap位图,数据结构,都是操作二进制位来进行记录,就只有0和1两个状态

指令 作用
setbig key offset value 将offset的状态value存储到key结果中 office可以表示周几,天等结果,value只能有两个值
getbig key offset 查看key列表中该offset的状态
bitcount key [start end] 查询[在start-end的offset区间中]状态为1的数量
127.0.0.1:6379> setbit sign 0 0 # 周一没有打卡 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 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(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 0 # 查询sign列表中周一的状态
(integer) 0 # 未打卡
127.0.0.1:6379> getbit sign 6 # 查询sign列表中周日的状态
(integer) 1 # 已打卡
127.0.0.1:6379> bitcount sign # 查询状态为1的有多少条
(integer) 4

2.8、事务

Redis事务的本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务的执行过程中,会按照顺序执行
一个事务在执行过程中可能会有多条执行执行,相当于一个指令块,而这些执行在执行过程中,保证一次性、顺序性、排他性。

--------队列 set命令1 set命令2 set命令3... 执行-----

在Redis中单条命令是保证原子性的,但是事务并不保证原子性
Redis事务没有隔离级别的概念
所有的命令在事务中并没有直接执行只是放在队列里边,只有当发起执行事务的操作的时候,这些命令才会被执行 Exec
redis的事务:

  • 开启事务(multi)
  • 命令入队(...)
  • 执行事务(exec)
  • 取消事务(discard)
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set name zhangsan
QUEUED # 命令入队
127.0.0.1:6379(TX)> set age 20
QUEUED # 命令入队
127.0.0.1:6379(TX)> set sex boy
QUEUED # 命令入队
127.0.0.1:6379(TX)> exec # 执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379> multi # 开始新的事务
OK
127.0.0.1:6379(TX)> set name zhangsan
QUEUED
127.0.0.1:6379(TX)> set height 180
QUEUED
127.0.0.1:6379(TX)> discard # 放弃事务
OK
127.0.0.1:6379> get height # 上述事务并没有执行
(nil)

2.8.1、异常

编译时异常(指令有错误,代码不会被执行) 所有的指令都不会被执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name zhangsan
QUEUED
127.0.0.1:6379(TX)> set age 20
QUEUED
127.0.0.1:6379(TX)> getset sex # 指令使用出现错误,在编译的时候就出现问题了
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set sex boy
QUEUED
127.0.0.1:6379(TX)> exec # 当一个指令出现问题之后,所有的指令都不会被执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name # 上述所有的命令并没有被执行
(nil)

运行时异常(语法有错误) 其他正常的语法可以执行

127.0.0.1:6379> set name zhangsan # 本条执行并没有包含在事务中,可以正常执行
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr name # name中存储的值是字符串,并不能做自增操作
QUEUED
127.0.0.1:6379(TX)> set age 20
QUEUED
127.0.0.1:6379(TX)> incr age
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range # 只有第一条执行报了运行时的错误
2) OK # 命令可以正常执行
3) (integer) 21 # 命令可以正常执行
127.0.0.1:6379> mget name age # 其他指令可以正常执行,age做了自增操作
1) "zhangsan"
2) "21"

2.8.2、锁

监控 watch 如果事务执行成功之后监控就会被取消掉 (面试常问)

如果在事务开始之前监视了一个对象,在提交事务的时候会将此刻的被监视的对象和之前监视的对象做一个对比,如果值一致,则事务可以正常提交,否则事务提交失败

悲观锁

  • 很悲观,认为什么时候都会出现问题,也就是无论什么时候都需要对资源加锁

乐观锁

  • 很乐观,认为什么时候都不会出现问题,所以不会对资源加锁,在更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 比如在mysql中获取version
  • 更新的时候比较version

正常执行成功的情况也就是乐观锁

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

测试多线程修改值,使用watch可以当做redis的乐观锁去操作执行失败的情况

# 第一个事务
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 # 因为刚才第二个事务已经对监控对象money做了操作,所以事务不会提交成功
(nil) # 事务提交失败
127.0.0.1:6379> get money # 再次查看被监控的对象发现金额已经被改变了
"1000"

# 第二个事务
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000 # 在第二个事务执行完之后,可以看到,money金额已经被修改成功
OK
指令 作用
watch key 用来监视一个key,来做乐观锁
unwatch 将之前监视的key取消掉

如果监控对象事务修改失败,直接重新监视最新的值即可

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(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 980
2) (integer) 40

2.9、Jedis

PS:本次Idea的版本是2021.3.3
接下来,我们要使用Java连接Redis,

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

2.9.1、新建一个空的项目

打开idea,创建一个空的项目
image

2.9.2、新建一个Model

image
image
将父项目的jdk设置成自己本地的JDK版本,并将level设置成8,否则好多语法都不支持,修改完之后点击apply
image

2.9.3、设置Jedis和fastjson依赖

可以从maven仓库,查找对应的依赖

    <dependencies>
        <!--首先是Jedis依赖-->
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.3.1</version>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>
    </dependencies>

2.9.4、测试java连接Redis数据库

首先打开window本地的服务端
image

public class Redis_ping {
    public static void main(String[] args) {
        //1、new Jedis()对象即可,该对象中有许多构造方法,常用的是String addr , int port
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis对象的所有命令就是我们之前学习的所有指令,转换成了对应的方法
        System.out.println(jedis.ping());
    }
}

image

2.9.5、常见命令的使用

public class TestKey {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        System.out.println("清空数据:" + jedis.flushDB());
        System.out.println("判断某个键是否存在:" + jedis.exists("username"));
        System.out.println("新增<'username','porterdong'>的键值对:" + jedis.set("username","porterdong"));
        System.out.println("新增<'password','password'>的键值对:" + jedis.set("password","password"));
        System.out.println("系统中所有的key如下:");
        Set<String> keys = jedis.keys("*");
        System.out.println(keys);
        System.out.println("删除键password:" + jedis.del("password"));
        System.out.println("判断password是否存在:" + jedis.exists("password"));
        System.out.println("查看username键所存储的值的类型:" + jedis.type("username"));
        System.out.println("随机返回key空间的一个:" + jedis.randomKey());
        System.out.println("重命名key:" + jedis.rename("username","name"));
        System.out.println("取出修改后的name:" + jedis.get("name"));
        System.out.println("按索引查询(修改数据库):" + jedis.select(0));
        System.out.println("删除当前数据库中的key:" + jedis.flushDB());
        System.out.println("查看当前数据库中所有的key数量:" + jedis.dbSize());
        System.out.println("删除所有数据库中的所有的key:" + jedis.flushAll());
    }
}

image

2.9.6、String类型指令的测试

public class TestString {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);
        jedis.flushDB();
        System.out.println("======增加数据======");
        System.out.println(jedis.set("key1","value1"));
        System.out.println(jedis.set("key2","value2"));
        System.out.println(jedis.set("key3","value3"));
        System.out.println("删除键key2:" + jedis.del("key2"));
        System.out.println("获取键key2:" + jedis.get("key2"));
        System.out.println("修改key1:" + jedis.set("key1","value1Change"));
        System.out.println("获取key1的值:" + jedis.get("key1"));
        System.out.println("在key3后边加入值:" + jedis.append("key3","End"));
        System.out.println("key3的值:" + jedis.get("key3"));
        System.out.println("增加多个键值对:" + jedis.mset("k1","v1","k2","v2","k3","v3","k4","v4"));
        System.out.println("获取多个键值对:" + jedis.mget("k1","k2","k3"));
        System.out.println("获取多个键值对:" + jedis.mget("k1","k2","k3","k4"));
        System.out.println("删除多个键值对:" + jedis.del("k1","k3"));
        System.out.println("获取多个键值对:" + jedis.mget("k1","k2","k3"));

        jedis.flushDB();
        System.out.println("======新增键值对防止覆盖原先值=======");
        System.out.println(jedis.setnx("key1","value1"));
        System.out.println(jedis.setnx("key2","value2"));
        System.out.println(jedis.setnx("key2","value2-new"));
        System.out.println(jedis.get("key1"));
        System.out.println(jedis.get("key2"));

        System.out.println("=======新增键值对并设置有效时间=====");
        System.out.println(jedis.setex("key3",2,"value3"));
        System.out.println(jedis.get("key3"));
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(jedis.get("key3"));
        System.out.println("=======获取原值,更新为新的值======");
        System.out.println(jedis.getSet("key2","key2GetSet"));
        System.out.println(jedis.get("key2"));
        System.out.println("获取key2的字串:" + jedis.getrange("key2",2,4));
    }
}

image
对于List、Set、Hash、Zset而言Jedis所有的命令和上边五大基本数据类型一致,可以直接通过Jedis对象调用对应的方法即可,下边就不一一展示了

2.9.7、使用Jedis操作事务

在使用Jedis执行事务的时候,需要将事务对象交给Transaction对象,由Transaction对象来执行指令块,并且指令在执行过程中可能会产生异常,所以需要将指令块放在try{}代码块中。

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379); //开启连接
        jedis.flushDB();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","zhangsan");
        jsonObject.put("age","20");
        String s = jsonObject.toJSONString();
        //开启事务
        Transaction multi = jedis.multi();
        try {
            //因为在一条事务的执行过程中,可能会出现错误,所有将所有的指令块放在try中
            multi.set("user1",s);
            //int i = 10 / 0; // 定义一个运行期异常,可以检测数据是否插入成功
            multi.set("user2",s);
            System.out.println(multi.exec());
        } catch (Exception e) {
            //如果事务执行过程中产生异常,则放弃所有的事务
            multi.discard();
            e.printStackTrace();
        } finally {
            System.out.println(jedis.mget("user1","user2"));
            //无论事务是否执行成功,都要关闭资源
            jedis.close();//关闭连接对象
        }
    }
}

image

2.10、SpringBoot整合Redis

通过SpringData整合Redis

2.10.1、新建一个model

image
选择对应的依赖
image
image
通过查看pom依赖的源码可以看到,从SpringBoot2.x之后,原来使用的Jedis工具被替换为了lettuce

  • Jedis:采用的是直连的,多个线程操作的话是不安全的,如果想要避免不安全,需要使用Jedis Pool连接池去解决,更像BIO模式
  • lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数量,更像NIO模式

源码分析

@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
	//该注解的作用是如果自定了一个Template类name这个类就会失效
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
	    //默认的RedisTemplate没有过多的设置,redis的对象都是需要进行序列化的
		//两个泛型都是Object的类型,我们使用后需要进行强制转换,因为我们期望的是<String,Object>类型
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean //因为String是Redis中最常用的类型,所有Spring单独提出来了一个方法
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

在RedisTemplate源码中定义了序列化
image
但是默认的序列化使用的JDK的序列化
image

pom依赖

<!--操作redis-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.10.2、将配置文件修改yml后缀并修改

# 配置Redis  
# 对于配置Redis连接池现在已经不推荐使用Jedis了
# 因为在RedisConnectionFactory源码中Jedis的依赖没有被注入,而lettuce推荐使用
spring:
  redis:
    host: 127.0.0.1
    port: 6379

2.10.3、测试连接

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired//注入RedisTemplate对象来操作redis
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //在企业的开发过程中,80%的情况都不会使用这个原生的方式去编写代码 一般都会封装一个工具类
        //redisTemplate 操作不同的数据类型,api和redis的指令是一样的
        //opsForValue 操作字符串  类似string
        //opsForList  操作列表
        //opsForSet   操作set
        //opsForHash  操作key-value 类似map
        //opsForGeo   操作地理位置
        //opsForHyperLogLog 操作基数
        //除了本身的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD
        //获取redis的连接对象
        //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //connection.flushDb();
        //connection.flushAll();
        //注意存入的值不能是中文,否则在redis-cli中看到的值是转义之后的,我们可以自定义RedisTemplate来实现
        redisTemplate.opsForValue().set("name","zhangsan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
}

image

2.10.4、自定义RedisTemplate

新建一个配置类RedisConfig用来编写Redis的一些配置
image
首先测试一下如果没有序列化会产生的一个结果,首先创建一个实体类
image

@Component //变成一个组件,方便我们调用
@AllArgsConstructor //有参构造
@NoArgsConstructor  //无参构造
@Data // lombok
// 在企业中,所有的pojo都会被序列化
public class User {
    private String name;
    private int age;
}

编写测试类

    @Test
    void test() throws JsonProcessingException {
        //但是在真实的开发过程中,一般都使用json来传递对象
        User user = new User("张三", 25);
        //将user对象通过ObjectMapper类的writeAsString来进行序列化
        //String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

如果直接传递对象的话那么会抛出一个异常,序列化失败,无法传输对象
image
关于对象的保存,需要进行序列化,在实体类去实现Serializable接口,再去测试方法
image
image
可以看到,如果对象被序列化了,就可以进行传递了
编辑RedisConfig来编写RedisTemplate

@SuppressWarnings({"all"})
@Configuration
public class RedisConfig {
    //这个基本上是一个固定的模板,可以直接用
    //编写我们自己的RedisTemplate类来序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplates(RedisConnectionFactory factory) {
        //为了开发方便,一般都会使用<String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(factory);
        //Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //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;
    }
}

修改测试类所引用的redisTemplate,如果点一下就可以进入到我们刚才编写的那个redisTemplate就可以了
image
如果不行的话,可以使用@Qualifier注解来解决歧义

    @Autowired
    @Qualifier("redisTemplates") //解决歧义
    private RedisTemplate redisTemplate;

再去执行测试类的话,就会发现在Redis的服务中,原本转义的key也显示出来了
image

2.10.5、封装工具类

@SuppressWarnings({"all"})
@Component
public 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 set(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)
     * @return
     */
    public long incr(String key,long delta){
        if(delta < 0){
            throw new RuntimeException("递增因子必须大于0");
        }else{
            return redisTemplate.opsForValue().increment(key, delta);
        }
    }

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

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

    /**
     * HashGet
     * @param key    键 不能为null
     * @param item   项  不能为null
     * @return
     */
    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 hHashKey(String key,String item){
        return redisTemplate.opsForHash().hasKey(key, item);
    }

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

    /**
     * hash递减,如果不存在就会创建一个,并把递减后的值返回
     * @param key   键
     * @param item  项
     * @param by    要减小几  大于0
     * @return
     */
    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 sHashKey(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 {
            return redisTemplate.opsForSet().remove(key, values);
        } 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倒数第二个元素,以此类推
     * @return
     */
    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 值
     */
    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  时间 秒
     */
    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 {
            return redisTemplate.opsForList().remove(key,count,value);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

在测试类中引入创建的RedisUtil工具类

    @Autowired
    private RedisUtil redisUtil;

重新创建一个测试方法,来测试刚才创建好的工具类

    @Test
    void testUtil(){
        redisUtil.set("username","porterdong");
        System.out.println(redisUtil.get("username"));
    }

image

3、Redis进阶

3.1、Redis.conf配制文件

启动Redis服务的时候,需要通过redis.conf来启动服务

3.1.1、单位

配置文件对大小写不敏感
image

3.1.2、包含

可以像Spring那样去包含多个配置文件,来整合成一个配置文件
image

3.1.3、网络

通过一系列的网络配置来控制连接远程Redis服务器
image

bind 127.0.0.1 -::1 # 绑定的是本机IPV4/IPV6,如果想让其他电脑可以访问这个redis就可以使*或者固定IP
protected-mode yes # Redis是否受保护,一般是开起的,保证redis的安全
port 6379 # redis的端口号,后边搭建主从复制集群的时候需要修改端口号
timeout 0 # 设置失效的时长

3.1.4、通用

配置redis服务器的一些通用配置
image

daemonize yes # 以守护进程的方式进行运行  默认是关闭的   需要手动开启服务
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 # 日志级别,debug用于测试和开发环境,默认是notice 用于生产环境
logfile "" # 日志的路径文件名
databases 16 # 默认有16个数据库
always-show-logo no # 是否一直显示redis启动logo

3.1.5、快照 SNAPSHOTTING

持久化,在指定的时间内,执行了多少次操作就会将数据保存起来 生成.rdb或者.aof文件

# save 3600 1 300 100 60 10000 在redis7 默认是关闭的  如果打开的话需要按照以下方式去打开
save 3600 1    # 在 3600秒内 执行了1次操作就会持久化
save 300 100   # 在300秒内,执行了100次操作就会持久化操作
save 60 10000  # 在60秒内,执行了10000次操作就会持久化操作

stop-writes-on-bgsave-error yes # 持久化如果出错了,是否还需要继续工作,默认是开启的
rdbcompression yes # 是否压缩rdb文件,默认开启,  会消耗一点CPU的资源
rdbchecksum yes   # 是否校验rdb文件
dbfilename dump.rdb # 快照的名称
dir ./ # rdb文件保存的目录  默认是当前目录

3.1.6、主从复制 REPLICATION

先不讲,等到搭建集群的时候再说
image

3.1.7、安全 SECURITY

Redis默认是没有密码的

# requirepass foobared 默认是空的 如果想设置密码可以按照以下模式设置
requirepass 123456

# 在redis数据库中查询的时候
127.0.0.1:6379> config get requirepass # 获取redis的密码
1) "requirepass"
2) "" # 密码是空的
# 也可以通过config命令来设置密码
127.0.0.1:6379> config set requirepass 123456
OK
127.0.0.1:6379> ping
(error) NOAUTH Authentication required. # 重新连接redis服务器发现 ping不成功
127.0.0.1:6379> auth 123456 # 登录设置的密码
OK
127.0.0.1:6379> ping
PONG

3.1.8、客户端 CLIENTS

# maxclients 10000 # 设置连接redis的客户端最多有多少,默认是关掉的

3.1.9、最大内存 MEMORY MANAGEMENT

maxmemory <bytes> # 配置Redis服务器的最大内存
maxmemory-policy noeviction # 内存到达上线之后的配置
# 1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
# 2、allkeys-lru : 删除lru算法的key
# 3、volatile-random:随机删除即将过期key
# 4、allkeys-random:随机删除
# 5、volatile-ttl : 删除即将过期的
# 6、noeviction : 永不过期,返回错误

3.1.10、AOF配置 APPEND ONLY MODE

appendonly no # 默认是不开启AOF模式的,默认使用RDB方式持久化,在大部分的情况,RDB够用了
appendfilename "appendonly.aof" # 持久化文件的名字
# appendfsync always # 每次修改同步一次  会消耗性能
appendfsync everysec # 每秒同步一次     可能会丢失1s内的数据
# appendfsync no # 不同步 速度最快

3.2、Redis持久化

Redis是内存数据库,每次断电内存中的数据都会丢失,所以内存数据使用持久化是必须的。
Redis持久化方式有两种:rdbaof

3.2.1、RDB

rdb的全称是Redis Database Backup File(redis数据备份文件),简单的说,就是将内存中的数据保存到磁盘中,当断电丢失数据之后从磁盘中读取备份恢复数据。
RDB的持久化四种情况

  • 执行save命令
  • 执行bgsave命令
  • Redis停机时
  • 触发RDB条件时

3.2.1.1、save命令

进入到/usr/local/bin目录下,查看是否有dump.rdb文件如果有的话,将其删除
image
删除完毕之后再次连接redis服务器执行执行save命令

127.0.0.1:6379> save
OK

在从bin目录下查看,此时已经生成了一个dump.rdb
image

缺点

save命令会导致主线程直接执行RDB,但是之前说过Redis是单线程的,在这个过程中,所有的指令都会被阻塞。一般此命令只会在数据进行迁移的时候才会使用

3.2.1.2、bgsave命令

先删除bin目录下的dump.rdb,然后再连接redis服务器,执行bgsave,也会生成一个dump.rdb文件,

127.0.0.1:6379> bgsave
Background saving started

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

疑问如果在执行bgsave的过程中,有人在做修改怎么办?

由于fork采用的是copy-on-write,在执行bgsave的过程中,redis会把内存中的数据只读化操作,然后拷贝一个副本,对副本进行修改操作
因为fork会对redis的数据做一个拷贝,所以在执行bgsave的时候一定要保证有足够的内存空间,否则会造成OOM

3.2.1.3、Redis停机

删除bin目录下的dump.rdb文件,然后重新连接redis服务器,执行shutdown命令,也会在bin目录生成一个dump.rdb

127.0.0.1:6379> shutdown
not connected>

3.2.1.4、触发RDB

虽然上述三种方式都可以对redis的数据做一个备份,但是以上三种方式都是自己手动触发的,一旦Redis宕机了,数据也会丢失,所以最好有一种定时备份的方式,来备份数据
在看redis.conf的时候,里边有个配置

# save 3600 1 300 100 60 10000 在redis7 默认是关闭的  如果打开的话需要按照以下方式去打开
save 3600 1    # 在 3600秒内 执行了1次操作就会持久化
save 300 100   # 在300秒内,执行了100次操作就会持久化操作
save 60 10000  # 在60秒内,执行了10000次操作就会持久化操作

现在我们将配置文件修改一下,改成以下方式来做个测试

save 60 5 # 60s内有5次操作就会备份数据

重新启动Redis服务端,删除bin目录下的dump.rdb文件,并连接redis服务器
image

127.0.0.1:6379> set id 1
OK
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> set sex boy
OK
127.0.0.1:6379> set bir 1997/01/26
OK

5条数据插入完成之后,查看bin目录下的内容,发现dump.rdb又出现了
image
注意:这种触发方式的触发时间,操作次数是自己可以灵活自定义的,需要根据需求来进行编辑

3.2.1.5、flushall

flushall是用来清空数据库的,flush之后也会出现一个dump.rdb文件
删除bin下的dump.rdb文件

127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> flushall
OK

对应的bin目录,已经出现dump.rdb文件了
image
说明,flushall,是对所有数据库进行一个清空,这个指令会备份所有数据库中的内容。

3.2.1.6、总结

rdb模式在大多数情况下是我们的首选、默认的持久化方式,因为它既能满足特定的备份,也会按照特定规则进行快照存储,但是也会有一些弊端,比如在执行触发save操作的时候配置文件我们编写的是save 60 5,如果说我执行了4次操作,但是服务器突然宕机了,前4次数据的操作也会丢失;如果设置的时间过短,当数据量过大的时候也会造成服务器负载变大。其实rdb模式除了容易造成数据丢失以外,其他的都挺好

  • 优点:适合大规模的数据备份 不会阻塞主线程
  • 数据有可能会丢失,fork会造成额外的开销

3.2.2、AOF Append Only File

将我们所有的命令都记录下来,生成一个类似于history的文件,恢复的时候直接把这个文件中的命令再重新执行一遍

执行过程

Redis在执行父进程的过程中,会fork一个子进程,以日志的形式来记录每个写入的操作,(因为读取数据并不会涉及到数据的变化),将Redis执行过的所有指令记录下来,只许追加文件但是不可以改写文件,redis启动的时候会读取该文件重新去执行里边的命令,换而言之,redis重启的话就回根据日志文件的内容将写指令从前往后执行一次以完成数据的恢复工作
AOF存储的是appendonly.aof文件

3.2.2.1、配置aof

因为redis默认打开的是rdb存储,aof默认是关闭的,所以需要修改配置文件,打开aof模式,并重启Server端

# 开始aof模式
appendonly no # 默认是不开启的,只需要将no修改为yes即可开启aof模式
appendfilename "appendonly.aof" # 生成的日志名称
appenddirname "appendonlydir" # 日志位置
# 生成日志的规则  一般不会进行修改
# appendfsync always  # 每次都需要存储  安全性最好,性能最差
appendfsync everysec # 每一秒进行存储 一般会采用此默认的形式
# appendfsync no # 不进行存储  性能最好 安全性最差
no-appendfsync-on-rewrite no # 读写设置,默认为no即每次的指令进行追加而不是进行修改
auto-aof-rewrite-percentage 100 
auto-aof-rewrite-min-size 64mb # 当日志文件大小大于等于64mb的时候就会保存到其他日志文件

进入bin目录查看该目录下的所有文件,发现并没有aof生成的目录或文件
image
开启redis服务,开启之后再次查看bin下的所有文件,此时会看到,当使用aof模式时开启服务端会自动生成一个appendonlydir目录,里边有三个临时文件
image
image
向数据库中插入几条数据

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> mset k2 v2 k3 v3
OK

查看bin目录下的appendonly.aof.1.incr.aof文件的内容,可以看到是将我们之前的操作的指令进行了存储。
image

如果aof文件被修改了怎么办?

redis提供了一个修复aof的工具,redis-check-aof,下面模拟一下aof文件被损坏之后的操作,先将redis服务关闭

127.0.0.1:6379> shutdown
not connected>

进入到bin目录下的appendonlydir目录,修改appendonly.aof.1.incr.aof文件,我将k2的键修改为了k2test Error,保存并退出
image
删除bin目录下的rdb文件,咱们现在单独去测试aof模式
启动redis服务,并使用客户端连接redis服务,当我去连接的时候发现redis拒绝了我的连接,因为数据出现问题了,下边去解决一下
image
使用官方修复工具去修复aof,在bin的目录下执行

redis-check-aof --fix appendonlydir/appendonly.aof.1.incr.aof # 恢复的是目录/文件
y # 是否继续,输入y回车

image
重新启动redis服务器,再次连接redis服务查看数据是否正确,发现发生错误的位置以及下边的内容都会丢失!!!,因为不能确定损坏的是哪些位置,所以是允许有容错在里边的

[root@VM-16-2-centos bin]# redis-cli -p 6379
127.0.0.1:6379> keys *
1) "k1"

但是redis默认使用的是rdb模式,所以将aof关闭即可。

3.2.3、扩展

RDB和AOF的对比

RDB AOF
持久化方式 定时对整个内存做快照 记录每一次执行的命令
数据完整性 不完整,两次备份之间的数据丢失 相对完整,取决于刷盘的策略
文件大小 会由压缩,体积较小 记录命令,体积较大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量的CPU和内存消耗 低,主要是磁盘IO的资源,但是AOF重写时会占用大量的CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高的场景

小结:
1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份 ),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

5、性能建议

  • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1这条.
    规则。
  • 如果Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价是带来了持续的10,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite 的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
  • 如果不Enable AOF,仅靠 Master-Slave Replcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉(断电),会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

3.3、Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发布者(pub)发送消息,订阅者(sub)接收消息。微博、微信、关注系统等!
Redis客户端可以订阅任意数量的频道
常见的发布订阅可以使用MQ、kafka等,但是redis也可以实现发布订阅,比如使用List数据类型
需要使用三部分:

  • 消息发送者
  • 频道
  • 消息订阅者

订阅/发布消息图
image
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系(菜鸟教程):
image
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
image

命令 描述
psubscribe pattern [pattern ...] 订阅一个或者多个符合给定模式的频道
pubsub subcommand [argument [argument ...]] 查看订阅与发布系统的状态
publish channel message 将信息发送给指定的频道
punsubscribe [pattern [pattern ...]] 退订所有给定模式的频道
subscribe channel [channel ...] 订阅一个或者多个频道的信息
unsubscribe channel [channel ...] 退订给定的频道

测试

需要开启两个redis-cli一个是发布端一个是订阅端

# 第一个cli 订阅端
127.0.0.1:6379> subscribe porterdong # 订阅porterdong频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "porterdong"
3) (integer) 1 # 等待推送者推送的信息
# 第二个cli 发布端
127.0.0.1:6379> publish porterdong "hello,porterdong" # 发布者向porterdong频道发布消息
(integer) 1
# 第一个cli  会自动弹出消息
1) "message"
2) "porterdong" # 订阅的频道
3) "hello,porterdong" # 接收到的消息

image

原理

Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,以此加深对 Redis 的理解.
Redis 通过 PUBLISH、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。
比如微信公众号:
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 频道,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中
image
通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub 从字面上理解就是发布 ( Publish )与订阅(Subscribe ),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景:

  • 实时消息系统
  • 实时聊天系统(频道当做聊天室,将所有的信息回显给所有用户)
  • 订阅关注系统

稍微复杂的场景可以使用消息中间件去做比如MQ、kafka等

3.4、Redis主从复制

3.4.1、概念

主从复制,是指将一台Redis服务器的数据,复制到其他Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(salve/follower);数据的复制是单向的,只能从主节点复制到从节点。Master以写入为主,Slave以读为主,因为80%的情况都是在读取数据库的内容。
默认情况下, 每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的内容主要包括:

  1. 数据冗余:主从复制实现了数据的备份,是持久化方式之外的一种数据冗余方式
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应连接主节点,读Redis数据时应连接从节点),分担服务器的负载;尤其是写少读多的场景下,通过多个节点分担读负载(最少是一主二从),可以大大提高Redis服务器的并发量。
  4. 高可用(集群)基石:除了上述作用之外,主从复制还是哨兵模式和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

一般来说,要将Redis运用于工程项目中,只是用一台Redis是万万不可能的(可能会宕机),原因如下:

  1. 从结构上,单个Redis服务器会发生单点故障,并且一台服务器要处理所有请求的负载,压力较大;
  2. 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有的内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应超过20G。

电商网站上的商品,一般都是一次上传,无数次浏览的,即读多写少。对于这种场景我们可以使用如下的结构模式。
image
只要在公司中,主从复制是必须要使用的,因为在真实的项目过程中,不可能使用单台Redis服务器

3.4.2、环境搭建

PS:只需要配置从库,不需要配置主库

127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master # 角色 master 主机
connected_slaves:0  # 已经连接的从机
master_failover_state:no-failover
master_replid:8cea831f286153a240a4fe4cd43cbbbf518c024e
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

配置从机的话需要配置配置文件,将redis的服务停止掉,将config目录下的配置文件复制几份

[root@VM-16-2-centos bin]# cd config # 进入配置文件目录 备份三个配置文件,一个当做主机,两个从机
[root@VM-16-2-centos config]# cp redis.conf redis-master.conf
[root@VM-16-2-centos config]# cp redis.conf redis-slave1.conf
[root@VM-16-2-centos config]# cp redis-slave1.conf redis-slave2.conf
[root@VM-16-2-centos config]# ll
total 432
-rw-r--r-- 1 root root 106555 Nov 27 16:18 redis.conf
-rw-r--r-- 1 root root 106555 Nov 27 18:02 redis-master.conf
-rw-r--r-- 1 root root 106555 Nov 27 18:02 redis-slave1.conf
-rw-r--r-- 1 root root 106555 Nov 27 18:02 redis-slave2.conf

3.4.2.1、master修改

修改redis-master.conf的配置文件,将日志打开,并修改rdb的文件名

port 6379 # 端口号不用改
daemonize yes # 守护进程打开 不用修改
pidfile /var/run/redis_6379.pid # pid进程文件不用修改
logfile "redis-master.log" # 将日志文件修改为redis-master.log表示主机的日志
dbfilename dump-master.rdb # 将快照文件名改为dump-master.rdb表示主机的快照

主机修改完毕

3.4.2.2、slave从机修改

修改redis-slave1.conf配置文件

port 6380 # 将端口号修改为 6380
daemonize yes # 守护进程打开 不用修改
pidfile /var/run/redis_6380.pid # pid进程文件修改为redis_6380.pid
logfile "redis-slave1.log" # 将日志文件修改为redis-slave1.log表示从机的日志
dbfilename dump-slave1.rdb # 将快照文件名改为dump-slave1.rdb表示从机的快照

修改redis-slave2.conf配置文件

port 6381 # 将端口号修改为 6381
daemonize yes # 守护进程打开 不用修改
pidfile /var/run/redis_6381.pid # pid进程文件修改为redis_6381.pid
logfile "redis-slave2.log" # 将日志文件修改为redis-slave2.log表示从机的日志
dbfilename dump-slave2.rdb # 将快照文件名改为dump-slave2.rdb表示从机的快照

从机修改完毕

3.4.2.3、启动redis

需要打开4个bash窗口,进入到/usr/local/bin目录下

#第一个bash启动master
[root@VM-16-2-centos bin]# redis-server config/redis-master.conf
#第二个bash启动slave1
[root@VM-16-2-centos bin]# redis-server config/redis-slave1.conf
#第三个bash启动slave2
[root@VM-16-2-centos bin]# redis-server config/redis-slave2.conf
#第四个bash查看redis进程
[root@VM-16-2-centos bin]# ps -ef|grep redis
root     28206     1  0 18:28 ?        00:00:00 redis-server 127.0.0.1:6379 # 主机Master的进程
root     28388     1  0 18:29 ?        00:00:00 redis-server 127.0.0.1:6380 # 从机Slave1的进程
root     28511     1  0 18:29 ?        00:00:00 redis-server 127.0.0.1:6381 # 从机Slave2的进程
root     28684 28623  0 18:30 pts/8    00:00:00 grep --color=auto redis

3.4.3、一主二从

默认情况下,每台Redis服务器都是主节点,我们一般情况下只需要配置从机就好了。
即给从节点认老大,找到对应的主节点。
登录到slave1和slave2,并连接master

3.4.3.1、动态设置 通过指令设置主从 暂时的

指令 作用
slaveof location port 设置location地址的,port端口的redis服务当做主节点

设置slave1从节点

127.0.0.1:6380> slaveof 127.0.0.1 6379 # 设置当前系统的端口6379设置为主节点
OK
127.0.0.1:6380> info replication # 查看服务器信息
# Replication
role:slave # 可以看到当前角色已经变成了从节点
master_host:127.0.0.1 # 主节点的ip地址
master_port:6379 # 主节点的端口号
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:14
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:e2e0f8af0b7b6c3fd38ca304cfea6ebd48fe2f82
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:0

设置slave2从节点

127.0.0.1:6381> slaveof 127.0.0.1 6379 # 设置当前系统的端口6379设置为主节点
OK
127.0.0.1:6381> info replication # 查看服务器信息
# Replication
role:slave # 当前节点已经变成了从节点
master_host:127.0.0.1 # 主节点的ip地址
master_port:6379 # 主节点的端口号
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:532
slave_repl_offset:532
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:e2e0f8af0b7b6c3fd38ca304cfea6ebd48fe2f82
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:532
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:533
repl_backlog_histlen:0

登录到master节点查看master节点的信息

[root@VM-16-2-centos bin]# redis-cli -p 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=714,lag=0 # 从节点1的信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=714,lag=1 # 从节点2的信息
master_failover_state:no-failover
master_replid:e2e0f8af0b7b6c3fd38ca304cfea6ebd48fe2f82
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:714
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:714

3.4.3.2、修改配置文件 永久的

修改从节点的配置文件,两个从节点都需进行配置

# replicaof <masterip> <masterport> # 这个指令用来设置主机地址 默认是关闭的,开启之后当本服务器开启时,会自动找到对应的主机服务
replicaof 127.0.0.1 6379 # 改成这个之后  当服务开启时就会找到对应的主机
# masterauth <master-password> # 设置连接主机的密码,如果主机有密码设置密码即可,默认关闭

3.4.3.3、测试主从复制

主机可以来写入数据,从机不能写入数据只能读取数据。主机中的所有数据都会自动被从机保存

# 在master主机插入一条数据
127.0.0.1:6379> set name porterdong
OK
# 在从机slave1查询数据
127.0.0.1:6380> keys * # 可以看到在主机设置的k-v 已经复制到slave1中
1) "name"
127.0.0.1:6380> get name
"porterdong"
# 在从机slave2查询数据
127.0.0.1:6381> keys * # 在slave2中也能看到主节点的数据
1) "name"
127.0.0.1:6381> get name
"porterdong"
# 在任意一个从节点写入数据
127.0.0.1:6381> set age 20 # 写入失败,从节点只能用来读取数据
(error) READONLY You can't write against a read only replica.

测试:如果主机突然宕机了,从机依然会连接主机,但是并没有写入操作了(如果设置了哨兵模式,主机宕机后,会自动设置一个新的主机),主机开启后,从机依然可以从主机当中读取数据
如果在设置主从复制时使用的是命令行的形式,当从机宕机后再次开启不会再自动连接主机了,推荐使用配置文件进行设置,只要变成了从机,可以直接把主机数据拷贝过来

3.4.3.4、复制原理

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

  • 全量复制:当slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
  • 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步

只要从机一连接master主机,依次完全同步(全量复制)将会被自动执行,我们的数据一定可以在从机中看到!!

3.4.4、层层链路

上一个主节点连接下一个从节点,从节点再次连接下一个从节点即
image
将上边的一主二从修改为这种模式,只需要将第二个从机(6381)主机设置为第一个从机(6380)即可

#修改slave2设置主机为6380
127.0.0.1:6381> slaveof 127.0.0.1 6380
OK
# 查看主机的信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 # 此时只有一个从机
slave0:ip=127.0.0.1,port=6380,state=online,offset=16184,lag=0 # 从机信息
master_failover_state:no-failover
master_replid:e2e0f8af0b7b6c3fd38ca304cfea6ebd48fe2f82
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:16184
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:16184
# 查看第一个从机信息
127.0.0.1:6380> info replication
# Replication
role:slave # 当前状态仍为从机
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:16198
slave_repl_offset:16198
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:1 # 有一个从机连接此从机
slave0:ip=127.0.0.1,port=6381,state=online,offset=16198,lag=0 # 从机信息
master_failover_state:no-failover
master_replid:e2e0f8af0b7b6c3fd38ca304cfea6ebd48fe2f82
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:16198
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:14463
repl_backlog_histlen:1736

通过以上代码之心过后发现master,slave1,slave2形成了一个链路,下面测试在主机中存入数据,表现如何

# 主机存入数据
127.0.0.1:6379> set blog cnblogs
OK
# master从机slave1中查询
127.0.0.1:6380> get blog
"cnblogs"
# slave1从机slave2中查询
127.0.0.1:6381> get blog
"cnblogs"

可以发现都能读取到从maser存入的数据。这时候也可以完成主从复制

思考

上面的链路是这样的,master是主机,slave2是从机,但是slave1看来既是从机也是主机,下边当master服务关闭的时候,查看效果

# 关闭master主机
127.0.0.1:6379> shutdown
not connected>
# 查看slave1的状态
127.0.0.1:6380> info replication
# Replication
role:slave # 此时 slave1还是从机
master_host:127.0.0.1
master_port:6379
......
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=16733,lag=1

在有哨兵模式之前,可以手动将从机变成主机,有点谋权篡位的意思,在想要变成主机的从机上执行命令

127.0.0.1:6380> slaveof no one # 将从机变成主机
OK
127.0.0.1:6380> info replication
# Replication
role:master # 状态已经变成了主机
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=16733,lag=1
master_failover_state:no-failover
master_replid:0633be0e3eadd151cbc0f7841ce7dc8a890b53ae
master_replid2:e2e0f8af0b7b6c3fd38ca304cfea6ebd48fe2f82
master_repl_offset:16733
second_repl_offset:16734
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:14463
repl_backlog_histlen:2271
# 6380主机端口设置数据
127.0.0.1:6380> set sex girl
OK
# 6381从机端口读取数据
127.0.0.1:6381> get sex
"girl"

注意:如果使用slaveof no one让当前从机变成主机后,原来的主机重启之后需要重新设置主从关系
测试完成之后将三台服务恢复到一主二从的状态

3.5、哨兵模式(自动设置主机)

3.5.1、概述

主从切换技术的的方法时:当主服务器宕机后,需要手动把一台服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。
即谋权篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库变为主库
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
image
这里的哨兵有三个作用

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

哨兵如何判断一个redis实例是否健康

  • 每隔1s发送一次ping命令,如果超过一定时间没有反馈则认为是主观下线
  • 如果大多数哨兵都认为实例主观下线,则判断服务已经客观下线了

故障的转移步骤

  • 根据算法随机选择一个slave作为新的master,执行slaveof no one变成主机
  • 其余所有好用的节点执行slaveof新master
  • 修改故障节点的配置,添加slaveof 新master

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
image
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量到大一定值时,name哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就回通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

3.5.2、测试哨兵模式

3.5.2.1、配置哨兵配置文件

进入/usr/local/bin/config文件夹中,新建文件sentinel.conf,并编写配置文件

# sentinel monitor name host port 仲裁数
sentinel monitor myredis 127.0.0.1 6379 1 # 如果1台哨兵服务器判定该主机服务主观下线了,就认为该服务客观下线,进行转移和通知

3.5.2.2、启动哨兵进程

启动哨兵的时候需要根据刚才的配置文件进行启动,类似redis-server的启动,进入到bin目录下

[root@VM-16-2-centos bin]# redis-sentinel config/sentinel.conf # 启动哨兵进程
17765:X 28 Nov 2022 16:00:44.942 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
17765:X 28 Nov 2022 16:00:44.942 # Redis version=7.0.5, bits=64, commit=00000000, modified=0, pid=17765, just started
17765:X 28 Nov 2022 16:00:44.942 # Configuration loaded
17765:X 28 Nov 2022 16:00:44.942 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 7.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379 # 哨兵的端口号
 |    `-._   `._    /     _.-'    |     PID: 17765 # 哨兵的pid
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

17765:X 28 Nov 2022 16:00:44.943 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
17765:X 28 Nov 2022 16:00:44.950 * Sentinel new configuration saved on disk
17765:X 28 Nov 2022 16:00:44.950 # Sentinel ID is 4644c6b41c7cd50e81e0d9ade0f6f55f2cfa87e5
17765:X 28 Nov 2022 16:00:44.950 # +monitor master myredis 127.0.0.1 6379 quorum 1 # 监测的主机信息
17765:X 28 Nov 2022 16:00:44.951 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379 # 检测到第一个从机
17765:X 28 Nov 2022 16:00:44.957 * Sentinel new configuration saved on disk
17765:X 28 Nov 2022 16:00:44.957 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379 # 检测到第二个从机
17765:X 28 Nov 2022 16:00:44.962 * Sentinel new configuration saved on disk

3.5.2.3、测试哨兵

先将主机中的数据清空掉,关闭master的服务

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> shutdown # 关闭master
not connected> exit
[root@VM-16-2-centos bin]

需要等待一段时间,哨兵会自动检测并转移和通知,下边是哨兵的日志文件

17765:X 28 Nov 2022 16:04:44.442 # +sdown master myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.442 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
17765:X 28 Nov 2022 16:04:44.442 # +new-epoch 1
17765:X 28 Nov 2022 16:04:44.442 # +try-failover master myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.449 * Sentinel new configuration saved on disk
17765:X 28 Nov 2022 16:04:44.449 # +vote-for-leader 4644c6b41c7cd50e81e0d9ade0f6f55f2cfa87e5 1
17765:X 28 Nov 2022 16:04:44.449 # +elected-leader master myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.449 # +failover-state-select-slave master myredis 127.0.0.1 6379 # 发现master主机已经宕机了,开始转移
17765:X 28 Nov 2022 16:04:44.504 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.504 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.587 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.821 * Sentinel new configuration saved on disk
17765:X 28 Nov 2022 16:04:44.821 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.821 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:44.889 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:45.877 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:45.877 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
17765:X 28 Nov 2022 16:04:45.932 # +failover-end master myredis 127.0.0.1 6379 # 转移结束
17765:X 28 Nov 2022 16:04:45.932 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6380 # 根据投票选举6380为新的主机
17765:X 28 Nov 2022 16:04:45.932 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6380
17765:X 28 Nov 2022 16:04:45.932 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
17765:X 28 Nov 2022 16:04:45.942 * Sentinel new configuration saved on disk
17765:X 28 Nov 2022 16:05:15.939 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380

查看原两台从机6380端口和6381端口的信息

# 6380端口
127.0.0.1:6380> info replication
# Replication
role:master # 6380端口的redis已经变成了主机,
connected_slaves:1 # 有一台从机已经连接
slave0:ip=127.0.0.1,port=6381,state=online,offset=127186,lag=1 # 从机信息
master_failover_state:no-failover
master_replid:c4e87383a30e6d70dc277fb721e0a159e25598c2
master_replid2:9f0a02e8ec50d1f65401869402523962ceecde0d
master_repl_offset:127186
second_repl_offset:111120
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:16734
repl_backlog_histlen:110453
# 6381端口
127.0.0.1:6381> info replication
# Replication
role:slave # 从机
master_host:127.0.0.1 # 主机ip
master_port:6380 # 主机端口
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0

根据上述的测试发现,如果master主机宕机了,那么哨兵会随记一个新的主机。

3.5.2.4、宕机已恢复

思考:如果原来宕机的主机恢复了,那么会发生什么东西?

开启6379服务端,等待哨兵日志

[root@VM-16-2-centos bin]# redis-server config/redis-master.conf 

哨兵日志,发现已经将原来宕机的主机恢复好后自动变成了从机认6380端口为主机

17765:X 28 Nov 2022 16:16:18.410 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380

查看6379和6380端口的信息

# 6379
127.0.0.1:6379> info replication
# Replication
role:slave # 变成了从机
master_host:127.0.0.1
master_port:6380 # 主机ip
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:167399

# 6380
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:2 # 已经有了两个从机
slave0:ip=127.0.0.1,port=6381,state=online,offset=170769,lag=1
slave1:ip=127.0.0.1,port=6379,state=online,offset=170769,lag=0 # 恢复的6379端口已经变成了从机

哨兵模式的规则:如果宕机的主机回来了,只能归并到新的主机下,当做从机

3.5.2.5、优缺点

优点:

  1. 哨兵集群,基于主从复制模式,所有的主从配置优点都已经集成了
  2. 主从可以切换,故障可以转移,系统的可用性就会更好
  3. 哨兵模式就是从主从模式升级的,从手动变成了自动更加健壮了

缺点:

  1. Redis不好在线扩容,集群容量一旦到大上限,在线扩容很麻烦,因为会有一堆配置文件
  2. 哨兵模式的配置太多了

3.5.2.6、哨兵模式的配置文件

# Example sentinel.conf

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 哨兵sentinel的工作目录
dir /tmp

# 哨兵sentinel监控的redis主节点的 ip port# master-name 可以自己命名的主节点名字 只能由字母A-Z、数字0-9 、这三个字符",-"组成。
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passwOrd

# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-mi11iseconds <master-name><mi11iseconds>
sentinel down-after-mi1liseconds mymaster 30000

# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
# 这个数字越小,完成failover所需的时间就越长,
# 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
# 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numsTaves>
sentinel parallel-syncs mymaster 1

# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2,当一个slave从一个错误的master那里同步数据开始计算时间。直到save被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4,当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
#默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# 邮件的shell编程
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh

# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>## 目前<state>总是“failover”
# <role>是“leader"或者“observer"中的一个。# 参数 from-ip,from-port,to-ip,to-port是用来和旧的master和新的master(即旧的slave)通信的
#这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置!

3.6、缓存穿透和雪崩(面试高频)

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
image

3.6.1、缓存穿透

3.6.1.1、概念

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

3.6.1.2、解决方案

布隆过滤器

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

缓存空对象

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

但是缓存空对象会有两个问题:

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

3.6.2、缓存击穿

3.6.2.1、概述

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

3.6.2.2、解决方案

设置热点数据永不过期

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

加互斥锁

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

3.6.3、缓存雪崩

3.6.3.1、概述

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
image
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

3.6.3.2、解决方案

redis高可用

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

限流降级

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

数据预热

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

posted @ 2022-11-21 16:08  BTDong  阅读(146)  评论(0编辑  收藏  举报