Redis笔记

Redis笔记

关于NoSQL概述

一、NoSQL的发展历程

1、单机MySQL时代

90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题:

  1. 数据量增加到一定程度,单机数据库就放不下了(MySQL中数据超过300万条,需要添加索引)
  2. 数据的索引(B+ Tree),一个机器内存也存放不下
  3. 访问量变大后(读写混合),一台服务器承受不住。

2、Memcached(缓存) + Mysql + 垂直拆分(读写分离)

网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效

率!

优化过程经历了以下几个过程:

  1. 优化数据库的数据结构和索引(难度大)
  2. 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了
  3. MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。

3、分库分表 + 水平拆分 + Mysql集群

4、如今最近的年代

如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。目前一个基本的互联网项目:

二、为什么要用NoSQL

用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!这时候我们就需要使用NoSQL数据库的,Nosql可以很好的处理以上的情况!

1.NoSQL是什么:

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

Not Only Structured Query Language

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

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

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

2…NoSQL特点:

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

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

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

4.传统的 RDBMS 和 NoSQL

传统的 RDBMS(关系型数据库)

结构化组织
SQL
数据和关系都存在单独的表中 row col
操作,数据定义语言
严格的一致性
基础的事务

Nosql

不仅仅是数据
没有固定的查询语言
键值对存储,列存储,文档存储,图形数据库(社交关系)
最终一致性
CAP定理和BASE
高性能,高可用,高扩展

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

  • 海量Velume
  • 多样Variety
  • 实时Velocity

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

  • 高并发
  • 高可扩
  • 高性能

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

推荐文章:《阿里云的这群疯子》和 阿里云技术架构

三、NoSQL的四大分类

1.KV键值对:

  • 新浪:Redis

  • 美团:Redis+Tair

  • 阿里、百度:Redis + Memcache

2.文档型数据库 (bson格式和 json一样)

  • MongoDB (一般必须掌握!)

    • MongoDB是一个基于分布式文件存储的数据库,c++编写,主要用来处理大量文档
    • MongoDB是一个介于关系型数据库RDBMS和非关系型数据库NoSQL的中间产品(最像关系型数据库!)
  • CouchDB

3.列存储数据库

  • Hbase
  • 分布式文件系统

4.图关系数据库

  • 用于存放关系的而非图片!比如:朋友圈社交网络、产品推荐
  • Neo4j,InfoGrid

Redis入门

一.概述

Redis是什么?

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k4Yla4sZ-1624986052976)(Redis笔记.assets/image-20210621223500428.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HCxc7Kiz-1624986052977)(Redis笔记.assets/image-20210621214644521.png)]

  • redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件(简称:数据的持久化),并且在此基础上实现了master-slave(主从)同步(简称:主从复制)。

  • 免费和开源!是当前最热门的非关系型数据库之一,也被人们称作结构化数据库!

Redis能干嘛?

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

Redis的一些特性

  1. 多样的数据类型

  2. 持久化

  3. 集群

  4. 事务

学习需要的东西

官网:https://redis.io/

中文网:http://www.redis.cn/

下载地址:

注意:windows在GitHub下载 (停更很久了,官方不推荐在windows使用Redis)因此,我们基于Linux学习!

二.Windows安装

1.下载安装包:https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100


2.下载好解压到自己电脑windows就可以了!Redis十分的小只有5M

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLReSdvX-1624986052979)(Redis笔记.assets/image-20210621221943710.png)]

3.开启Redis,双击运行服务即可!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m1aGGFJY-1624986052980)(Redis笔记.assets/image-20210621222211339.png)]

默认端口:6379

4.使用Redis客户端连接redis服务

在客户端做一个简单的测试,以<k,v>形式存储一个数据,并取出!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ke4BAomY-1624986052982)(Redis笔记.assets/image-20210621222526022.png)]

记住:windows下使用虽然简单,但是Redis推荐我们使用Linux去开发使用!

地址:https://www.redis.io/topics/introduction

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7d69We4-1624986052983)(Redis笔记.assets/image-20210621223246138.png)]

三.Linux安装

1.下载安装包:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7UkURJl-1624986052984)(Redis笔记.assets/image-20210621223921042.png)]
2.解压完成即可!

3.进入Redis解压后的文件,可以看到redis的配置文件!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ta4W891o-1624986052985)(Redis笔记.assets/image-20210621225312591.png)]

4.基本的环境安装

 1. yum install gcc-c++   安装gcc
 2. gcc -v  查看gcc版本
 3. make
 4. make install

执行完make后执行make install

5.可以看到我们的redis被安装在/usr/local/bin(默认路径)下


6.将redis配置文件复制到我们的当前目录下(拷贝到当前目录下新建的一个config),之后就使用这个拷贝过来的配置文件redis.config去启动

7.默认我们的redis不是后台启动的,所以使用vim修改配置文件redis.config

将deamonize 的属性改为yes

8.启动我们的Redis服务(6.24版本启动后无任何返回值,老版本的会返回启动成功),无返回值可使用ps -ef检验是否启动

9.启动成功后我们可以用redis-cli连接,测试成功!

10.使用命令 查看redis的进程是否开启

ps -ef|grep redis


进程开启!

11.如何关闭我们的redis服务

1. shutdown  关闭连接
2. exit	退出


12.后面我们会使用单机多Redis启动集群!

四.测试性能

redis-benchmark是一个压力测试工具 ,来模拟 N 个客户端同时发出 M 个请求 【官方自带的性能测试工具】

redis性能测试工具可选参数:

我们来简单的测试下:

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

测试的是单机性能!

五.基础知识

redis默认有16个数据库!

默认使用的是我们的第0个,

可以使用select切换数据库,和DBSIZE查看数据库的大小,keys * 查看数据库所有的key,flushdb清除当前数据库数据!flushall清空全部的数据库内容!

思考:为什么选择6379为redis的默认端口?明星名字 [了解即可]

Redis是单线程的!

明白redis是很快的,官方表示redis是基于内存操作cpu并不是Redis性能的瓶颈,而是在于内存和网络带宽!

大家所熟知的 Redis 确实是单线程模型,指的是执行 Redis 命令的核心模块是单线程的,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的。Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令。Redis 6.0 网络处理多线程,即Theaded IO 指的是在网络 IO 处理方面上了多线程!

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

  • 误区一:高性能的服务器一定是多线程的?

  • 误区二:多线程(cpu会上下文切换,消耗一定资源)一定比单线程快?

cpu、内存、硬盘的速度需要了解一下

核心:redis的数据全部是放在内存当中,所以使用单线程处理效率就是最高的,多线程(上下文切换:耗时的操作!),对于内存来说。如果没有上下文切换效率是最高的!,多次读写在一个cpu上的,在内存情况下这个就是最佳的方案!

五大基本数据类型

Redis官网介绍:

全段翻译:

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

所以提到的命令需要全部记住!

RedisKey

expire key 时间 多少秒后失效,key消失,时间的单位是s

ttl key 查看key还能存活多少秒

EXISTS key 判断当前key是否存在

move key 数据库号 将key移动到某数据库下

type key 查看当前key的一个数据类型

不会的命令去官网查即可

String(字符串)

常用操作:

##############################################################
127.0.0.1:6379> set key v1   #设置key v1
OK
127.0.0.1:6379> get key    #获取key
"v1"
127.0.0.1:6379> append key hello   #追加一个hello,但是如果key不存在则相当于set key 去存放数据
(integer) 7
127.0.0.1:6379> strlen key   #key对应value的长度
(integer) 7
127.0.0.1:6379> append key ",songqixiang"   #再追加一个,songqixiang
(integer) 19
127.0.0.1:6379> get key
"v1hello,songqixiang"      #再次检验key对应的值
###############################################################
一般用户浏览量:每一个人浏览执行自增1, incr 1
127.0.0.1:6379> set views 0		#初始浏览量为0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views    #浏览量自增1  ,如果不存在的key自增,会以0为初始值创建key并自增1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views    #浏览量自减1
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> INCRBY views 10   #浏览量自增指定数目
(integer) 10
127.0.0.1:6379> DECRBY views 10   #浏览量自减指定数目
(integer) 0
###############################################################
字符串范围(获取部分字符串value),相当于java的substring

127.0.0.1:6379> set key1 "hello,redis"  #设置key1的值
OK
127.0.0.1:6379> getrange key1 0 4     #截取字符串[0,4]
"hello"
127.0.0.1:6379> getrange key1 0 -1   #获取全部的字符串和get key是一样的
"hello,redis"

替换字符串value,相当于java的replace

127.0.0.1:6379> set key abcdefg
OK
127.0.0.1:6379> get key
"abcdefg"
127.0.0.1:6379> setrange key 1 xx   #替换从指定位置开始的字符串,此处是从1位置开始替换为xx
(integer) 7
127.0.0.1:6379> get key
"axxdefg"
###############################################################
setex(set with exprie)		#设置过期时间
setnx(set if not exist)		#如果不存在设置

127.0.0.1:6379> setex k3 30 "hello"  #设置一个key3的值hello,30s后过期!
OK
127.0.0.1:6379> ttl k3
(integer) 24

127.0.0.1:6379> setnx mykey redis     #如果mykey不存在,创建mykey
(integer) 1  #创建成功,返回1
127.0.0.1:6379> setnx mykey mongodb   #如果mykey存在,创建失败
(integer) 0	 #创建失败,返回0
127.0.0.1:6379> get mykey
"redis"
###############################################################
批量的获取值和设置值
mset
mget

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3  #一次设置多个<k,v>
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3   #一次获取多个key
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4    # msetnx属于原子性操作:要么做成功,要么什么都不做
(integer) 0  #设置失败
127.0.0.1:6379> get key4
(nil)


#对象(进阶的用法)
set user:1 {name:zhangsan,age:3}  #设置一个user:1 对象(key),(value)值为json字符串保存一个对象

#这里的key是一种巧妙地设计,user:{id}:{filed}    #如此设计再redis当中是完全ok的(也就是看作一个整体)

127.0.0.1:6379> mset user:1:name zhangsan user:1:age 20
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "20"

###############################################################
组合命令
getset   先get后set

127.0.0.1:6379> getset db redis   #先get db发现为nil,没取到,然后设置db为redis
(nil)
127.0.0.1:6379> get db      #得到设置的redis
"redis" 
127.0.0.1:6379> getset db mongodb    #先get db发现为redis,然后设置db为mongodb
"redis"
127.0.0.1:6379> getset db mongodb   #得到设置的mongod
"mongodb"

String类型的使用场景:value除了是我们的字符串,也可能是我们的数字

  • 计数器!

  • 统计数量

List(列表)

基本的数据类型,列表

举一个例子,一个list列表存放4个元素,再redis中我们可以通过定义规则,把List玩成 栈、队列、阻塞队列!

所有的list命令都是使用l开头的!

###############################################################
127.0.0.1:6379> lpush list one     #将一个或者多个值,插入到列表头部(左)  lpush(第一个字符表示left的意思)
(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    #读取的时候是从左向右读取!
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1    
1) "three"
2) "two"
127.0.0.1:6379> rpush list four    #将一个或者多个值,插入到列表头部(右)  rpush(第一个字符表示right的意思)
(integer) 4
127.0.0.1:6379> lrange list 0 -1	#读取的时候是从左向右读取!
1) "three"
2) "two"
3) "one"
4) "four"

上边可以将list看做一个平面,双向栈(两头进)!

###############################################################

同理我们想要移出list中的元素,也是双向移除
lpop(left pop)
rpop(right pop)

127.0.0.1:6379> lrange list 0 -1     #查看list中的全部元素
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> lpop list       #从左边开始移除一个元素,移出的是three
"three"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "four"
127.0.0.1:6379> rpop list       #从右边开始移除一个元素,移出的是four
"four"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"

###############################################################

上边我们通过push和pop已经实现了存放和移除,我们同样可以使用index去读取
lindex 是按照下标读取元素,下标是从左到右以0开始的!

127.0.0.1:6379> lindex list 1   #通过下标获取list中的某一个值
"one"
127.0.0.1:6379> lindex list 0
"two"

###############################################################
除了以上我们存、取、读、我们还可以获取我们的list的长度
Llen
 
127.0.0.1:6379> flushdb        #取清空数据库
127.0.0.1:6379> lpush list 1   #在list中添加数据
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 3
(integer) 3  
127.0.0.1:6379> lrange list 0 -1   #读取list的值
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> llen list   #获取list的长度!
(integer) 3
127.0.0.1:6379> 

###############################################################
移除指定的值
Lrem (list remove)

127.0.0.1:6379> lrem list 1 1     #移除一个元素,指定元素1(第一个1是count元素个数,第二个1是元素1)
(integer) 1
127.0.0.1:6379> lrange list 0 -1  #获取全部的值
1) "3"
2) "3"
3) "2"
127.0.0.1:6379> lrem list 1 3    #移除一个元素,指定元素3
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
###############################################################
ltrim 修剪,只保留指定位置的元素

127.0.0.1:6379> rpush mylist hello1
(integer) 1
127.0.0.1:6379> rpush mylist hello2
(integer) 2
127.0.0.1:6379> rpush mylist hello3
(integer) 3
127.0.0.1:6379> rpush mylist hello4   #mylist中添加4个元素
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1    #读取mylist当中的所有元素
1) "hello1"  
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> ltrim mylist 1 2      #修剪到只保留1,2两个元素(通过下标截取指定的长度!,相当于修树枝)
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello3"
127.0.0.1:6379> 
###############################################################
组合命令:rpopfpush:先从右边移除一个元素,然后将该元素从左边添加到另一个list当中!
移除列表当中的最后一个元素,将他移动到新的列表当中

127.0.0.1:6379> rpush mylist hello1
(integer) 1
127.0.0.1:6379> rpush mylist hello2
(integer) 2
127.0.0.1:6379> rpush mylist hello3
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
127.0.0.1:6379> rpoplpush mylist anotherlist   #先从右边移除一个元素,然后将该元素从左边添加到另一个list当中
"hello3"
127.0.0.1:6379> lrange mylist 0 -1   #原列表当中的最右边元素已经被移除
1) "hello1"
2) "hello2"
127.0.0.1:6379> lrange anotherlist 0 -1   #查看目标列表当中存在了上步移出的元素
1) "hello3"

###############################################################
lset 将列表中指定下标的值,替换为另一个值

127.0.0.1:6379> exists list   #判断是否存在list
(integer) 0
127.0.0.1:6379> lset list 0 hello   #不存在替换失败
(error) ERR no such key
127.0.0.1:6379> lpush list hello1   #添加值
(integer) 1  
127.0.0.1:6379> lrange list 0 -1  
1) "hello1"
127.0.0.1:6379> lset list 0 redis    #修改存在下标的值,成功!
OK
127.0.0.1:6379> lrange list 0 -1     
1) "redis"
127.0.0.1:6379> lset list 1 other    #修改不存在下标的值,修改失败!
(error) ERR index out of range

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

127.0.0.1:6379> lpush list hello
(integer) 1
127.0.0.1:6379> lpush list word
(integer) 2
127.0.0.1:6379> linsert list before "word" redis
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "redis"
2) "word"
3) "hello"
127.0.0.1:6379> linsert list after "word" after
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "redis"
2) "word"
3) "after"
4) "hello"

小节

  • 他其实就是一个链表,before 、node、 after、 left、right都可以插入值!
  • key不存在,创建新的链表
  • key存在,新增内容
  • 如果移除了所有值,空链表也是不存在!
  • 在两边插入或改动值,效率最高!中间元素相对来说效率会低一些!

可用作消息队列:从左边存值,右边取值,达成排队效果(lpush、rpop)|栈 左边存值,左边取值(lpush、lpop)

Set(无序集合)

set当中的值是不能重复的!

###############################################################
set集合中添加元素

127.0.0.1:6379> sadd myset hello   #set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset redis
(integer) 1
127.0.0.1:6379> sadd myset mongodb
(integer) 1
127.0.0.1:6379> sadd myset hello     #在这里可以看到、重复的元素无法添加成功!
(integer) 0
127.0.0.1:6379> smembers myset    #查看set集合当中的所有元素
1) "redis"
2) "hello"
3) "mongodb"
127.0.0.1:6379> sismember myset hello   #判断myset集合当中是否存在hello元素,存在则返回1,不存在则返回0
(integer) 1
127.0.0.1:6379> sismember myset hello1
(integer) 0
127.0.0.1:6379> 
###############################################################
获取set集合的长度
127.0.0.1:6379> scard myset     #获取set集合的长度
(integer) 3
###############################################################
srem 移除某一个元素
127.0.0.1:6379> srem myset hello    #移除myset集合当中的hello元素
(integer) 1
127.0.0.1:6379> smembers myset		#查看myset,hello元素已经被移除!
1) "redis"
2) "mongodb"

###############################################################
set是无序不重复集合,srandmember抽随机!(只是随机获取元素!)

127.0.0.1:6379> sadd myset lovesqx
(integer) 1
127.0.0.1:6379> smembers myset
1) "redis"
2) "lovesqx"
3) "mongodb"
127.0.0.1:6379> srandmember myset            #随机抽选出一个元素!
"mongodb"
127.0.0.1:6379> srandmember myset
"lovesqx"
127.0.0.1:6379> srandmember myset
"lovesqx"
127.0.0.1:6379> srandmember myset
"redis"
127.0.0.1:6379> srandmember myset 2			#随机抽选出指定个数的元素!
1) "redis"
2) "mongodb"
127.0.0.1:6379> srandmember myset 2
1) "redis"
2) "lovesqx"
127.0.0.1:6379> 

###############################################################
spop随机移除某个元素

127.0.0.1:6379> spop myset   #随机删除一个set集合中元素!
"mongodb"
127.0.0.1:6379> smembers myset
1) "redis"
2) "lovesqx"
###############################################################
将一个指定的元素,移动到另一个set集合当中
smove 【数据源】  【目的地】  【元素】
127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset redis
(integer) 1
127.0.0.1:6379> sadd myset mongodb
(integer) 1
127.0.0.1:6379> sadd myset2 songqixiang
(integer) 1
127.0.0.1:6379> smembers myset
1) "redis"
2) "hello"
3) "mongodb"
127.0.0.1:6379> smembers myset2
1) "songqixiang"
127.0.0.1:6379> smove myset myset2 hello   #将myset中的hello元素移动到myset2集合!
(integer) 1
127.0.0.1:6379> smembers myset
1) "redis"
2) "mongodb"
127.0.0.1:6379> smembers myset2
1) "songqixiang"
2) "hello"
127.0.0.1:6379>         #移动成功!

###############################################################
微博、b站 共同关注(交集!)
数字集合类:
	-差集: sdiff
	-交集: sinter
	-并集: sunion

127.0.0.1:6379> sadd key1 a    #初始化两个set集合、key1、key2
(integer) 1
127.0.0.1:6379> sadd key1 b
(integer) 1
127.0.0.1:6379> sadd key1 c
(integer) 1
127.0.0.1:6379> sadd key2 c
(integer) 1
127.0.0.1:6379> sadd key2 d
(integer) 1
127.0.0.1:6379> sadd key2 e
(integer) 1
127.0.0.1:6379> sdiff key1 key2     #key1中与key2不同的是!(与key2想比,key1独有的)
1) "a"
2) "b"
127.0.0.1:6379> sinter key1 key2    #key1和kkey2的交集
1) "c"
127.0.0.1:6379> sunion key1 key2    #key2和key1的并集
1) "b"
2) "c"
3) "a"
4) "d"
5) "e"
127.0.0.1:6379> 

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

共同关注、共同爱好、推荐好友 !

Hash(哈希)

Map集合!key-value(map集合)、key对应的值是一个键值对集合(map集合)、

###############################################################
Hash本质和String类型没有太大区别还是一个简单的集合、只是value是键值对的集合

127.0.0.1:6379> hset myhash field1 redis        #set一个key-value
(integer) 1
127.0.0.1:6379> hget myhash field1
"redis"													   #HMSET从Redis 4.0版本后已被官方弃用,建议使用HSET
127.0.0.1:6379> hset myhash field2 monggodb field3 java    #set多个key-value,hmset已经过时(hset可以设置多个)
(integer) 2
127.0.0.1:6379> hmget myhash field2 field3      #hmget获取多个key
1) "monggodb"
2) "java"
127.0.0.1:6379> hgetall myhash					#以键值对的形式获取全部的key-value
1) "field1"
2) "redis"
3) "field2"
4) "monggodb"
5) "field3"
6) "java" 
127.0.0.1:6379> hdel myhash field1           #删除指定的hash中的key字段,其对样的value值也消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "monggodb"
3) "field3"
4) "java"
###############################################################
hlen获取hash的长度
127.0.0.1:6379> hlen myhash    #获取myhash的长度
(integer) 3
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "monggodb"
3) "field3"
4) "java"
5) "field"
6) "redis"
###############################################################
hexists 判断hash中是否存在指定字段
127.0.0.1:6379> hexists myhash field2    #判断myhash中是否存在指定字段field,存在返回1、不存在返回0
(integer) 1
127.0.0.1:6379> hexists myhash field3
(integer) 1
###############################################################
#只获取hash中所有的key
#只获取hash中所有的value
127.0.0.1:6379> hkeys myhash   #获取myhash中的所有的key
1) "field2"
2) "field3"
3) "field"
127.0.0.1:6379> hvals myhash   #获取myhash当中所有大value
1) "monggodb"
2) "java"
3) "redis"

###############################################################
自增和自减:incr(increment) 、 decr(decrement)

127.0.0.1:6379> hincrby myhash field4 1    #hash中的指定key所对应的value增加指定的值(增量是负的,相当于减)
(integer) 2
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "monggodb"
3) "field3"
4) "java"
5) "field"
6) "redis"
7) "field4"
8) "2"
127.0.0.1:6379> hsetnx myhash field5 zhangsan     #如果不存在field5属性 ,创建并赋值
(integer) 1
127.0.0.1:6379> hsetnx myhash field5 zhangsan2    #如果存在则失败!
(integer) 0

hash做一些变更的数据,user,age,,name尤其是经常变动的信息和用户信息之类的!

  • 【hash更适合于对象的存储!】比如:hset user:1 name zhangsan age 20
  • 【String适合字符串存储!】比如 :set username zhangsan

Zset(有序集合)

在set的基础上,增加了一个值 !区别: set k1 v1 | zset k1 score1 v1

###############################################################
添加一个|多个值!
127.0.0.1:6379> zadd myset 1 one    #其中1表示为添加的元素做一个排序作用
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> 
###############################################################
排序的实现
命令:
   -升序:【zrangebyscore  key min max】min < max
   -降序:【zrevrangebyscore key max min】max > min
   
127.0.0.1:6379> zadd salary 2500 xiaohong     #首先是添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 3000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 lisi     
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf     #将salary按照成绩排序,由【-∞,+∞】升序
1) "lisi"
2) "xiaohong"
3) "zhangsan"   
127.0.0.1:6379> zrangebyscore salary -inf +inf withcores   
(error) ERR syntax error
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #将salary按照成绩排序,由【-∞,+∞】升序,带上成绩的信息!
1) "lisi"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "3000" 
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores   #将salary按照成绩排序,由在【-∞,2500】内升序
1) "lisi"
2) "500"
3) "xiaohong"
4) "2500"
###############################################################
移除元素zrem

127.0.0.1:6379> zrange salary 0 -1
1) "lisi"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "lisi"
2) "zhangsan"
127.0.0.1:6379> 

获取集合中的元素个数zcard
127.0.0.1:6379> zcard salary
(integer) 2

###############################################################
zount 统计在一个范围内元素的个数

127.0.0.1:6379> zadd myset 1 hello   #添加3个元素到myset这个有序集合中
(integer) 1
127.0.0.1:6379> zadd myset 2 word 
(integer) 1
127.0.0.1:6379> zadd myset 3 redis 
(integer) 1
127.0.0.1:6379> zcount myset 1 3    #统计score在1-3这个范围当中元素的个数字,返回值就成员的数量!
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2

其余的一些api,通过我们的学习剩下的工作中有需要去查查看官方文档!

案例思路:存储班级成绩表、工资表排序、排行榜实现、带权重的消息判断:为消息分级别赋予权重!

三大特殊数据类型

geospatial地理位置

应用:朋友的定位、附近的人、打车距离计算!

在Redis的3.2版本就推出了这个Geo地理位置功能,这个功能可以推算地理位置的信息、两地之间的距离、方圆几里的人!

城市经度纬度在线查询:http://www.jsons.cn/lngcode/

关于geospatial地理位置的官方文档:http://www.redis.cn/commands/geoadd.html

Geo的相关命令:中文文档翻译的是有一点问题的!

在这个使用命令时redis的提示可以看出就是、经度、纬度、名称!

geoadd

#geoadd添加地理位置
规则:地球两极(南极和北极)是无法添加的!我们一般会下载城市数据,使用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 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 108.96 34.26 xian
(integer) 1
127.0.0.1:6379> 

geopos

获取当前定位:一定是一个坐标值!

#geopos获取某一个地址的经度和纬度
127.0.0.1:6379> geopos china:city beijing    #获取指定城市的坐标
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city chongqing shenzhen   #一次获取2个城市的坐标
1) 1) "106.49999767541885376"
   2) "29.52999957900659211"
2) 1) "114.04999762773513794"
   2) "22.5200000879503861"
127.0.0.1:6379> 

geodist

获取两地之间的直线距离!

单位如下:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。
#北京到上海的直线距离
127.0.0.1:6379> geodist china:city beijing shanghai    #查看上海到北京的的直线距离
"1067378.7564"
127.0.0.1:6379> geodist china:city beijing shanghai km   #查看上海到北京的的直线距离,转换为km单位!
"1067.3788"
127.0.0.1:6379> geodist china:city beijing chongqing km   #查看重庆到北京的距离
"1464.0708"

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

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

获取指定数量的人!

所有的城市都要录入,china:city 当中!

127.0.0.1:6379> georadius china:city 110 30 1000 km   #以100 30这个经纬度为中心 寻找1000km内的城市
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "chongqing"
2) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist  #显示到中心位置的距离(附近的人距你的距离)
1) 1) "chongqing"
   2) "341.9374"
2) 1) "xian"
   2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord   #显示出城市的经度纬度
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) "483.8340"
   3) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 1   #筛选出指定的结果!
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 2
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) "483.8340"
   3) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379> 

georadiusbymember

#找出位于指定元素周围的其他元素!(以china:city中的成员为中心、查找周围元素)

127.0.0.1:6379> georadiusbymember china:city beijing 1000 km  #距离北京1000km的城市!
1) "beijing"
2) "xian"
127.0.0.1:6379> georadiusbymember china:city shanghai 400 km   #距离上海400km的城市!
1) "hangzhou"
2) "shanghai"
127.0.0.1:6379> 

geohash 了解即可

简述:就是将二维的经纬度转换为一纬的字符串!

其底层就就是Zset,所以说我们可以使用Zset命令去操作geo、删除数据就可以使用Zrem

Hyperloglog处理基数

什么是基数?

A {1,3,5,7,8,7} 基数:5

B {1,3,5,7,8} 基数:5

基数的概念:不重复的元素的个数!

  • 比如:1 2 3 4 2 3 4的基数就是4因为只有1 2 3 4 而2 3 4 是重复的(基本就4个)

简介

Redis2.8.9版本就更新了Hyperloglog数据结构!

Redis Hyperloglog基数统计的算法!

  • 优点:占用的内存十分的小,而且是固定的!2^64元素只占12kb内存!如果是考虑内存的话首选Hyperloglog
  • 缺点:存在0.81%的错误率,但是还在接受范围内!

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

  • 传统的方式,set保存用户的id,然后统计set元素的的数量去作为标准判断!比如:用户:1 用户:2 去访问某个网站后,2个id内保存在set集合此时set集合中的数据为{1,2},然后用户:1又来访问则id:1又被保存在set集合当中,此时set集合中的数据为{1,2,1}此时统计的id将毫无意义,因为id重复,而且我们需要的并不是保存用户的id而是同时数量!
  • 我们将用户的id用PFadd存在hyperloglog当中,然后去使用PFcount去统计基数数量!

测试使用!

Hyperloglog的命令一般都是pf卡头

127.0.0.1:6379> pfadd mykey1 a b c d e f g h i j    #创建第一组元素
(integer) 1
127.0.0.1:6379> pfadd mykey2 i j z x c v b n m         #创建第二组元素
(integer) 1
127.0.0.1:6379> pfcount mykey1                     #统计mykey1中的元素的基数数量
(integer) 10
127.0.0.1:6379> pfcount mykey2						#统计mykey2中的元素的基数数量
(integer) 9 
127.0.0.1:6379> pfmerge mykey3 mykey1 mykey2        #合并mykey1和mykey2生成mykey3(并集) 
OK
127.0.0.1:6379> pfcount mykey3                      #统计mykey3中的元素的基数数量                 
(integer) 15
127.0.0.1:6379> 

如果允许容错,那么一定可以使用我们的Hyperloglog!否则,只能使用set或我们自己的数据类型!

Bitmaps位图

位存储

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

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

365天=365 bit1字节= 8bit46个字节左右!

例如:如下统计7天的签到情况:如果某天签到设为1、如果某天未签到设为0

#使用bitmap来记录我们一周打卡情况!
周一 0 周二 0 周三 1  。。。
127.0.0.1:6379> setbit sign 0 0      #表示第一天缺勤
(integer) 0
127.0.0.1:6379> setbit sign 1 0		 #表示第二天签到
(integer) 0
127.0.0.1:6379> setbit sign 2 1      #表示第三天签到
(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 1
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
127.0.0.1:6379> 

查看某一天是否打卡

127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> 

统计操作我们的打卡天数

127.0.0.1:6379> bitcount sign    #统计1的个数
(integer) 4

事务

事务的概述

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

一次性、顺序性、排他性

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

Redis与MySQL区别:

  • MySQL事务满足ACID(原子性、一致性、隔离性、持久性)
  • MySQL的原子性体现在一组事务,一组事务中的命令必须同时成功或者失败!
  • Redis没有隔离级别的概念
  • Redis单条命令是保持原子性的,但事务不保证原子性!
  • Redis事务满足一次性(一次执行一组命令)、顺序性(顺序执行)、排他性(不允许干扰)

Redis事务的执行

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

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

正常执行事务!

127.0.0.1:6379> multi           #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1		#从此开始命令入队,第一位
QUEUED	
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> keys *
QUEUED
127.0.0.1:6379(TX)> exec 		 #执行事务,从第一位开始依次执行
1) OK
2) OK
3) "v1"
4) 1) "k2"
   2) "k1"                    #事务执行完成后事务消失!
127.0.0.1:6379> 

取消事务discard

127.0.0.1:6379> multi             #开启一个事务
OK
127.0.0.1:6379(TX)> set k1 v1		#命令入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> discard			#放弃事务
OK
127.0.0.1:6379> exec			#执行事务
(error) ERR EXEC without MULTI   #当前未开启事务!

事务错误

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

127.0.0.1:6379> multi 		 #开启一个事务
OK
127.0.0.1:6379(TX)> set k1 v1		#命令入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 			#执行语法错误的命令
(error) ERR wrong number of arguments for 'set' command  #提示语法错误
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec  #事务执行
(error) EXECABORT Transaction discarded because of previous errors.#事务执行失败
127.0.0.1:6379> get k1   #看到语法错误导致所有命令都不执行!
(nil)
127.0.0.1:6379> 

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

127.0.0.1:6379> set k1 hello     #初始化k1为一个字符串
OK
127.0.0.1:6379> get k1
"hello"
127.0.0.1:6379> multi		
OK
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> incr k1  #此处制造逻辑错误,k1对应的值是字符串不能自增
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range    #运行时报错
4) "v2"
127.0.0.1:6379> 

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

监控

悲观锁:

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

乐观锁:

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

使用watch key监控指定数据,相当于乐观锁加锁。

正常执行

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(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby use 20
QUEUED
127.0.0.1:6379(TX)> 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:(在xshell开启一个客户端,先执行)

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后都会自动释放锁,不管是否成功

Jedis

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

Java操作Redis的中间件!

Redis远程配置

具体操作步骤:

  1. 在阿里云配置安全组,开放6379端口

  2. 防火墙放行6379

    接下来通过Vim去修改redis.conf中的一些配置!

  3. daemonize yes

  4. protected-mode no

  5. 注释 bind 127.0.0.1

  6. bind 0.0.0.0 所有ip可以连接,引出安全问题,我们去设置一个密码即可!

  7. requirepass xxxx 配置redis密码

上边的配置工作完成后,我们重启redis-server

然后执行下图操作,ping后返回pong连接成功!

完成!

Java连接远程redis

接下来就是使用jedis连接远程redis,实现java操作redis!

1.创建一个空的Maven项目(注意:project,moudle,javacomplie需要配置JDK)

2.添加Moudle即可!

3.导入对应的依赖

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.2.0</version>
    </dependency>
    <!--fastjson-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.70</version>
    </dependency>

4.编码测试

  • 连接数据库
  • 操作命令
  • 断开连接

做一个测试,判断是否连接成功!

public class TestPing {
    public static void main(String[] args) {
        //1.new jedis对象,连接远程服务器上的redis
        Jedis jedis = new Jedis("60.205.180.178",6379);
        jedis.auth("123456");
        //jedis 的所有命令就是我们之前学习的所有命令!
        System.out.println(jedis.ping());

    }
}

输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZGVGAtIX-1624986052987)(Redis笔记.assets/image-20210624165651801.png)]

连接远程成功!

如果想要连接本地windows下的方法如下,只需修改jedis!

 Jedis jedis = new Jedis("127.0.0.1",6379);

常用的API

就是在jedis中将我们之前的命令,改为方法即可!

  • Rediskey

String、Set、List、Hash、Zset使用方法同上!

使用Jedis对事务进行一个测试!

package com.sqx;

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

//jedis测试事务
public class Testtx {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("60.205.180.178", 6379);
        jedis.auth("123456");
        
        jedis.flushDB();//清空redis数据库!
        
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","world");
        jsonObject.put("name","songqixiang");

        Transaction transaction = jedis.multi();//开启事务
        String jsonString = jsonObject.toJSONString();//将json对象转为字符串格式

        try {
            transaction.set("user1",jsonString);
            transaction.set("user2",jsonString);
            
            //  int i =1/0;   //执行到此会抛出一个异常,导致事务执行失败!
             
            transaction.exec();//执行事务!
        } catch (Exception e) {
            transaction.discard();//取消(放弃)事务
            e.printStackTrace();
        } finally {
           	//打印输出一下我们的user信息!
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            //关闭连接
            jedis.close();
        }
    }

}

Springboot整合Redis

SpringBoot 操作数据:spring-data、 jpa、 jdbc、 mongodb 、redis 等 !

SpringData 也是和 SpringBoot 齐名的项目!

说明: 在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce 音标:[letɪs]?

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

源码分析

我们在学习Springboot的时候,学习了自动装配的原理,让我们具体的分析下Redis的整个流程吧!

Springboot项目启动默认会加载spring.factories文件,只有当我们导入redis的启动器后,通过@conditional注解判断后,我们对应的RedisAutoConfiguration生效,如下:

RedisAutoConfiguration:Redis的自动配置类

@Bean
@ConditionalOnMissingBean(name = "redisTemplate") 
// 我们可以自己定义一个redisTemplate来替换这个默认的!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
// 两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, Object>
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}
@Bean
@ConditionalOnMissingBean // 由于 String 是redis中最常使用的类型,所以说单独提出来了一个bean!
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

我们可以看到这是一个javaconfig配置类,其中有2个bean,RedisTemplate和StringRedisTemplate会被注入到IOC容器当中,供我们以后使用!

同时,我们知道每一个自动配置类的属性和方法都在一个配置文件当中这次是RedisProperties!我们点进去就可以看到我们可以在配置文件中通过配置去改动的属性和一些方法!

RedisTemplate:需要传递一个RedisConnectionFactory,我们点入这个工厂的源码发现,这是一个接口,并且存在2个实现类

一个是关于Jedis的一个是关于lettuce的,他们具有相同的作用我们jeids上我们已经详细介绍了(封装java操作操作redis的方法)!

我们分别点入这2个实现类

  • JedisConnectionFactory :爆红,提示缺少对应的包!
  • LettuceConnectionFactory:完美使用,一切正常,

我们可以得出Lettuce才是我们现在RedisTemplate默认使用的,java操作Redis的类!

此时可得出一个结论:我们的RedisTemplate之所以可以操作Redis,其本质还是我们传入的 Jedis或者Lettuce,只是相当于又套了一层外壳!

最后我们回到RedisTemplate我们发现这个类上存在如下注解:

@ConditionalOnMissingBean(name = {"redisTemplate"})   //如果存在ioc中存在一个叫redisTemplate则RedisTemplate失效!

我们看到这个注解我们就该想到,这是再给我们自定义的机会,我们可以通过配置一个叫redisTemplate来替代原有的RedisTemplate,是我们开发更加的便捷!

以上便是对Redis的简单源码分析!

整合测试

1.导入依赖

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

2.配置连接

#配置Redis
spring.redis.host=60.205.180.178
spring.redis.port=6379

3.测试类

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired
    RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
        // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
        // opsForValue 操作字符串 类似String
        // opsForList 操作List 类似List
        // opsForSet  操作Set  类似Set
        // opsForHash
        // opsForZSet
        // opsForGeo
        // opsForHyperLogLog
        // 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD

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

        redisTemplate.opsForValue().set("name","zhangsan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

}

自定义RestTemplate

到此我们已经知道什么是RestTemplate了,但是默认的RestTemplate不是太好用,我们探究源码发现,

可以配置一个redisTemplate去替代我们原生的RestTemplate;

@Configuration
public class RedisConfig {
    //自定义的restTemplate
    @Bean
    public RedisTemplate<String, Object> redisTemplate1(RedisConnectionFactory redisConnectionFactory) {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String,Object>();
        template.setConnectionFactory(redisConnectionFactory);

        return template;
    }
}

当我们调用的时候通过自动装配即可!

  @Autowired
  @Qualifier("redisTemplate1")
  RedisTemplate redisTemplate;

序列化问题

如果数据传递不适序列化,就会出现乱码情况如下:

关于序列化问题,举个例子:

此时我们的User对象不是按照Json格式,也没有实现序列化执行后!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFCUF27s-1624986052987)(Redis笔记.assets/image-20210624221129571.png)]

我们可以看到爆红了,提示我们,我们没有实现默认的序列化!

所以我们需要去实现序列化:

方式一:pojo下的User实现序列化接口!

方式二:自定义序列化规则,设置再RestTemplate中

代码如下,【模板】

@Configuration
public class RedisConfig {

    //自定义的restTemplate
    @Bean
    public RedisTemplate<String, Object> redisTemplate1(RedisConnectionFactory redisConnectionFactory) {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String,Object>();
        template.setConnectionFactory(redisConnectionFactory);

        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 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;
    }
}

设置好序列化规则后,我们再调用我们自己的Restemplate就行!

使用步骤!

package com.sqx.redis02springboot;

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


@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired
    @Qualifier("redisTemplate1")
    RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
        // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
        // opsForValue 操作字符串 类似String
        // opsForList 操作List 类似List
        // opsForSet  操作Set  类似Set
        // opsForHash
        // opsForZSet
        // opsForGeo
        // opsForHyperLogLog
        // 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD

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

        redisTemplate.opsForValue().set("name","zhangsan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

    @Test
    void Test() throws JsonProcessingException {
        //真实开发一般都是采用json来传递数据!
        // 如果直接传递对象会报一个未序列化的错误,所以我们的User对象因该先实现一个序列化接口,用于避免!

//情况1:传递json一切正常
       /* User user = new User("宋淇祥", 20);
        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));*/

//情况2:传递为序列化的user,报错,如果user实现序列化接口则正常输出
        User user = new User("宋淇祥", 20);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }
}

至此,序列化问题得以解决!

RedisUtils

实际开发中,我们一般不会笨重的去使用这种方式:

所以我们会自定义一个工具类RedisUtils,把常用的一些个命令写入即可!

说实话:我感觉,又回到了Jredis,可能是自己功力太浅!

Redis.conf详解

我们知道,启动Redis服务的时候,就是依赖这个配置文件!

让我们进入配置文件分析一下!Vim Redis.conf 然后gg直接从文件头部开始分析!

单位

、

配置文件中,Unit单位对大小写是不敏感的!

包含 INCLUDES

、

相当于我们学习Spring的时候,整合配置文件时的import,JSP、Thymeleaf中的include标签!

网络 NETWORK

bind 127.0.0.1     #用于固定访问Redis服务的ip,我们将其注释掉意味着,所有ip可以访问!

protected-mode no    #关闭保护模式,我们可以选择开启,但是当需要远程访问Redis服务的时候将其开启!

port		#端口设置,redis在当前服务主机上的爆出来的访问端口号!

通用 GENERAL

daemonize yes    #是否以后台的守护进程的方式运行,默认是No,我们需要配置为Yes手动开启!

pidfile /var/run/redis_6379.pid     #如果以后台的方式运行,我们就需要指定一个pid进程文件!

#日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)	
# notice (moderately verbose, what you want in production probably)    生产环境使用!
# warning (only very important / critical messages are logged)		只打印关键日志
loglevel notice            #我们的日志级别设置,可供选择的有debug、verbose、notice、warning 4个级别!
logfile ""     #日志的文件位置名,为空的话就是直接打印输出

databases 16         #数据库的数量,默认是16个!

always-show-logo no    #是否总是显示启动server时的logo

快照 SNAPSHOTTING

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

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

#以下配置的是一些持久化规则!
# save 3600 1      如果在3600s(1小时)内,有一个以上的key被修改,那么就执行持久化操作!
# save 300 100		如果在300s(5分钟)内,有100个以上的key被修改,那么就执行持久化操作!
# save 60 10000		同理,在60s(1分钟)内,有10000个以上的key被修改,那么就执行持久化操作,缺点:如果59s的时候宕机数据丢失!
#之后我们学习持久化,会自己定义这个规则!


stop-writes-on-bgsave-error yes     #持久化出错我们是否还持久工作,默认为yes

rdbcompression yes    #是否压缩我们的rdb文件,默认为开启的,需要消耗一些cpu资源!

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

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

复制 REPLICATION 我们讲解Redis主从复制的时候再进行详解!

安全 SECURITY

#我们可以在此配置文件设置一下我们的Redis密码!
 requirepass 123456
 
#使用命令获取我们的Redis的密码!
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
#使用命令修改我们密码
127.0.0.1:6379> config set requirepass "123456"
ok
#设置完密码后,我们如果不输入密码验证,就会失去一切命令的权限
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
#密码验证即可!
127.0.0.1:6379> auth 123456
OK

限制 CLIENTS、MEMORY MANAGEMENT

maxclients 10000     #设置Redis的最大客户端的设置

maxmemory <bytes>	 #Redis配置最大内存容量

maxmemory-policy noeviction      #内存达到上限的时候的处理策略!
 	#移除一些过期的key
 	#报错
	#.....
maxmemory-policy的六种策略:
        #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持久化的,在大部分情况下我们的rdb是够用的!

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

# appendfsync always	 #每次修改都会同步(sync),消耗性能!
appendfsync everysec     #每秒执行一次我们的同步(sync)
# appendfsync no		 #不执行同步!这个时候操作系统执行同步(sync),速度最快!

具体的配置会在Redis持久化中详细分析!

Redis持久化(重点)

面设和工作必须!

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

RDB

什么是RDB(Redis DataBase)

、

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

  • Redis会单独创建(fork)一个子进程来进行持久化, ,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。

  • 这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

  • RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置! 有时候在生产环境我们会将这个文件进行备份!

  • rdb保存的文件是dump.rdb 都是在我们的配置文件中快照中进行配置的!

  • 、

  • 在生产环境一般会对rdb文件进行保存!

个人理解:就是将内存的数据按一定时间间隔后去保存到rdb文件当中,然后我们可以继续对内存中的数据进行操作,如果出现错误,我们可以读取rdb文件,失去当前的内存状态而回rdb中记录的状态,所以这种操作称之为快照!

测试:

我们去自定义一下我们的策略!

、

测试前我们保证不存在dump.rdb文件!

、

接下来,我们使用save命令:

Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。

127.0.0.1:6379> save
OK

执行完save发现生成了dump.rdb文件


如果此时删除rdb文件然后我们测试我们1分钟内修改5次key,判断是否会持久化
在这里插入图片描述

然后并没有生成rdb文件,我们再次执行save,发现生成了dump.rdb文件

由上得,当我们1分钟内修改5次key的时候,没有生成rdb文件,而是将内存数据保存在快照当中,而当我们执行save,会将快照中的数据以rdb文件的形式持久化到我们的硬盘当中!

触发机制

  1. save的规则满足的情况下,会自动触发rdb原则
  2. 执行flushall命令,也会触发我们的rdb原则
  3. 退出redis,也会自动产生rdb文件

备份就是生成一个rdb文件!

恢复我们的rdb文件!

我们将我们redis中的内存数据恢复至rdb文件中保存的状态!

步骤:就是将我们的dump.rdb文件放在我们的redis启动目录即可,redis启动后会自动检查dump.rdb文件,恢复其中的数据!

127.0.0.1:6379> config get dir       #通过此命令查看我们的redis启动目录位置!
1) "dir"
2) "/usr/local/bin"
127.0.0.1:6379> 

我们将其dump.rdb文件移如即可!

其实rdb机制的默认配置已经足够使用了,但是我们仍然要学习!

优点:

  1. 适合大规模的数据恢复(因为是单独开启一个进程去处理!)
  2. 对数据的完整性要求不高

缺点:

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

AOF

什么是AOF (APPEND ONLY FILE)

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

、

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

aof保存的文件是 appendonly.aof文件

、

aof默认是不开启的,我们需要手动进行开启,将如下配置改为yes

、

接下来重启我们的redis就可以生效了,发现已经存在appendonly.aof

、

如果aof文件损坏(被修改)redis是启动不起来的,我们该怎么办?

、

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

我们只需执行如下命令redis-check-aof --fix appenfonly.aof

、

文件得以修复!

我们接下来重启redis,发现数据也都通过读取aof的信息恢复了!

重写规则说明

aof默认是文件的无限制追加!文件会越来越大!

、

我们可以看到,当我们aof文件的大小大于64M,太大了,我们会fork一个新的进程,来将我们的文件进行重写!

优点和缺点!

#aof的配置!
appendonly no   #默认是不开启我们的aof模式的,默认是使用rdb持久化的,在大部分情况下我们的rdb是够用的!

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

# appendfsync always	 #每次修改都会同步(sync),消耗性能!
appendfsync everysec     #每秒执行一次我们的同步(sync)
# appendfsync no		 #不执行同步!这个时候操作系统执行同步(sync),速度最快!

优点:

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

缺点:

  1. 相对于记录数据文件来说,因为aof记录的是所有操作(日志),就文件大小来说,aof远远大于rdb,
  2. 修复速度比rdb慢!
  3. Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化

扩展 (了解即可)

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文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
  • 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

Redis订阅发布

Redis可以做,消息队列做的更好

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

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

订阅/发布消息图: 第一个:消息发送者, 第二个:频道 第三个:消息订阅者!

、

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

、

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

、

命令

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

、

测试一下

如下,我们开启2个客户端连接我们的Redis服务,

在这里插入图片描述

我们可以看到当一个用户订阅一个频道后,如果频道接收到消息用户可以实时收到!

代码展示:

客户端1(订阅端):

127.0.0.1:6379> subscribe sqx            #订阅一个频道 sqx
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sqx"
3) (integer) 1
#等待推送信息,以下就是我们的频道推送的消息!
1) "message"
2) "sqx"
3) "hello,redis"
1) "message"
2) "sqx"
3) "songqixiang"

客户端2(发送端):

127.0.0.1:6379> publish sqx "hello,redis"   #发布者发布信息到频道sqx  消息为"hello,redis"
(integer) 1
127.0.0.1:6379> publish sqx "songqixiang"
(integer) 1
127.0.0.1:6379> 

原理

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

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

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

下面两个图来解释一下上述话的意思:

\标准版

\乞丐版

客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。(看来图是不是通俗易懂哈哈哈哈)

应用场景

1.实时消息系统

2.实时聊天(频道当作聊天室,将信息回显给所有人即可!)

3.订阅关注系统都是可以的

简单的我们可以使用Redis去做,复杂的就要交给专业的技术kafka、RabbitMQ、RocketMQ等都可以!

Redis主从复制

概念

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

(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。

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

一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。

作用

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

为什么使用集群

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

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

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

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

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

、

主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用! 一主二从

只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!

总结

  1. 单台服务器难以负载大量的请求
  2. 单台服务器故障率高,系统崩坏概率大
  3. 单台服务器内存容量有限。

模拟Redis集群

由于我们是一台服务器去模拟一个Redis集群,所以就模拟最简单的 “一主二仆”

首先我们通过创建连接先模拟是3台服务器!

、

其次我们要准备好3个redis.conf 配置文件(复制原有的redis.conf 三份即可)!

、

然后通过Vim为每个配置文件修改一些配置,需要修改的配置如下

、

为上述的3个配置文件修改以下4个部分

  • port:进程占用的端口号
  • pid(port ID):记录了进程的 ID,文件带有锁。可以防止程序的多次启动。【并不是进程号】
  • logfile:明确日志文件的位置
  • dbfilename:dumpxxx.file 持久化文件位置

修改完成后启动我们的3个Redis服务

、

查看服务是否启动成功!

、

成功开启3个redis服务!

配置"一主二从"

首先是模拟访问3个Redis服务,**默认情况下,每台Redis服务器都是主节点,**可以通过命令

info replication  #查看当前主机信息!

、

所以一般情况下,我们只需要配置从机就可以了,认老大,一主(79)二从(80,81)

从机配置

127.0.0.1:6380> slaveof 127.0.0.1 6379    设置当前主机是 ip:127.0.0.1 port:6379 服务的从机
OK
127.0.0.1:6380> info replication
# Replication
role:slave     #当前角色是从机
master_host:127.0.0.1     #可以看到主机的信息
master_port:6379		
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:-1
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:e4e53623daed1f642f950cf9256334859ddef047
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380> 


#同样在主机中可以查看从机的信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2		
slave0:ip=127.0.0.1,port=6380,state=online,offset=28,lag=1    #从机的信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=28,lag=0
master_failover_state:no-failover
master_replid:4fe01cc1fe600bf3583a09dd5b0ec8ef3bd769ce
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

注意:如果6379的配置文件中redis设置了密码,就再去80和81文件中添加一个除之前修改的的4个属性外还需配置一个

masterauth 123456如果不配置将无法实现主从复制!

、

设置好后,再次使用info replication查看当前主机信息即可!

真实的主从配置应该是在配置文件中去配置是永久的,而我们通过命令配置的是暂时的!

如下:在配置文件的的REPLICATION复制模块下

、

细节

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

、

从机中不可写,一旦写入报错

、

测试存在问题

1.如果现在老大没了,主机宕机了,什么情况?

在从机中查看信息发现,主机信息不变,但是没有了写操作!如果此时老大又回来了 ,从机依旧可以获取到主机中的信息!

2.如果从机宕机了,什么情况?

如果是使用我们的命令行配置的主从,重启后就会变回主机!只要再变回从机,就可以立刻获取到主机中的所有信息(数据)!

主从复制原理

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

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

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

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

层层链路

我们一主二从是如下

、

我们还可以这样设计

、

此时中间的仍然是从节点,只能读!

注:【真实开发上述的2种模型都不会使用!】

上述模型行中,老大宕机的话,从机不重启依旧连接着宕机的老大!所以引入新问题?

如果老大宕机,这个时候能否产生新的老大?手动【谋朝篡位】

#我们在从机当中直接执行如下命令,当前从机变为主机
127.0.0.1:6380> slaveof no one
OK

其他的节点,就可以手动连接到我们新的主节点!如果此时老大修复了,那就只能重新连接,无法恢复老大地位!

上面是手动配置,接下来引入自动配置模式也就是哨兵模式!

哨兵模式(重点)

(自动选举老大的模式!)

概述

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

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

在这里插入图片描述

哨兵的作用:

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

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

、

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

哨兵测试

我们目前的状态是 “一主二从”

1.配置一个sentinel.conf的配置文件,进行如下配置!

、

意思是:哨兵监控我们的主机 6379的redis服务,1代表如果该服务宕机,salve就进行投票,票数最多的就会成为主机!

2.启动哨兵

[root@songqixiang bin]# redis-sentinel config/sentinel.conf       #启动命令!
8772:X 27 Jun 2021 13:45:37.932 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
8772:X 27 Jun 2021 13:45:37.932 # Redis version=6.2.4, bits=64, commit=00000000, modified=0, pid=8772, just started
8772:X 27 Jun 2021 13:45:37.932 # Configuration loaded
8772:X 27 Jun 2021 13:45:37.933 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.2.4 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379       #哨兵端口
 |    `-._   `._    /     _.-'    |     PID: 8772
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

8772:X 27 Jun 2021 13:45:37.937 # Sentinel ID is bbf206afdfb71e5ece1bcc92251a477201f845bb
8772:X 27 Jun 2021 13:45:37.937 # +monitor master myredis 127.0.0.1 6379 quorum 1
8772:X 27 Jun 2021 13:46:07.990 # +sdown master myredis 127.0.0.1 6379

如果我们的Master节点断掉了,这个时候就会从"从机"中随计选择一个服务器!

此时Master再回来也无济于事,只能重新连接,或者改做为奴(slave),新主机称霸天下!

优缺点

优点:

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

缺点:

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

哨兵模式的配置

# 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 <master-name> <ip> <redis-port> <quorum>

sentinel monitor mymaster 127.0.0.1 6379 1


当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码

设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码

sentinel auth-pass <master-name> <password>

sentinel auth-pass mymaster MySUPER--secret-0123passw0rd


#指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒

sentinel down-after-milliseconds <master-name> <milliseconds>

sentinel down-after-milliseconds mymaster 30000


#这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,

#这个数字越小,完成failover所需的时间就越长,

#但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。

#可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。


sentinel parallel-syncs <master-name> <numslaves>

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 <master-name> <milliseconds>

sentinel failover-timeout mymaster 180000


SCRIPTS EXECUTION

#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。

#对于脚本的运行结果有以下规则:

#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10#脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。

#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。

#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,

#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,

#一个是事件的类型,

#一个是事件的描述。

#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。

#通知脚本

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

【上述配置一般是由运维来配置!】

Redis缓存穿透和雪崩(重点)

缓存穿透

概念

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

、

所谓缓存穿透:(即缓存中查询不到)

解决方案

1.布隆过滤器 :

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

、

2.缓存空对象 :

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

、

这样做有2个问题:

  • 存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
  • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿

微博服务器宕机!(量太大缓存过期!)

概述

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

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

解决方案

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

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

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

在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

缓存雪崩

概述

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

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

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

、

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

Redis高可用

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

限流降级

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

数据预热

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

完结撒花!

如需要Markdown版本的笔记,我上传的资源当中是有的可以下载!

posted @ 2022-01-23 20:36  爪洼ing  阅读(6)  评论(0编辑  收藏  举报