redis-狂神基础版

Redis

1、nosql简介

什么是nosql

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

Not Only Structured Query Language

关系型数据库:列+行,同一个表下数据的结构是一样的。

非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。

NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。

nosql特点

  1. 方便扩展(数据之间没有关系,很好扩展!)

  2. 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)

  3. 数据类型是多样型的!(不需要事先设计数据库,随取随用)

  4. 传统的 RDBMS 和 NoSQL

    传统的 RDBMS(关系型数据库)
    - 结构化组织
    - SQL
    - 数据和关系都存在单独的表中 row col
    - 操作,数据定义语言
    - 严格的一致性
    - 基础的事务
    - ...
    
    Nosql
    - 不仅仅是数据
    - 没有固定的查询语言
    - 键值对存储,列存储,文档存储,图形数据库(社交关系)
    - 最终一致性
    - CAP定理和BASE
    - 高性能,高可用,高扩展
    - ...
    

了解:3V + 3高

  1. 海量Velume
  2. 多样Variety
  3. 实时Velocity

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

  1. 高并发
  2. 高可扩
  3. 高性能

真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的。

nosql四大分类

KV键值对

  1. ​ 新浪:Redis
  2. ​ 美团:Redis + Tair
  3. ​ 阿里、百度:Redis + Memcache

文档型数据库(bson数据格式):

1、MongoDB(掌握)

  • 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
  • MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的,NoSQL中最像关系型数据库的数据库。

2、ConthDB

列存储数据库

  1. HBase(大数据必学)
  2. 分布式文件系统

图关系数据库

用于广告推荐,社交网络

Neo4j、InfoGrid

2、redis入门

概述

redis是什么

Redis(Remote Dictionary Server ),即远程字典服务。

是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

redis能做什么

  1. 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
  2. 高效率、用于高速缓冲
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器、计数器(eg:浏览量)

特性

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

**注意:**Windows在Github上下载(停更很久了!)

Redis推荐都是在Linux服务器上搭建的。

Windows安装

windows下 redis下载地址:

https://github.com/tporadowski/redis/releases

redis的使用: 默认端口号:6379

先在系统变量中添加redis的文件路径,再进入cmd使用redis-server.exe命令开启服务。

Linux安装

xshell 家庭、教育免费版:

https://www.xshell.com/zh/free-for-home-school/

xshell连接Centos,先查看centos中ssh服务是否安装并开启,再用xshell连接!

windows与linux环境传输文件,可采用xftp

1、redis下载

https://redis.io/download/

2、传输到centos,并解压 tar -zxvf 解压包名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hGpbLNTh-1667921096466)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103182113801.png)]

3、基本环境安装:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywkp8mmV-1667921096468)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103182259609.png)]

yum install gcc-c++
# 然后进入redis目录下执行
make
# 然后执行
make install

4、redis默认安装路径 /usr/local/bin

5、将redis的配置文件复制到 程序安装目录 /usr/local/bin/dconfig

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQiFg5Lv-1667921096469)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103183717869.png)]

6、redis默认不是后台启动的,需要修改配置文件!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7kS18gYR-1667921096470)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103184059634.png)]

7、通过指定的配置文件启动redis服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zDd2edR0-1667921096471)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103184702736.png)]

8、使用redis-cli连接指定的端口号测试,Redis的默认端口6379

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DaliPZRe-1667921096472)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103184727853.png)]

9、查看redis进程是否开启 ps -ef|grep redis

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ofvo0ExR-1667921096474)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103184958321.png)]

10、关闭redis服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHLBETSh-1667921096474)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103185112736.png)]

11、再次查看进程是否关闭

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9DPPdz2-1667921096476)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103190404232.png)]

测试性能

redis-benchmark是redis自带的压力测试工具!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g0pJrx0O-1667921096477)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103190756898.png)]

简单测试性能:

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

基础知识

redis默认有16个数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DcCBtYpn-1667921096478)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103192556728.png)]

默认使用的第0个;

16个数据库为:DB 0~DB 15
默认使用DB 0 ,可以使用select n切换到DB n,dbsize可以查看当前数据库的大小,与key数量相关。

127.0.0.1:6379> config get databases # 命令行查看数据库数量databases
1) "databases"
2) "16"

127.0.0.1:6379> select 8 # 切换数据库 DB 8
OK
127.0.0.1:6379[8]> dbsize # 查看数据库大小
(integer) 0

# 不同数据库之间 数据是不能互通的,并且dbsize 是根据库中key的个数。
127.0.0.1:6379> set name sakura 
OK
127.0.0.1:6379> SELECT 8
OK
127.0.0.1:6379[8]> get name # db8中并不能获取db0中的键值对。
(nil)
127.0.0.1:6379[8]> DBSIZE
(integer) 0
127.0.0.1:6379[8]> SELECT 0
OK
127.0.0.1:6379> keys *
1) "counter:__rand_int__"
2) "mylist"
3) "name"
4) "key:__rand_int__"
5) "myset:__rand_int__"
127.0.0.1:6379> DBSIZE # size和key个数相关
(integer) 5

keys * :查看当前数据库中所有的key。

flushdb:清空当前数据库中的键值对。

flushall:清空所有数据库的键值对。

Redis是单线程的,Redis是基于内存操作的。

所以Redis的性能瓶颈不是CPU,而是机器内存和网络带宽。

那么为什么Redis的速度如此快呢,性能这么高呢?QPS达到10W+

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

  • 误区1:高性能的服务器一定是多线程的?
  • 误区2:多线程(CPU上下文会切换!)一定比单线程效率高!

核心:Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),**对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,**在内存存储数据情况下,单线程就是最佳的方案。

3、支持的五大数据类型

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

Redis-key(键值对)

在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。

下面学习的命令:

  • exists key:判断键是否存在
  • del key:删除键值对
  • move key db:将键值对移动到指定数据库
  • expire key second:设置键值对的过期时间
  • type key:查看value的数据类型
127.0.0.1:6379> keys * # 查看当前数据库所有key
(empty list or set)
127.0.0.1:6379> set name qinjiang # set key
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> move age 1 # 将键值对移动到指定数据库
(integer) 1
127.0.0.1:6379> EXISTS age # 判断键是否存在
(integer) 0 # 不存在
127.0.0.1:6379> EXISTS name
(integer) 1 # 存在
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> keys *
1) "age"
127.0.0.1:6379[1]> del age # 删除键值对
(integer) 1 # 删除个数


127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> EXPIRE age 15 # 设置键值对的过期时间

(integer) 1 # 设置成功 开始计数
127.0.0.1:6379> ttl age # 查看key的过期剩余时间
(integer) 13
127.0.0.1:6379> ttl age
(integer) 11
127.0.0.1:6379> ttl age
(integer) 9
127.0.0.1:6379> ttl age
(integer) -2 # -2 表示key过期,-1表示key未设置过期时间

127.0.0.1:6379> get age # 过期的key 会被自动delete
(nil)
127.0.0.1:6379> keys *
1) "name"

127.0.0.1:6379> type name # 查看value的数据类型
string

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgqJB53P-1667921096480)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103194935508.png)]

关于TTL命令

Redis的key,通过TTL命令返回key的过期时间,一般来说有3种:

  1. 当前key没有设置过期时间,所以会返回-1.
  2. 当前key有设置过期时间,而且key已经过期,所以会返回-2.
  3. 当前key有设置过期时间,且key还没有过期,故会返回key的正常剩余时间.

关于重命名RENAME和RENAMENX

RENAME key newkey修改 key 的名称
RENAMENX key newkey仅当 newkey 不存在时,将 key 改名为 newkey 。
更多命令学习:https://www.redis.net.cn/order/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NjSIuaZ-1667921096481)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221103195101284.png)]

String(字符串)

普通的set、get直接略过。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0S1kt4W1-1667921096482)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221104100224676.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooWkpLAj-1667921096484)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221104100242934.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jQ9rwmbb-1667921096485)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221104100319139.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcXviURs-1667921096486)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221104100530874.png)]

String类似的使用场景:value除了是字符串还可以是数字,用途举例:

  • 计数器
  • 统计多单位的数量:uid:123666:follow 0
  • 粉丝数
  • 对象存储缓存

List(列表)

基本的数据类型:列表

所有的List命令都是以 l 开头的

127.0.0.1:6379[1]> LPUSH list one #将一个值或多个值 插入列表的头部
(integer) 1
127.0.0.1:6379[1]> LPUSH list two
(integer) 2
127.0.0.1:6379[1]> LPUSH list three
(integer) 3

#######################################################
127.0.0.1:6379[1]> LRANGE list 0 -1 #获取list中的全部值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379[1]> LRANGE list 0 1 #通过区间获取具体的值
1) "three"
2) "two"

########################################################
127.0.0.1:6379[1]> RPUSH list four  #往尾部插入值
(integer) 4
127.0.0.1:6379[1]> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"

########################################################
127.0.0.1:6379[1]> LPOP list #移除list中的第一个元素
"three"
127.0.0.1:6379[1]> RPOP list #移除list最后一个元素
"four"
127.0.0.1:6379[1]> LRANGE list 0 -1
1) "two"
2) "one"

########################################################
127.0.0.1:6379[1]> LINDEX list 1 #通过下表获得list中的某一个值
"one"
127.0.0.1:6379[1]> LINDEX list 0
"two"

########################################################
127.0.0.1:6379[1]> LLEN list #返回列表的长度
(integer) 2

########################################################
移除指定的值
取关 uid

127.0.0.1:6379[1]> LREM list 1 one #移除list中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379[1]> LRANGE list 0 -1
1) "five"
2) "four"
3) "three"
4) "two"

########################################################
trim 修剪: list 截断

127.0.0.1:6379[1]> LPUSH mylist hello
(integer) 1
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "hello"
127.0.0.1:6379[1]> LPUSH mylist hello1
(integer) 2
127.0.0.1:6379[1]> LPUSH mylist hello2
(integer) 3
127.0.0.1:6379[1]> LPUSH mylist hello3
(integer) 4
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "hello3"
2) "hello2"
3) "hello1"
4) "hello"
127.0.0.1:6379[1]> LTRIM mylist 1 2 #通过下标截取指定的长度,此时list已被修改,只剩下截取的部分
OK
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "hello2"
2) "hello1"

########################################################
rpoplpush  #移除列表中的最后一个元素,并将其移到新列表中

127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "hello2"
2) "hello1"
127.0.0.1:6379[1]> RPOPLPUSH mylist otherlist
"hello1"
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "hello2"
127.0.0.1:6379[1]> LRANGE otherlist 0 -1
1) "hello1"

########################################################
lset #将列表中指定下标的值替换为另一个值,更新操作
127.0.0.1:6379[1]> EXISTS mylist #判断是否存在某个列表
(integer) 1
127.0.0.1:6379[1]> LSET mylist 0 item #
OK
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "item"

########################################################
linsert #将某个具体的value插入到列表中某个元素的前面或后面

127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "item"
127.0.0.1:6379[1]> LINSERT mylist before "item" "other"  #插入到指定元素的前面
(integer) 2
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "other"
2) "item"
127.0.0.1:6379[1]> LINSERT mylist after  "item" "hello" #插入到指定元素的后面
(integer) 3
127.0.0.1:6379[1]> LRANGE mylist 0 -1
1) "other"
2) "item"
3) "hello"


小结

  • list实际上是一个链表,before Node after , left, right 都可以插入值
  • 如果key不存在,则创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,空链表,也代表不存在
  • 在两边插入或者改动值,效率最高!修改中间元素,效率相对较低

应用:

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

Set(集合)

set集合中的值是不能重复的!

127.0.0.1:6379[1]> SADD myset hello #set集合中添加元素
(integer) 1
127.0.0.1:6379[1]> SADD myset hy
(integer) 1
127.0.0.1:6379[1]> SADD myset love 
(integer) 1
127.0.0.1:6379[1]> SMEMBERS myset #查看指定set集合的所有值
1) "hello"
2) "love"
3) "hy"
#######################################
127.0.0.1:6379[1]> keys *
1) "myset"
127.0.0.1:6379[1]> SISMEMBER myset hello #判断某一个值是否在set中
(integer) 1
127.0.0.1:6379[1]> SISMEMBER myset hhhh
(integer) 0

########################################
127.0.0.1:6379[1]> scard myset #获取set集合中的元素个数
(integer) 3

########################################
srem   #移除集合中指定的元素
127.0.0.1:6379[1]> srem myset hello
(integer) 1
127.0.0.1:6379[1]> SISMEMBER myset hello
(integer) 0
127.0.0.1:6379[1]> SMEMBERS myset
1) "love"
2) "hy"

########################################
SRANDMEMBER 无序不重复集合,抽随机!
127.0.0.1:6379[1]> SMEMBERS myset
1) "love"
2) "hy"
127.0.0.1:6379[1]> sadd myset today 
(integer) 1
127.0.0.1:6379[1]> sadd myset hello
(integer) 1
127.0.0.1:6379[1]> sadd myset pig
(integer) 1
127.0.0.1:6379[1]> SMEMBERS myset
1) "pig"
2) "hy"
3) "love"
4) "today"
5) "hello"
127.0.0.1:6379[1]> SRANDMEMBER myset 2 #随机抽取两个元素
1) "love"
2) "today"
127.0.0.1:6379[1]> SRANDMEMBER myset 2
1) "pig"
2) "love"
127.0.0.1:6379[1]> SRANDMEMBER myset #随机抽取一个元素
"love"


########################################
删除指定的key,随机删除key!

127.0.0.1:6379[1]> SMEMBERS myset
1) "pig"
2) "hy"
3) "love"
4) "today"
5) "hello"
127.0.0.1:6379[1]> spop myset 
"love"
127.0.0.1:6379[1]> spop myset
"hy"
127.0.0.1:6379[1]> SMEMBERS myset
1) "pig"
2) "today"
3) "hello"

########################################
将指定的值 移动到另一个set集合中
127.0.0.1:6379[1]> SMEMBERS myset
1) "pig"
2) "today"
3) "hello"
127.0.0.1:6379[1]> SMOVE myset hyset hello #将指定的值移动到另一个set集合中
(integer) 1
127.0.0.1:6379[1]> SMEMBERS myset
1) "pig"
2) "today"
127.0.0.1:6379[1]> SMEMBERS hyset
1) "hello"

########################################
微博,B站,共同关注!(并集)
数字集合类:
- 差集 SDIFF
- 交集 SINTER
- 并集 SUNION

127.0.0.1:6379[1]> FLUSHdb
OK
127.0.0.1:6379[1]> sadd myset1 a
(integer) 1
127.0.0.1:6379[1]> sadd myset1 b
(integer) 1
127.0.0.1:6379[1]> sadd myset1 c
(integer) 1
127.0.0.1:6379[1]> sadd myset2 c
(integer) 1
127.0.0.1:6379[1]> sadd myset2 d
(integer) 1
127.0.0.1:6379[1]> sadd myset2 e
(integer) 1
127.0.0.1:6379[1]> sdiff myset1 myset2 #差集
1) "a"
2) "b"
127.0.0.1:6379[1]> SINTER myset1 myset2 #交集
1) "c"
127.0.0.1:6379[1]> SUNION myset1 myset2 #并集
1) "e"
2) "c"
3) "b"
4) "a"
5) "d"


微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!

共同关注,共同爱好,二度好友,推荐好友!

Hash(哈希)

Map集合,key-map!

value值是一个map集合!

hash特别适合用于存储对象。

------------------------HSET--HMSET--HSETNX----------------
127.0.0.1:6379> HSET studentx name sakura # 将studentx哈希表作为一个对象,设置name为sakura
(integer) 1
127.0.0.1:6379> HSET studentx name gyc # 重复设置field进行覆盖,并返回0
(integer) 0
127.0.0.1:6379> HSET studentx age 20 # 设置studentx的age为20
(integer) 1
127.0.0.1:6379> HMSET studentx sex 1 tel 15623667886 # 设置sex为1,tel为15623667886
OK
127.0.0.1:6379> HSETNX studentx name gyc # HSETNX 设置已存在的field 若存在则不能设置
(integer) 0 # 失败
127.0.0.1:6379> HSETNX studentx email 12345@qq.com #若不存在 则可以设置
(integer) 1 # 成功

----------------------HEXISTS--------------------------------
127.0.0.1:6379> HEXISTS studentx name # name字段在studentx中是否存在
(integer) 1 # 存在
127.0.0.1:6379> HEXISTS studentx addr
(integer) 0 # 不存在

-------------------HGET--HMGET--HGETALL-----------
127.0.0.1:6379> HGET studentx name # 获取studentx中name字段的value
"gyc"
127.0.0.1:6379> HMGET studentx name age tel # 获取studentx中name、age、tel字段的value
1) "gyc"
2) "20"
3) "15623667886"
127.0.0.1:6379> HGETALL studentx # 获取studentx中所有的field及其value
 1) "name"
 2) "gyc"
 3) "age"
 4) "20"
 5) "sex"
 6) "1"
 7) "tel"
 8) "15623667886"
 9) "email"
10) "[email protected]"


--------------------HKEYS--HLEN--HVALS--------------
127.0.0.1:6379> HKEYS studentx # 查看studentx中所有的field
1) "name"
2) "age"
3) "sex"
4) "tel"
5) "email"
127.0.0.1:6379> HLEN studentx # 查看studentx中的字段数量
(integer) 5
127.0.0.1:6379> HVALS studentx # 查看studentx中所有的value
1) "gyc"
2) "20"
3) "1"
4) "15623667886"
5) "[email protected]"

-------------------------HDEL--------------------------
127.0.0.1:6379> HDEL studentx sex tel # 删除studentx 中的sex、tel字段
(integer) 2
127.0.0.1:6379> HKEYS studentx
1) "name"
2) "age"
3) "email"

-------------HINCRBY--HINCRBYFLOAT------------------------
127.0.0.1:6379> HINCRBY studentx age 1 # studentx的age字段数值+1
(integer) 21
127.0.0.1:6379> HINCRBY studentx name 1 # 非整数字型字段不可用
(error) ERR hash value is not an integer
127.0.0.1:6379> HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6
"90.8"

Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!Hash更适合于对象的存储,Sring更加适合字符串存储!

Zset(有序集合)

在set 的基础上,增加了一个值,set k1 v1 , zset k1 score1 v1

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

score相同:按字典顺序排序

有序集合的成员是唯一的,但分数(score)却可以重复。

-------------------ZADD--ZCARD--ZCOUNT--------------
127.0.0.1:6379[1]> ZADD zset 1 one 
(integer) 1
127.0.0.1:6379[1]> ZADD zset 2 two 
(integer) 1
127.0.0.1:6379[1]> ZADD zset 3 three 
(integer) 1
127.0.0.1:6379[1]> ZRANGE zset 0 -1
1) "one"
2) "two"
3) "three"

127.0.0.1:6379[1]> ZADD salary 2500 xiaohong 
(integer) 1
127.0.0.1:6379[1]> ZADD salary 5000 zhangsan
(integer) 1
127.0.0.1:6379[1]> ZADD salary 500 hy
(integer) 1
127.0.0.1:6379[1]> ZRANGEBYSCORE salary -inf +inf #按salary从小到大排序
1) "hy"
2) "xiaohong"
3) "zhangsan"

127.0.0.1:6379[1]> ZREVRANGE salary 0 -1 #按salary从大到小排序
1) "zhangsan"
2) "xiaohong"
3) "hy"


127.0.0.1:6379[1]> ZRANGEBYSCORE salary -inf +inf  withscores #带上薪水值升序查询
1) "hy"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"

127.0.0.1:6379[1]> ZCOUNT zset 0 2 # 获取score在 [0,2]区间的成员数量
(integer) 2


----------------ZINCRBY--ZSCORE--------------------------
127.0.0.1:6379> ZINCRBY myzset 5 m2 # 将成员m2的score +5
"7"
127.0.0.1:6379> ZSCORE myzset m1 # 获取成员m1的score
"1"
127.0.0.1:6379> ZSCORE myzset m2
"7"

--------------ZRANK--ZRANGE-----------------------------------
127.0.0.1:6379> ZRANK myzset m1 # 获取成员m1的索引,索引按照score排序,score相同索引值按字典顺序顺序增加
(integer) 0
127.0.0.1:6379> ZRANK myzset m2
(integer) 2
127.0.0.1:6379> ZRANGE myzset 0 1 # 获取索引在 0~1的成员
1) "m1"
2) "m3"
127.0.0.1:6379> ZRANGE myzset 0 -1 # 获取全部成员
1) "m1"
2) "m3"
3) "m2"

#testset=>{abc,add,amaze,apple,back,java,redis} score均为0
------------------ZRANGEBYLEX---------------------------------
127.0.0.1:6379> ZRANGEBYLEX testset - + # 返回所有成员
1) "abc"
2) "add"
3) "amaze"
4) "apple"
5) "back"
6) "java"
7) "redis"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 0 3 # 分页 按索引显示查询结果的 0,1,2条记录
1) "abc"
2) "add"
3) "amaze"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 3 3 # 显示 3,4,5条记录
1) "apple"
2) "back"
3) "java"
127.0.0.1:6379> ZRANGEBYLEX testset (- [apple # 显示 (-,apple] 区间内的成员
1) "abc"
2) "add"
3) "amaze"
4) "apple"
127.0.0.1:6379> ZRANGEBYLEX testset [apple [java # 显示 [apple,java]字典区间的成员
1) "apple"
2) "back"
3) "java"

-----------------------ZRANGEBYSCORE---------------------
127.0.0.1:6379> ZRANGEBYSCORE myzset 1 10 # 返回score在 [1,10]之间的的成员
1) "m1"
2) "m3"
3) "m2"
127.0.0.1:6379> ZRANGEBYSCORE myzset 1 5
1) "m1"
2) "m3"

--------------------ZLEXCOUNT-----------------------------
127.0.0.1:6379> ZLEXCOUNT testset - +
(integer) 7
127.0.0.1:6379> ZLEXCOUNT testset [apple [java
(integer) 3

------------------ZREM--ZREMRANGEBYLEX--ZREMRANGBYRANK--ZREMRANGEBYSCORE-------------------------------
127.0.0.1:6379> ZREM testset abc # 移除成员abc
(integer) 1
127.0.0.1:6379> ZREMRANGEBYLEX testset [apple [java # 移除字典区间[apple,java]中的所有成员
(integer) 3
127.0.0.1:6379> ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成员
(integer) 2
127.0.0.1:6379> ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成员
(integer) 2


# testset=> {abc,add,apple,amaze,back,java,redis} score均为0
# myzset=> {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)}
----------------ZREVRANGE--ZREVRANGEBYSCORE--ZREVRANGEBYLEX-----------
127.0.0.1:6379> ZREVRANGE myzset 0 3 # 按score递减排序,然后按索引,返回结果的 0~3
1) "m9"
2) "m7"
3) "m4"
4) "m3"
127.0.0.1:6379> ZREVRANGE myzset 2 4 # 返回排序结果的 索引的2~4
1) "m4"
2) "m3"
3) "m2"
127.0.0.1:6379> ZREVRANGEBYSCORE myzset 6 2 # 按score递减顺序 返回集合中分数在[2,6]之间的成员
1) "m4"
2) "m3"
3) "m2"
127.0.0.1:6379> ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典区间的成员
1) "java"
2) "back"
3) "apple"
4) "amaze"

-------------------------ZREVRANK------------------------------
127.0.0.1:6379> ZREVRANK myzset m7 # 按score递减顺序,返回成员m7索引
(integer) 1
127.0.0.1:6379> ZREVRANK myzset m2
(integer) 4


# mathscore=>{(xm,90),(xh,95),(xg,87)} 小明、小红、小刚的数学成绩
# enscore=>{(xm,70),(xh,93),(xg,90)} 小明、小红、小刚的英语成绩
-------------------ZINTERSTORE--ZUNIONSTORE-----------------------------------
127.0.0.1:6379> ZINTERSTORE sumscore 2 mathscore enscore # 将mathscore enscore进行合并 结果存放到sumscore
(integer) 3
127.0.0.1:6379> ZRANGE sumscore 0 -1 withscores # 合并后的score是之前集合中所有score的和
1) "xm"
2) "160"
3) "xg"
4) "177"
5) "xh"
6) "188"

127.0.0.1:6379> ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取两个集合的成员score最小值作为结果的
(integer) 3
127.0.0.1:6379> ZRANGE lowestscore 0 -1 withscores
1) "xm"
2) "70"
3) "xg"
4) "87"
5) "xh"
6) "93"

应用案例:

  • set排序 存储班级成绩表 工资表排序!
  • 普通消息,1.重要消息 2.带权重进行判断
  • 排行榜应用实现,取Top N测试

4、三种特殊类型

Geospatial(地理位置)

朋友的定位、微信附近的人、打车距离计算?

Redis的Geo在Redis3.2版本就推出了!

使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用

六个命令

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3qindzHw-1667921096488)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108103245252.png)]

getadd

getadd添加地理位置

规则:两极无法添加,一般会下载城市数据,直接通过java程序一次性导入!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q7aY4uw5-1667921096488)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106140722538.png)]

geopos

获取城市指定的经度和纬度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pLijQhUr-1667921096490)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106140846662.png)]

geodist

获取两人(两城市)的直线距离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EIPLQXx9-1667921096491)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106141006873.png)]

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

附近的人?(获得所有附近人的地址、定位!)通过半径查询

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tf0t0ojn-1667921096491)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106141423350.png)]

georadiusbymember

找出位于指定元素周围的其它元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dAgSUcs6-1667921096493)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106141559841.png)]

GEO底层的实现原理其实就是Zset!故我们可以使用Zset命令操作geo!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H4v7tr5c-1667921096494)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106142029358.png)]

Hyperloglog(基数统计)

什么是基数

数据集中不重复的元素的个数。

B:{2,3,4,5,5,7} 基数=5

简介

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

花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。

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

其底层使用string数据类型

网页UV(一个人访问多次网站,还是算作一个人!)

测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Srr2ZXIF-1667921096495)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221106143311277.png)]

如果允许容错,那么一定可以使用Hyperloglog !

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

BitMaps(位图)

位存储

统计用户信息,活跃、不活跃! 登录 、未登录!两种状态的,都可以使用Bitmaps!

信息状态只有 0 和 1

测试

------------setbit--getbit--------------
127.0.0.1:6379> setbit sign 0 1 # 设置sign的第0位为 1 
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 设置sign的第2位为 1  不设置默认 是0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> type sign
string

#查看某一天是否打卡
127.0.0.1:6379> getbit sign 2 # 获取第2位的数值
(integer) 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 4 # 未设置默认是0
(integer) 0

-----------bitcount----------------------------
#统计打卡的天数
127.0.0.1:6379> BITCOUNT sign # 统计sign中为1的位数
(integer) 4

5、基本事务操作

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

Redis事务本质:一组命令的集合。

----------------- 队列 set set set 执行 -------------------

事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。

- 一次性
- 顺序性
- 排他性
Redis事务没有隔离级别的概念
Redis单条命令是保证原子性的,但是事务不保证原子性!

Redis事务操作过程

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

所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(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> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事务执行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
   2) "k2"
   3) "k1"

取消事务(discard)

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> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC 
(error) ERR EXEC without MULTI # 当前未开启事务
127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行
(nil)

事务错误

代码语法错误(编译时异常)所有的命令都不执行

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> error k1 # 这是一条语法错误命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队 
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> get k1 
(nil) # 其他命令并没有被执行

代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** >>> 所以不保证事务原子性

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 # 这条命令逻辑错误(对字符串进行增量) 语法是正确的
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 运行时报错
4) "v2" # 其他命令正常执行

# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。

监控 Watch (面试常问)

悲观锁

  • 很悲观,认为什么时候都会出现问题,无论做什么都会加锁

乐观锁

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

Redis监视测试

正常执行成功!

127.0.0.1:6379> set money 100 # 设置余额:100
OK
127.0.0.1:6379> set use 0 # 支出使用:0
OK
127.0.0.1:6379> watch money # 监视money (上锁)
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行
1) (integer) 80
2) (integer) 20

测试多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion)

我们启动另外一个客户端模拟插队线程。

线程1:

127.0.0.1:6379> watch money # money上锁
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> 	# 此时事务并没有执行

模拟线程插队,线程2:

127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money
(integer) 600

回到线程1,执行事务:

127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败
(nil) # 没有结果,说明事务执行失败

127.0.0.1:6379> get money # 线程2 修改生效
"600"
127.0.0.1:6379> get use # 线程1事务执行失败,数值没有被修改
"0"

解锁获取最新值,然后再加锁进行事务。

unwatch进行解锁。

注意:每次提交执行exec后都会自动释放锁,不管是否成功

6、Jedis

使用Java来操作Redis,Jedis是Redis官方推荐使用的Java连接redis的客户端。

测试

1、导入依赖


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

<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.70version>
dependency>

2、编码测试

  • 连接数据库
  • 操作命令
  • 断开连接
public class TestPing {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.xx.xxx", 6379);
        String response = jedis.ping();
        System.out.println(response); // 输出PONG
    }
}

常用的API:

String

Lsit

Set

Hash

Zset

3、事务

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("39.99.xxx.xx", 6379);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "kuangshen");
        // 开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();
        // jedis.watch(result)
        try {
            multi.set("user1", result);
            multi.set("user2", result);
            // 执行事务
            multi.exec();
        }catch (Exception e){
            // 放弃事务
            multi.discard();
        } finally {
            // 关闭连接
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();
        }
    }
}

7、SpringBoot集成Redis

7.1、集成测试

springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。

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

lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式

源码分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qFt5JhEy-1667921096496)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221107093714841.png)]

集成测试

1、导入依赖

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

2、连接配置

# 配置redis
spring.redis.host=39.99.xxx.xx
spring.redis.port=6379
#与连接池相关的配置。注意使用时一定使用Lettuce的连接池。

3、测试

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {

        // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
        // opsForValue 操作字符串 类似String
        // opsForList 操作List 类似List
        // opsForHah

        // 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD

        // 获取连接对象
        //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //connection.flushDb();
        //connection.flushAll();

        redisTemplate.opsForValue().set("mykey","我是hy"); //真实开发一般都是用json传输对象
        System.out.println(redisTemplate.opsForValue().get("mykey"));
    }
}


/*
	结果出现中文乱码的情况:
	IDEA 无 ,Redis  有
	这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否则就全是乱码。
	采用额默认的序列化器是采用JDK序列化器
	解决:可自行定制RedisTemplate对其进行修改
*/

7.2、定制RedisTemplate

@Configuration
public class RedisConfig {

    //固定模板 开箱即用
   @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        // 将template 泛型设置为 
        RedisTemplate<String, Object> template = new RedisTemplate();
        // 连接工厂,不必修改
        template.setConnectionFactory(redisConnectionFactory);
        /*
         * 序列化设置
         */
        // key、hash的key 采用 String序列化方式
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // value、hash的value 采用 Jackson 序列化方式
        template.setValueSerializer(RedisSerializer.json());
        template.setHashValueSerializer(RedisSerializer.json());
        template.afterPropertiesSet();
        
        return template;
    }
}

7.3、RedisUtils工具类

使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。

package com.zxy.demo.redis;

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

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

/**
 * Redis工具类
 * @author ZENG.XIAO.YAN
 * @date   2018年6月7日
 */
@Component
public final class RedisUtil {
	
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;

	// =============================common============================
	/**
	 * 指定缓存失效时间
	 * @param key 键
	 * @param time 时间(秒)
	 * @return
	 */
	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)
	 * @return
	 */
	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)
	 * @return
	 */
	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
	 * @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 对应多个键值
	 * @return true 成功 false 失败
	 */
	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)
	 * @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 键
	 * @return
	 */
	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 键
	 * @return
	 */
	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代表所有值
	 * @return
	 */
	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 键
	 * @return
	 */
	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 值
	 * @param time 时间(秒)
	 * @return
	 */
	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 时间(秒)
	 * @return
	 */
	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 值
	 * @param time 时间(秒)
	 * @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;
		}
	}
}

8、Redis.conf详解

启动的时候,就是通过配置文件来启动的!

单位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jH1WIyiU-1667921096498)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221107182107873.png)]

​ 配置文件unit单位对大小写不敏感!

包含

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-elKPppdn-1667921096498)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221107182209768.png)]

可以引入其它的配置文件

网络

bind 127.0.0.1 #绑定的ip
protected-mode yes #保护模式:开启
port 6379 #端口设置 

GENERAL 通用

daemonize yes  #以守护进程的方式运行,默认为no,需要手动开启
pidfile /var/run/redis_6379.pid #如果以后台的方式运行,就需要指定一个pid文件!

#日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)  生产环境使用
# warning (only very important / critical messages are logged)
loglevel notice
logfile ""   #日志的文件位置名
databases 16 #数据库的数量,默认为16个
always-show-logo no #是否总是显示logo

快照

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJS2LiDN-1667921096500)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221107183408716.png)]

save 3600 1#如果3600s内,如果至少有1个key被修改,便进行持久化
save 300 100 
save 60 10000

stop-writes-on-bgsave-error yes #持久化如果出错,是否还继续工作

rdbcompression yes #是否压缩 rdb 文件。需要压缩一些cpu资源

rdbchecksum yes #保存rdb文件的时候,是否进行错误的校验

dir ./   #rdb 文件保存目录


REPLICATION 复制(与主从复制相关)

SECURITY 安全

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0wQeFq5-1667921096502)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221107184215422.png)]

设置redis的密码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jYRBTbbG-1667921096502)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221107184358214.png)]

客户端限制 CLIENTS

maxclients  10000 #设置连接上redis的最大客户端数量
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 : 永不过期,返回错误

APPEND ONLY MODE aof配置

appendonly no #默认是不开启aof模式,默认是rdb方式持久化
appendfilename "appendonly.aof" #aof持久化文件的名字

# appendfsync always #每次修改都会 sync同步
appendfsync everysec #每秒执行一次sync,可能会丢失这1s内的数据!
# appendfsync no #不执行sync,这时操作系统自己同步数据,速度最快!

9、Redis持久化 (RDB与AOF)

面试和工作,持久化都是重点!

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

9.1、RDB(Redis Databases)

什么是RDB

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrvUjQCa-1667921096504)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108095556686.png)]

在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;

默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。

工作原理

在进行 RDB 的时候,redis 的主线程是不会做 io 操作的,主线程会 fork 一个子线程来完成该操作;

1、Redis 调用forks。同时拥有父进程和子进程。
2、子进程将数据集写入到一个临时 RDB 文件中。
3、当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

触发机制

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

2、执行flushall命令,也会触发我们的rdb原则

3、退出redis,也会自动产生rdb文件

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NsFW3Iud-1667921096505)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108094710150.png)]

如何恢复rdb文件!

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

2、查看需要存在的位置

[root@localhost ~]# redis-cli -p 6379
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin" #如果在这个目录下存在dump.rdb文件,启动就会自动恢复其中的数据!

优点、缺点

优点:

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

缺点:

  • 需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改的数据就没有了。
  • fork进程时,会占用一定的内存空间!

9.2、AOF(Append Only File)

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

AOF是什么

快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-StzEBeV1-1667921096506)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108100316098.png)]

appendonly no yes则表示启用AOF

默认是不开启的,我们需要手动配置,然后重启redis,就可以生效了!

如果这个aof文件有错误,这时候redis是启动不起来的,我需要修复这个aof文件。

redis给我们提供了一个工具redis-check-aof --fix

优点、缺点

appendonly yes  # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用
appendfilename "appendonly.aof"  #aof持久化的文件名

# appendfsync always # 每次修改都会sync 消耗性能
appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据
# appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快

优点:

  1. 每一次修改都会同步,文件的完整性会更加好
  2. 没秒同步一次,可能会丢失一秒的数据
  3. 从不同步,效率最高

缺点:

  1. 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
  2. Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化

10、Redis发布订阅

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

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

订阅/发布消息图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gd3iIjw-1667921096507)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108102747477.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Wqq3T4M-1667921096509)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108102908191.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TzxcsQj0-1667921096510)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108102925977.png)]

发布订阅命令:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZXHj8vTd-1667921096511)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108103351808.png)]

测试

------------订阅端----------------------
127.0.0.1:6379> SUBSCRIBE sakura # 订阅sakura频道
Reading messages... (press Ctrl-C to quit) # 等待接收消息
1) "subscribe" # 订阅成功的消息
2) "sakura"
3) (integer) 1
1) "message" # 接收到来自sakura频道的消息 "hello world"
2) "sakura"
3) "hello world"
1) "message" # 接收到来自sakura频道的消息 "hello i am sakura"
2) "sakura"
3) "hello i am sakura"

--------------消息发布端-------------------
127.0.0.1:6379> PUBLISH sakura "hello world" # 发布消息到sakura频道
(integer) 1
127.0.0.1:6379> PUBLISH sakura "hello i am sakura" # 发布消息
(integer) 1

-----------------查看活跃的频道------------
127.0.0.1:6379> PUBSUB channels
1) "sakura"

使用场景

  1. 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
  2. 多人在线聊天室。

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

11、Redis主从复制

11.1、概念

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

默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qSi0Qfij-1667921096512)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108114546092.png)]

​ 主从复制,读写分离! 80%情况下都是在进行读操作,减缓服务器压力。

11.2、作用

1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
2、故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
3、负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写 的场景下,通过多个从节点分担负载,提高并发量。
4、高可用基石:主从复制还是哨兵和集群能够实施的基础。

11.3、环境配置

只配置从库,不用配置主库!

[root@localhost ~]# redis-cli -p 6379
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master  #角色 master
connected_slaves:0 #没有从机
master_failover_state:no-failover
master_replid:86cc888247e7a73e873f71d53ec617b3a518492b
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

添加从机:

copy出主从机的conf文件,并修改配置!

[root@localhost dconfig]# cp redis.conf  redis79.conf
[root@localhost dconfig]# ls
redis79.conf  redis.conf
[root@localhost dconfig]# cp redis.conf  redis80.conf 
[root@localhost dconfig]# cp redis.conf  redis81.conf
[root@localhost dconfig]# ls
redis79.conf  redis80.conf  redis81.conf  redis.conf

需修改的信息:

1、端口

2、pid名字

3、log文件名

4、dump. rdb名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHlGFLxg-1667921096513)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108182155560.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKWAJBpW-1667921096514)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108182234244.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0DRBe6p-1667921096515)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108182255130.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYwhfiOc-1667921096517)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108182354215.png)]

主从机分别启动,并查看进程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wm5yXVHx-1667921096518)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108183152269.png)]

11.4、一主二从

默认情况下,每台redis服务器都是主节点

我们一般情况下只用配置从机就好了!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fvxLHyd9-1667921096519)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108185741020.png)]

主机上查看从机:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJZN4tdt-1667921096520)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108185906046.png)]

我们这里是使用命令搭建,是暂时的,==真实开发中应该在从机的配置文件中进行配置,==这样的话是永久的。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gtlqqcSC-1667921096521)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108190633973.png)]

主机添加键值对:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oIqumlsi-1667921096522)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108190503228.png)]

从机直接从主机读取到:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ieffdOOA-1667921096522)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108190539518.png)]

1、当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。

2、当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从 机,又可以获取到主机的所有数据。这里就要提到一个同步原理。

3、第1条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:

  • 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机
  • 使用哨兵模式(自动选举)

如果主机断开了连接,我们可以使用SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,只能重新配置!

11.5、哨兵模式(重点)

自动选举主机的模式!

哨兵模式能够后台监控主句是否故障,如果故障了根据投票数自动将从机转换为主机!

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

单哨兵模式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6KQIuOG-1667921096523)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108193727374.png)]

哨兵的作用:

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

多哨兵模式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBS9UaCP-1667921096524)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108193910690.png)]

测试

1、添加哨兵的核心配置文件sentinel.conf:

[root@localhost dconfig]# vim sentinel.conf
#添加配置信息如下:
#格式 sentinel monitor 被监控的名称 host port 1
sentinel monitor mymaster 127.0.0.1 6379 1

后面的数字1,代表主机挂了,slave投票看让谁接替成为主机。

2、启动哨兵

#启动哨兵
[root@localhost bin]# redis-sentinel dconfig/sentinel.conf

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tV2uZhNO-1667921096525)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108195556723.png)]

关闭主机6379,让哨兵自动选举出主机:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-84PEGDqC-1667921096525)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108200222980.png)]

由上可看出,选举出的主机为6381

优点:

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

缺点:

  1. Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
  2. 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项

哨兵模式的全部配置

完整的哨兵模式配置文件 sentinel.conf

# Example sentinel.conf
 
# 哨兵sentinel实例运行的端口 默认26379
port 26379
 
# 哨兵sentinel的工作目录
dir /tmp
 
# 哨兵sentinel监控的redis主节点的 ip port 
# master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor    
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass  
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
 
 
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds  
sentinel down-after-milliseconds mymaster 30000
 
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs  
sentinel parallel-syncs mymaster 1
 
 
 
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面: 
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。  
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout  
sentinel failover-timeout mymaster 180000
 
# SCRIPTS EXECUTION
 
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
 
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script  
  sentinel notification-script mymaster /var/redis/notify.sh
 
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#       
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。 
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script  
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh #一般是运维来配置

12、缓存穿透和雪崩(面试高频、工作常用)

解决Redis的高可用问题!

12.1、缓存穿透(查不到)

概念

在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。

解决方案

1、布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,从而避免了对底层存储系统的查询压力。

image-20221108225722124

2、缓存空对象

一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。

image-20221108225908834

此方法存在两个问题:

1、存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间

2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

12.2、缓存击穿(量太大,缓存过期)

概念

相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。

比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。

解决方案

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

这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。

2、加互斥锁(分布式锁)

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

保证同时刻只有一个线程访问。这样对锁的要求就十分高。

12.3、缓存雪崩

概念

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njPEDUQS-1667921096527)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108231350493.png)]

大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2w8w0WwI-1667921096528)(C:\Users\Silence\AppData\Roaming\Typora\typora-user-images\image-20221108231218620.png)]

案例:双十一 会停掉一些服务,以此保证主要服务的高可用!

解决方案

1、redis高可用

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

2、限流降级

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

3、数据预热

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

你可能感兴趣的:(数据库,redis,nosql)