Redis

根据以下学习视频,个人整理的笔记

https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0&vd_source=7a8946d22777450e46486d5fd60d8d4d

为什么要用NoSQL

我们现在处于大数据时代!

大数据:一般的数据库无法进行分析处理了!

纵观历史:

  • 单机MySQL的年代!

    在这里插入图片描述

    在90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够了!

    那个时候,更多的去使用静态网页 Html,所以服务器根本没有太大的压力!

    思考一下,这种情况下:整个网站的瓶颈是什么?

    1、数据量如果太大,一台机器根本放不下了

    2、数据的索引(B+ Tree),一个机器内存也放不下

    3、访问量(读写混合),一个服务器承受不了!

    只要开始出现以上三种情况之一,那么你就必须要晋级!

  • Memcached(缓存)+ MySQL + 垂直拆分(读写分离)

    实际上,网站百分之八十的情况下,都是在读。如果每次都要去查询数据库的话就十分麻烦!所以说我们希望减轻数据的压力,我们可以使用缓存来保证效率

    发展过程:优化数据结构和索引—>文件缓存(IO)—>Memcached(当时最热门的技术)

    在这里插入图片描述

  • 分库分表 + 水平拆分 + MySQL集群

    技术和业务在发展的同时,对人的要求也越来越高了

    本质:数据库(读,写)

    早些年MySQL的引擎MyISAM:表锁,十分影响效率!高并发下就会出现严重的锁问题

    后来转战到MySQL的引擎Innodb:行锁

    慢慢地就开始使用分库分表来解决写的压力!MySQL在那个年代推出了表分区!但是这个并没有多少公司使用

    后来又推出了MySQL的集群,很好地满足了那个年代的所有需求!

    在这里插入图片描述

  • 如今最近的年代

    定位,音乐,热榜等等也是一种数据

    MySQL等关系型数据库就不够用了!因为数据量很大,而且变化很快!

    MySQL 还有的人使用它来存储一些比较大的文件(博客,图片),数据库表会很大,效率就会低!如果有一种数据库来专门处理这种数据,那MySQL的压力就会变得十分小(研究如何处理这些问题!)大数据的IO压力下,表几乎没法更改

    目前的一个基本互联网项目的架构图如下:

    在这里插入图片描述

回到主题,为什么要用NoSQL?

用户的个人信息,社交网络,地理位置,用户自己产生的数据,用户的日志等等这样的数据爆发式增长!

这个时候我们就需要使用NoSQL数据库了。NoSQL数据库可以很好地处理以上情况

什么是NoSQL

NoSQL:Not Only SQL(不仅仅是SQL)泛指非关系型数据库

随着Web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区!暴露出来很多难以克服的问题。

NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术!

很多的数据类型的存储不需要固定的格式,不需要多余的操作就可以横向扩展的。

NoSQL的特点

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

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

  • 数据类型是多样型的!(不需要事先设计数据库!随取随用!如果是数据量十分大的表,很多人就无法设计了)

  • 传统的 RDBMS 和 NoSQL 的区别

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

了解:3V+3高

  • 大数据时代的3V:主要是描述问题的
    • 海量Volume
    • 多样Variety
    • 实时Velocity
  • 大数据时代的3高:主要是对程序的要求
    • 高并发
    • 高可扩(随时可以水平拆分,机器不够了,可以扩展机器)
    • 高性能

真正在公司中的社会实践:NoSQL + RDBMS 一起使用才是最强的,可以看一下阿里巴巴的架构演进!

技术没有高低之分,就看你如何使用!(提升内功,思维的提高!)

阿里巴巴演进分析

思考问题:这么多的东西难道都是在一个数据库中的吗?

在这里插入图片描述

在这里插入图片描述

技术急不得,越是慢慢学,才能越扎实!

敏捷开发,极限编程

开源才是技术的王道

2008 年,汶川地震,大雪淹没道路

2020年,疫情爆发

任何一家互联网的公司,都不可能只是简简单单地让用户能用就好了!

大量公司做的都是相同的业务;(竞品协议),随着这样的竞争,业务是越来越完善了,然后对于开发者的要求也是越来越高!

在这里插入图片描述

如果你未来想当一个架构师:没有什么是加一层解决不了的

1、商品的基本信息
	名称、价格、商家信息;关系型数据库就可以解决了(MySQL、Oracle(淘宝早些年去IOE了))
	淘宝内部的MySQL 不是大家用的MySQL
	
2、商品的描述、评论(文字比较多)
	文档型数据库,MongoDB
	
3、图片
	分布式文件系统 FastDFS
	淘宝自己的 TFS
	Google的 GFS
	Hadoop,HDFS
	阿里云的 OSS
	
4、商品的关键字(搜索)
	搜索引擎 solr elasticsearch
	ISearch
	
5、商品热门的一些波段信息
	内存数据库
	Redis、Tair......
	
6、商品的交易,外部的支付接口
	三方应用
	

要知道,一个简单的网页背后的技术一定不是大家所想的那么简单

大型互联网应用的问题:

  • 数据类型太多了!
  • 数据源太多了,经常重构
  • 数据要改造,大面积改动比较麻烦

解决问题:

在这里插入图片描述

在这里插入图片描述

这里以上都是NoSQL入门概述,不仅能够提升大家的知识,还可以帮助大家了解大厂的工作内容!

NoSQL的四大分类

KV键值对:

  • 新浪:Redis
  • 美团:Redis + Tair
  • 阿里,百度:Redis + memcache

文档型数据库(bson格式 和 Json一样):

  • MongoDB(一般必须要掌握)
    • MongoDB 是一个基于分布式文件存储的数据库,是用C++ 编写的,主要用来处理大量的文档
    • MongoDB 是一个介于关系型数据库和非关系型数据库中间的产品!MongoDB 是非关系型数据库中功能最丰富,最像关系型数据库
  • ConthDB

列存储数据库:

  • HBase
  • 分布式文件系统

图关系数据库:

  • 它不是存图形的,放的是关系,比如:朋友圈社交网络,广告推荐
  • Neo4j,InfoGrid

在这里插入图片描述

Redis入门

Redis是什么?

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的API。

在这里插入图片描述

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

Redis 是免费和开源!是当下最热门的NoSQL之一!也被人们称为结构化数据库

Redis能干嘛?

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

Redis的特性

  • 多样的数据类型
  • 持久化
  • 集群
  • 事务

须知

Redis的官网:https://redis.io/

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

下载地址:通过官网下载即可

注意:Windows版本的Redis需要在GitHub上下载,但是已经停更很久了,因为官方不建议在Windows环境上开发Redis。

Redis推荐都是在Linux服务器上搭建的,所以我们是基于Linux学习

Windows下安装Redis

1、下载安装包 Tags · dmajkic/redis · GitHub

2、下载完毕得到压缩包

3、解压到自己电脑的目录下即可,Redis十分地小,只有5M

在这里插入图片描述

4、开启Redis,双击 redis-server.exe 运行服务即可!Redis默认端口号:6379

在这里插入图片描述

5、使用Redis客户端 redis-cli.exe 来连接Redis

在这里插入图片描述

记住一句话,Windows下使用Redis确实简单,但是Redis推荐我们使用Linux去开发使用

在这里插入图片描述

Linux下安装Redis

(后来我用了 redis-5.0.7.tar.gz 这个版本的)

1、下载安装包,在官网下载最新版本即可 redis-6.0.6.tar.gz

2、上传Redis文件

在这里插入图片描述

3、在Linux上解压Redis安装包,程序一般放到 /opt 目录下

mv redis-6.0.6.tar.gz /opt

4、解压Redis

tar -zxvf redis-6.0.6.tar.gz

在这里插入图片描述

在这里插入图片描述

5、安装基本的环境,因为Redis使用C语言写的

yum install gcc-c++

# 查看版本信息
gcc -v

# 执行make命令,它会去把所有需要的文件全部给你配置上
make

make install

6、Redis的默认安装路径在 /usr/local/bin

在这里插入图片描述

7、在当前目录(/usr/local/bin)下新建一个目录 kconfig ,将Redis的配置文件,复制到 /usr/local/bin/kconfig下

mkdir kconfig

cp /opt/redis-5.0.7/redis.conf kconfig

在这里插入图片描述

8、Redis默认不是后台启动的,我们要修改配置文件 redis.conf

在这里插入图片描述

9、通过指定的配置文件,启动Redis服务

redis-server kconfig/redis.conf

在这里插入图片描述

10、使用 redis-cli 连接测试

redis-cli -p 6379

在这里插入图片描述

11、查看Redis的进程是否开启

ps -ef|grep redis

在这里插入图片描述

12、关闭Redis服务,并且退出。此时Redis对应的进程也消失了

shutdown

在这里插入图片描述

13、后面我们会使用单机多Redis启动集群测试!

测试性能

redis-benchmark 是一个压力测试工具!

官方自带的性能测试工具

在这里插入图片描述

我们来简单测试一下,先开启Redis服务才能测试

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

在这里插入图片描述

如何查看这些分析呢?

在这里插入图片描述

Redis的基础知识

Redis默认有16个数据库

在这里插入图片描述

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

可以使用 select 进行切换数据库!

在这里插入图片描述

select 3 # 切换到数据库3

dbsize # 查看当前数据库大小

set name qinjiang # 存放key为 name ,value为 qinjiang 的数据

keys * # 查看当前数据库所有的key的值

flushdb # 清空当前数据库的数据

flushall # 清空所有数据库的数据

思考:为什么Redis是6379?(了解一下即可)

Redis是单线程的

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

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

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

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

误区2:多线程一定比单线程效率高?

核心:Redis是将所有的数据全部放在内存中的,跟CPU关系不大,所以此时使用单线程会减少CPU上下文操作,节省时间。**我们所说的多线程,是在单核CPU下的多线程,会进行上下文切换,这个切换操作就会耗费时间。**对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,对于Redis来说单线程没有上下文切换,这个就是最佳的。

关于Redis-Key的基本命令

我们现在讲解的所有命令大家一定要全部记住,后面我们使用SpringBoot,Jedis,所有的方法就是这些命令。

select 3 # 切换到数据库3

dbsize # 查看当前数据库大小

set name qinjiang # 存放key为 name ,value为 qinjiang 的数据

keys * # 查看当前数据库所有的key的值

flushdb # 清空当前数据库的数据

flushall # 清空所有数据库的数据



exists name # 判断当前数据库是否存在key为name的key

move name 1 # 把当前数据库key为name的key移动到数据库1

expire name 10 # 设置当前数据库key为name的有效值为10秒,10秒后过期

ttl name # 查看当前key为name的剩余时间

type name # 查看当前key为name的类型

后面如果遇到不会的命令,可以在官网查看帮助文档!

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)。

String字符串类型详解

90%的java程序员使用Redis只会使用一个String类型!

###############################################################################################
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> exists key1
(integer) 1
127.0.0.1:6379> append key1 "hello" # 字符串追加,如果当前key1不存在则会新建后追加字符串
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1 # 获取长度
(integer) 7
127.0.0.1:6379> append key1 ",kuangshen"
(integer) 17
127.0.0.1:6379> strlen key1 
(integer) 17
127.0.0.1:6379> get key1
"v1hello,kuangshen"
127.0.0.1:6379> 

###############################################################################################
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 加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> decr views
(integer) -1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> incrby views 10 # 一次性加10操作
(integer) 9
127.0.0.1:6379> incrby views 10
(integer) 19
127.0.0.1:6379> decrby views 5 # 一次性减5操作
(integer) 14
127.0.0.1:6379> decrby views 5
(integer) 9
127.0.0.1:6379> 

###############################################################################################
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set key1 "hello,kuangshen"
OK
127.0.0.1:6379> get key1
"hello,kuangshen"
127.0.0.1:6379> getrange key1 0 3 # 截取字符串[0,3]
"hell"
127.0.0.1:6379> getrange key1 0 -1 # 获取全部的字符串
"hello,kuangshen"
127.0.0.1:6379> 

###############################################################################################
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> setrange key2 1 xx # 替换指定位置开始的字符串
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"
127.0.0.1:6379> 

###############################################################################################
# setex(set with expire) 设置过期时间
# setnx(set if not exist)不存在则设置(这个在分布式锁中比较常用!)
127.0.0.1:6379> setex key3 30 "hello" # 设置key并且设置过期时间为30秒
OK
127.0.0.1:6379> ttl key3
(integer) 26
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis" # 如果当前key不存在,则设置key,否则不会设置key
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
2) "key2"
3) "key1"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey "redis" # 当前key存在,设置失败
(integer) 0
127.0.0.1:6379> setnx mykey "mongoDB"
(integer) 0
127.0.0.1:6379> get mykey
"redis"


# mset
# mget
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 批量设置 key-value
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 # 通过key批量获取value
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # k1存在则k1创建失败,k4虽然不存在,但也是创建失败。因为msetnx是一个原子性操作
(integer) 0
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379> 

###############################################################################################
# 对象
set user:1 {name:zhangsan,age:3} # 设置一个 user:1 对象 值为JSON字符串来保存一个对象!

# 这里的key是一个巧妙的设计 user:{id}:{filed} ,如此设计在Redis中是完全可以的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
127.0.0.1:6379> 

###############################################################################################
getset # 先get再set

127.0.0.1:6379> keys *
1) "k2"
2) "user:1:name"
3) "user:1:age"
4) "k1"
5) "k3"
6) "db"
127.0.0.1:6379> getset db1 redis # 先get后set,如果不存在值则返回nil
(nil)
127.0.0.1:6379> get db1
"redis"
127.0.0.1:6379> getset db1 mongodb # 如果存在值,先获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db1
"mongodb"
127.0.0.1:6379> 

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

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存存储

List列表类型详解

在Redis里面,我们可以把List玩成 栈、队列、阻塞队列!

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

####################################################################################
127.0.0.1:6379> lpush list one # 将一个值或者多个值,插入到列表的头部(从列表的左边插入)
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 获取list中全部的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 # 通过区间获取list中的值
1) "three"
2) "two"
127.0.0.1:6379> rpush list right # 将一个值或者多个值,插入到列表的尾部(从列表的右边插入)
(integer) 4
127.0.0.1:6379> lrange list 0 -1 
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> lpop list # 移除列表的第一个元素
"three"
127.0.0.1:6379> rpop list # 移除列表的最后一个元素
"right"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1 # 通过下标获得list中的值
"one"
127.0.0.1:6379> lindex list 0
"two"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> lpush list one
(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> llen list # 返回当前list的长度
(integer) 3

####################################################################################
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one # 移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 1 three
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrem list 2 three # 移除list集合中指定个数的value,精确匹配
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
127.0.0.1:6379> 

####################################################################################
# trim 修剪
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> rpush mylist "hello3"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2 # 通过list下标截取指定的元素,这个list已经被改变了,截断了,list只剩下截取的元素
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
127.0.0.1:6379> 

####################################################################################
# rpoplpush 移除列表的最后一个元素,并且把这个元素添加到另外一个列表
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
127.0.0.1:6379> rpoplpush mylist myotherlist # 移除mylist列表的最后一个元素,并且把这个元素添加到另外一个myotherlist列表
"hello2"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello2"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> exists list # 判断是否存在list列表
(integer) 0
127.0.0.1:6379> lset list 0 item # 操作失败,list列表不存在
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value1"
127.0.0.1:6379> lset list 0 item # 将list列表中指定下标的值替换为另外一个值,相当于更新操作
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
127.0.0.1:6379> lset list 1 other # 操作失败,下标不存在
(error) ERR index out of range
127.0.0.1:6379> 

####################################################################################
# linsert 将某个具体的value插入到列表中某个元素的前面或者后面
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "world"
(integer) 2
127.0.0.1:6379> linsert mylist before "world" "other" # 将other插入到world的前面
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> linsert mylist after world new # 将new插入到world的后面
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"
127.0.0.1:6379> 

小结:

  • list实际上是一个链表,before Node after,左边或者右边都可以插入值
  • 如果key不存在,可以创建新的链表
  • 如果key存在,新增内容
  • 如果移除了key,也就是list移除了,那么所有的value都消失了,代表不存在
  • 在两边插入或者改动值,效率最高

使用场景:

  • 消息排队
  • 消息队列
  • 既可以作为队列,也可以作为栈

Set集合类型详解

set中的值,不能重复!

####################################################################################
127.0.0.1:6379> sadd myset hello # 给myset集合中添加hello元素
(integer) 1
127.0.0.1:6379> sadd myset kuangshen
(integer) 1
127.0.0.1:6379> sadd myset lovekuangshen
(integer) 1
127.0.0.1:6379> smembers myset # 查看myset集合的所有值
1) "lovekuangshen"
2) "kuangshen"
3) "hello"
127.0.0.1:6379> sismember myset hello # 判断hello元素是不是在myset集合中
(integer) 1
127.0.0.1:6379> sismember myset world
(integer) 0
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> scard myset # 获取set集合中的元素的个数
(integer) 3
127.0.0.1:6379> sadd myset lovekuangshen # 不能添加重复值
(integer) 0
127.0.0.1:6379> sadd myset lovekuangshen2
(integer) 1
127.0.0.1:6379> scard myset
(integer) 4
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> srem myset hello # 移除myset集合中的hello元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 3
127.0.0.1:6379> smembers myset
1) "lovekuangshen2"
2) "lovekuangshen"
3) "kuangshen"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> srandmember myset # 从myset集合中随机取出一个元素
"kuangshen"
127.0.0.1:6379> srandmember myset
"lovekuangshen2"
127.0.0.1:6379> srandmember myset
"kuangshen"
127.0.0.1:6379> srandmember myset
"lovekuangshen"
127.0.0.1:6379> srandmember myset 2 # 从myset集合中随机取出两个元素
1) "lovekuangshen2"
2) "kuangshen"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> smembers myset
1) "lovekuangshen2"
2) "lovekuangshen"
3) "kuangshen"
127.0.0.1:6379> spop myset # 随机移除集合中的一个元素
"kuangshen"
127.0.0.1:6379> spop myset 
"lovekuangshen2"
127.0.0.1:6379> smembers myset
1) "lovekuangshen"
127.0.0.1:6379> 

####################################################################################
 127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset world
(integer) 1
127.0.0.1:6379> sadd myset kuangshen
(integer) 1
127.0.0.1:6379> sadd myset2 set2
(integer) 1
127.0.0.1:6379> smove myset myset2 kuangshen # 把myset集合中的kuangshen元素移动到myset2集合中
(integer) 1
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
127.0.0.1:6379> smembers myset2
1) "set2"
2) "kuangshen"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> sadd key1 a
(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集合的差集
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2 # key1集合和key2集合的交集
1) "c"
127.0.0.1:6379> sunion key1 key2 # key1集合和key2集合的并集
1) "b"
2) "a"
3) "c"
4) "e"
5) "d"
127.0.0.1:6379> 

Hash哈希类型详解

把它想象成key-map,map集合本身是key-value

本质上还是key-value,只是这个value是一个map集合

####################################################################################
127.0.0.1:6379> hset myhash field1 kuangshen # set一个具体的key-value,key为myhash,value为field1-kuangshen
(integer) 1
127.0.0.1:6379> hget myhash field1 # 获取一个字段值
"kuangshen"
127.0.0.1:6379> hmset myhash field1 hello field2 world # 同一个key下,同时set多个的value
OK
127.0.0.1:6379> hmget myhash field1 field2 # 获取同一个key下的多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall myhash # 获取hash中全部的数据
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> hdel myhash field1 # 删除hash指定key中的指定字段
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"

####################################################################################
127.0.0.1:6379> hmset myhash field1 hello field2 world
OK
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> hlen myhash # 获取hash表的字段数量
(integer) 2
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> hexists myhash field1 # 判断hash中指定key中的指定字段是否存在
(integer) 1
127.0.0.1:6379> hexists myhash field3
(integer) 0
127.0.0.1:6379> hkeys myhash # 获取hash中指定key的所有字段
1) "field2"
2) "field1"
127.0.0.1:6379> hvals myhash # 获取hash中指定key的所有字段对应的值
1) "world"
2) "hello"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> hset myhash field3 5
(integer) 1
127.0.0.1:6379> hincrby myhash field3 1 # 对指定key中的指定字段的值进行 +1 操作
(integer) 6
127.0.0.1:6379> hincrby myhash field3 -1 # 对指定key中的指定字段的值进行 +(-1) 操作
(integer) 5
127.0.0.1:6379> hsetnx myhash field4 hello # 如果指定key中的指定字段不存在则创建并且赋值
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 world # 指定key中的指定字段存在,set失败
(integer) 0
127.0.0.1:6379> 

hash更适合于对象的存储,String更加适合字符串存储!

Zset有序集合详解

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

####################################################################################
127.0.0.1:6379> zadd myset 1 one # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 在同一个key中添加多个值
(integer) 1
127.0.0.1:6379> zrange myset 0 -1 # 遍历
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> zadd salary 2500 xiaohong
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 kuangshen
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf # -inf负无穷,+inf正无穷。从小到大排序显示
1) "kuangshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores # 从小到大排序显示,并且附带score参数显示
1) "kuangshen"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores # 2500以下从小到大排序显示,并且附带score参数显示
1) "kuangshen"
2) "500"
3) "xiaohong"
4) "2500"
127.0.0.1:6379> 

####################################################################################
127.0.0.1:6379> zrange salary 0 -1
1) "kuangshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong # 移除salary中的xiaohong元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kuangshen"
2) "zhangsan"
127.0.0.1:6379> zcard salary # 获取有序集合中的指定key的元素个数
(integer) 2
127.0.0.1:6379> zrevrange salary 0 -1 # 补充:对指定key中的元素从大到小进行排序显示
1) "zhangsan"
2) "kuangshen"
127.0.0.1:6379> 


####################################################################################
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 kuangshen
(integer) 2
127.0.0.1:6379> zcount myset 1 3 # 获取指定区间的元素个数
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
127.0.0.1:6379> 

使用场景:

  • 存储班级成绩表
  • 工资表排序
  • 消息重要性的排序
  • 排行榜应用实现

Redis三种特殊数据类型

Geospatial地理位置详解

朋友的定位,附近的人,打车距离的计算。。。

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

可以查询一些测试数据:城市经纬度查询-国内城市经度纬度在线查询工具 (jsons.cn)

Geo只有六个命令:

在这里插入图片描述

官方文档关于Geo的介绍:https://www.redis.net.cn/order/3685.html

  • geoadd

添加地理位置

# geoadd 添加地理位置
# 规则:两极(北极和南极)无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
# 有效的经度从-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 114.05 22.52 shenzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
127.0.0.1:6379> 

  • 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 beijing chongqing
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
2) 1) "106.49999767541885376"
   2) "29.52999957900659211"
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
"1464070.8051"
127.0.0.1:6379> geodist china:city beijing chongqing km # 查看北京到重庆的直线距离(km)
"1464.0708"

  • georadius

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

127.0.0.1:6379> georadius china:city 110 30 1000 km # 以 110,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 # 以 110,30 这个经纬度为中心,寻找方圆500km以内的城市,并且显示对应的直线距离
1) 1) "chongqing"
   2) "341.9374"
2) 1) "xian"
   2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord # 以 110,30 这个经纬度为中心,寻找方圆500km以内的城市,并且显示对应的经纬度
1) 1) "chongqing"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 1 # 以 110,30 这个经纬度为中心,寻找方圆500km以内的城市,并且显示对应的经纬度,和直线距离,并且只显示一个
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

找出位于指定范围内的元素,中心点是由给定的位置元素决定

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 
1) "hangzhou"
2) "shanghai"
127.0.0.1:6379> 
  • geohash

返回一个或多个位置元素的 Geohash

该命令将返回11个字符的Geohash字符串

# 将二维的经纬度转换为一维的字符串,如果两个字符串越像,那么这两个地方的距离越近

127.0.0.1:6379> geohash china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
127.0.0.1:6379> 

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

127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 移除地理位置
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
127.0.0.1:6379> 

Hyperloglog基数统计

什么是基数?

A{1,3,5,7,8,7}

B{1,3,5,7,8}

基数就是一个集合内不重复的元素的个数,比如这里A的基数是5,B的基数是5

简介:在Redis 2.8.9 版本就更新了hyperloglog数据结构

Redis Hyperloglog 是用来做基数统计的算法

使用场景:网站的访问量(一个人访问一个网站多次,但是只算做一次访问量)

传统方式:使用 set 集合保存用户id,统计 set 集合中元素数量作为标准判断!

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

使用Hyperloglog统计网站访问量的优点:

  • 占用的内存是固定的,只需要12kb内存

测试使用:

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

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

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

Bitmap位图场景详解

使用场景:统计用户活跃或者不活跃,登录或者未登录,打卡或者未打卡。像这样两个状态的都可以使用 Bitmap!

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

测试:

例子:比如使用bitmap来记录周一到周日的打卡

周一:1 周二:0 周三:0 周四:1 周五:1 周六:0 周日:0

127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0

查看某一天的打卡情况

127.0.0.1:6379> getbit sign 3 # 查看周四打卡情况
(integer) 1
127.0.0.1:6379> getbit sign 6 # 查看周日打卡情况
(integer) 0
127.0.0.1:6379> 

统计打卡的天数

127.0.0.1:6379> bitcount sign # 查看打卡天数
(integer) 3
127.0.0.1:6379> 

Redis基本的事务操作

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

一次性、顺序性、排它性

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

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

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

Redis事务的三个阶段

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

Redis可以实现乐观锁

正常执行事务:

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 k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379> 

放弃事务:

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> discard # 取消事务,事务队列中的命令都不会被执行
OK
127.0.0.1:6379> get k4 # get不到k4的值
(nil)
127.0.0.1:6379> 

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

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> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec # 执行事务报错,所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # get不到k5的值
(nil)
127.0.0.1:6379> 

运行时异常,如果事务队列中存在语法性错误,那么执行命令的时候,其它命令是可以正常执行的,错误命令会抛出异常

127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 运行时异常的命令,字符串不能进行+1操作
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 虽然第一条命令报错了,但是其它命令依旧正常执行
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> 

Redis实现乐观锁

悲观锁:

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

乐观锁:

  • 很乐观,认为什么时候都不会出现问题,所以不会加锁。更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 乐观锁在MySQL中是通过version实现乐观锁的

Redis的监视测试:

这是正常执行成功的情况

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功
1) (integer) 80
2) (integer) 20
127.0.0.1:6379> 

测试多线程下修改值的情况(开启两个客户端模拟多线程)

这是第一个客户端

127.0.0.1:6379> watch money # 使用watch相当于乐观锁
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec # 在事务将要执行前,下面的客户端把money的值修改了,money被监视着,所以事务执行失败
(nil)
127.0.0.1:6379> 


#############################################################################################
127.0.0.1:6379> unwatch # 如果发现事务执行失败则先unwatch取消监控,相当于解锁
OK
127.0.0.1:6379> watch money # 重新watch监控,相当于重新上锁
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec # 这里执行事务前会先比对监视的值money是否发生变化,若变化则事务执行失败
1) (integer) 990
2) (integer) 30
127.0.0.1:6379> 

这是第二个客户端

127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> 

Jedis

我们要使用Java来操作Redis

什么是Jedis?

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

测试:

打开IDEA,创建一个空项目

在这里插入图片描述

配置到这个项目的JDK和lambda

在这里插入图片描述

新建一个普通maven项目的module

在这里插入图片描述

还要配置一下编译器的版本,为1.8

在这里插入图片描述

导入依赖

<dependencies>
    <!--导入jedis的包-->
    <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.62</version>
    </dependency>
</dependencies>

测试连接数据库

package com.kuang;

import redis.clients.jedis.Jedis;

public class TestPing {
    public static void main(String[] args) {
        // new一个Jedis对象
        // 这里我们使用本机的Redis作为测试
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis的很多方法就是我们之前学习的所有命令
        System.out.println(jedis.ping());// 输出PONG则代表成功

    }
}

Jedis的常用API

所有的API代码就是对应我们上面学习的命令

String、List、Set、Hash、Zset

Key相关的

package com.kuang;

import redis.clients.jedis.Jedis;

import java.util.Set;

public class TestKey {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        System.out.println("清空数据:"+jedis.flushDB());
        System.out.println("判断某个键是否存在:"+jedis.exists("username"));
        System.out.println("新增<'username','kuangshen'>的键值对:"+jedis.set("username", "kuangshen"));
        System.out.println("新增<'password','password'>的键值对:"+jedis.set("password", "password"));
        System.out.print("系统中所有的键如下:");
        Set<String> keys = jedis.keys("*");// 查看所有的keys
        System.out.println(keys);
        System.out.println("删除键password:"+jedis.del("password"));
        System.out.println("判断键password是否存在:"+jedis.exists("password"));
        System.out.println("查看键username所存储的值的类型:"+jedis.type("username"));
        System.out.println("随机返回key空间的一个:"+jedis.randomKey());
        System.out.println("重命名key:"+jedis.rename("username","name"));
        System.out.println("取出改后的name:"+jedis.get("name"));
        System.out.println("按索引查询:"+jedis.select(0));
        System.out.println("删除当前选择数据库中的所有key:"+jedis.flushDB());
        System.out.println("返回当前数据库中key的数目:"+jedis.dbSize());
        System.out.println("删除所有数据库中的所有key:"+jedis.flushAll());
    }
}

String相关的

package com.kuang;

import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

public class TestString {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        jedis.flushDB();
        System.out.println("===========增加数据===========");
        System.out.println(jedis.set("key1","value1"));
        System.out.println(jedis.set("key2","value2"));
        System.out.println(jedis.set("key3", "value3"));
        System.out.println("删除键key2:"+jedis.del("key2"));
        System.out.println("获取键key2:"+jedis.get("key2"));
        System.out.println("修改key1:"+jedis.set("key1", "value1Changed"));
        System.out.println("获取key1的值:"+jedis.get("key1"));
        System.out.println("在key3后面加入值:"+jedis.append("key3", "End"));
        System.out.println("key3的值:"+jedis.get("key3"));
        System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03","key04"));
        System.out.println("删除多个键值对:"+jedis.del("key01","key02"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));

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

        System.out.println("===========新增键值对并设置有效时间=============");
        System.out.println(jedis.setex("key3", 2, "value3"));
        System.out.println(jedis.get("key3"));
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(jedis.get("key3"));

        System.out.println("===========获取原值,更新为新值==========");
        System.out.println(jedis.getSet("key2", "key2GetSet"));
        System.out.println(jedis.get("key2"));

        System.out.println("获得key2的值的字串:"+jedis.getrange("key2", 2, 4));
    }
}

List相关的

package com.kuang;

import redis.clients.jedis.Jedis;

public class TestList {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("===========添加一个list===========");
        jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
        jedis.lpush("collections", "HashSet");
        jedis.lpush("collections", "TreeSet");
        jedis.lpush("collections", "TreeMap");
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部
        System.out.println("collections区间0-3的元素:"+jedis.lrange("collections",0,3));
        System.out.println("===============================");
        // 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
        System.out.println("删除指定元素个数:"+jedis.lrem("collections", 2, "HashMap"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("删除下表0-3区间之外的元素:"+jedis.ltrim("collections", 0, 3));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections列表出栈(左端):"+jedis.lpop("collections"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections添加元素,从列表右端,与lpush相对应:"+jedis.rpush("collections", "EnumMap"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections列表出栈(右端):"+jedis.rpop("collections"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("修改collections指定下标1的内容:"+jedis.lset("collections", 1, "LinkedArrayList"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("===============================");
        System.out.println("collections的长度:"+jedis.llen("collections"));
        System.out.println("获取collections下标为2的元素:"+jedis.lindex("collections", 2));
        System.out.println("===============================");
        jedis.lpush("sortedList", "3","6","2","0","7","4");
        System.out.println("sortedList排序前:"+jedis.lrange("sortedList", 0, -1));
        System.out.println(jedis.sort("sortedList"));
        System.out.println("sortedList排序后:"+jedis.lrange("sortedList", 0, -1));
    }
}

Set相关的

package com.kuang;

import redis.clients.jedis.Jedis;

public class TestSet {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("============向集合中添加元素(不重复)============");
        System.out.println(jedis.sadd("eleSet", "e1","e2","e4","e3","e0","e8","e7","e5"));
        System.out.println(jedis.sadd("eleSet", "e6"));
        System.out.println(jedis.sadd("eleSet", "e6"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("删除一个元素e0:"+jedis.srem("eleSet", "e0"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("删除两个元素e7和e6:"+jedis.srem("eleSet", "e7","e6"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("随机的移除集合中的一个元素:"+jedis.spop("eleSet"));
        System.out.println("随机的移除集合中的一个元素:"+jedis.spop("eleSet"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("eleSet中包含元素的个数:"+jedis.scard("eleSet"));
        System.out.println("e3是否在eleSet中:"+jedis.sismember("eleSet", "e3"));
        System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet", "e1"));
        System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet", "e5"));
        System.out.println("=================================");
        System.out.println(jedis.sadd("eleSet1", "e1","e2","e4","e3","e0","e8","e7","e5"));
        System.out.println(jedis.sadd("eleSet2", "e1","e2","e4","e3","e0","e8"));
        System.out.println("将eleSet1中删除e1并存入eleSet3中:"+jedis.smove("eleSet1", "eleSet3", "e1"));//移到集合元素
        System.out.println("将eleSet1中删除e2并存入eleSet3中:"+jedis.smove("eleSet1", "eleSet3", "e2"));
        System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));
        System.out.println("eleSet3中的元素:"+jedis.smembers("eleSet3"));
        System.out.println("============集合运算=================");
        System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));
        System.out.println("eleSet2中的元素:"+jedis.smembers("eleSet2"));
        System.out.println("eleSet1和eleSet2的交集:"+jedis.sinter("eleSet1","eleSet2"));
        System.out.println("eleSet1和eleSet2的并集:"+jedis.sunion("eleSet1","eleSet2"));
        System.out.println("eleSet1和eleSet2的差集:"+jedis.sdiff("eleSet1","eleSet2"));//eleSet1中有,eleSet2中没有
        jedis.sinterstore("eleSet4","eleSet1","eleSet2");//求交集并将交集保存到dstkey的集合
        System.out.println("eleSet4中的元素:"+jedis.smembers("eleSet4"));
    }
}

Hash相关的

package com.kuang;

import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.Map;

public class TestHash {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        Map<String,String> map = new HashMap<String,String>();
        map.put("key1","value1");
        map.put("key2","value2");
        map.put("key3","value3");
        map.put("key4","value4");
        //添加名称为hash(key)的hash元素
        jedis.hmset("hash",map);
        //向名称为hash的hash中添加key为key5,value为value5元素
        jedis.hset("hash", "key5", "value5");
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));//return Map<String,String>
        System.out.println("散列hash的所有键为:"+jedis.hkeys("hash"));//return Set<String>
        System.out.println("散列hash的所有值为:"+jedis.hvals("hash"));//return List<String>
        System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 6));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 3));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("删除一个或者多个键值对:"+jedis.hdel("hash", "key2"));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("散列hash中键值对的个数:"+jedis.hlen("hash"));
        System.out.println("判断hash中是否存在key2:"+jedis.hexists("hash","key2"));
        System.out.println("判断hash中是否存在key3:"+jedis.hexists("hash","key3"));
        System.out.println("获取hash中的值:"+jedis.hmget("hash","key3"));
        System.out.println("获取hash中的值:"+jedis.hmget("hash","key3","key4"));
    }
}

事务

package com.kuang;

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

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

        jedis.flushDB();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","world");
        jsonObject.put("name","kuangshen");

        // 开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();

        try {
            multi.set("user1",result);
            multi.set("user2",result);

            int i = 1/0;// 代码抛出异常,事务执行失败

            multi.exec();// 执行事务
        } catch (Exception e) {
            multi.discard();// 如果抛出异常,则放弃事务。在Redis中如果不放弃事务,运行时异常不会影响其它正常命令的执行
            e.printStackTrace();
        } finally {
            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

jedis:底层采用的是直接连接,多个线程操作的话,是不安全的,如果想要避免不安全的情况,则需要使用jedis pool连接池解决!更像 BIO 模式

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

SpringBoot所有的配置类,都有一个自动配置类(Redis的是RedisAutoConfiguration)
自动配置类都会绑定一个 properties 配置文件(Redis的是RedisProperties)

SpringBoot的Redis的源码分析

在这里插入图片描述

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.data.redis;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

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

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class) // 我们可以自己定义一个redisTemplate来替换默认的
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 默认的 RedisTemplate 没有过多的设置,Redis中对象的保存都是需要序列化的
        // 两个泛型都是 Object 的类型,我们后面使用需要强制转换 <String,Object>
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean // 由于String是Redis中最常使用的类型,所以说单独提出来了一个bean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

整合测试一下

新建一个SpringBoot的Module项目

在这里插入图片描述

导入相关依赖

在这里插入图片描述

连接配置 application.properties

# SpringBoot所有的配置类,都有一个自动配置类(Redis的是RedisAutoConfiguration)
# 自动配置类都会绑定一个 properties 配置文件(Redis的是RedisProperties)

# 配置Redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0

测试

package com.kuang;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {

        // 在企业开发中,我们80%的情况下,都不会使用这个原生的方式去编写代码

        // redisTemplate 有不同的方法可以操作不同的数据类型,对应的api和我们之前学过的Redis的指令是一样的

        // opsForValue 操作String
        // opsForList 操作List
        // opsForSet 操作Set
        // opsForHash 操作Hash
        // opsForZSet
        // opsForGeo
        // opsForHyperLogLog
        // ......

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

        redisTemplate.opsForValue().set("mykey","kuangshen");
        System.out.println(redisTemplate.opsForValue().get("mykey"));
    }

}

看一下Redis的序列化配置

在这里插入图片描述

在这里插入图片描述

默认的序列化方式是JDK,我们可能使用JSON来序列化,所以我们一般会自定义RedisTemplate。

自定义RedisTemplate

通过RedisConfig自定义RedisTemplate

package com.kuang.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    // 这是写好的一个固定模板,大家在企业中,拿去就可以直接使用
    // 自定义 RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);

        // Jackson 序列化配置
        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;
    }


}
package com.kuang.pojo;

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

import java.io.Serializable;

@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
// 在企业中,我们的所有 pojo 都会序列化
public class User implements Serializable{

    private String name;
    private int age;

}
package com.kuang;

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


import javax.annotation.Resource;

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Resource
    private RedisTemplate redisTemplate;// 自动装配到我们自定义的RedisTemplate

    @Test
    public void test() throws JsonProcessingException {

        User user = new User("狂神说", 3);

        // 真实的开发一般都使用json来传递对象
        // String jsonUser = new ObjectMapper().writeValueAsString(user);

        // redisTemplate.opsForValue().set("user",jsonUser);
        redisTemplate.opsForValue().set("user",user);// 如果直接传对象,这个User类必须实现Serializable接口,实现序列化

        System.out.println(redisTemplate.opsForValue().get("user"));
    }
}

在我们真实的开发中,或者在公司,一般都可以看到一个公司自己封装的 RedisUtil

package com.kuang.utils;

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

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

// 在我们真实的开发中,或者在公司,一般都可以看到一个公司自己封装的 RedisUtil
@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

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

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


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


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


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

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

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

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


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

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


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


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


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

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

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

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


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


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

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


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


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


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


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


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

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


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


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


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


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


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

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

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

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


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


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


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


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

    }


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

    }


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


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

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


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

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

    }

}
package com.kuang;

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


import javax.annotation.Resource;

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RedisUtil redisUtil;

    @Test
    public void test1(){
        redisUtil.set("name","kuangshen");
        System.out.println(redisUtil.get("name"));
    }

}

小结

所有的Redis操作,其实对于Java开发人员来说,十分简单,更重要的是要去理解redis的思想和每一种数据结构的用处及作用场景!

Redis配置文件的详解

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

在这里插入图片描述

  • 单位

在这里插入图片描述

unit单位对大小写不敏感

  • 包含

在这里插入图片描述

就好比我们学习spring的时候,可以把多个配置文件都包含进来

  • 网络
bind 127.0.0.1 # 绑定的IP

protected-mode yes # 保护模式开启

port 6379 # 端口设置

  • 通用
daemonize yes # 以守护进程的方式运行,默认是no,我们需要自己开启为yes

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

# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice # 默认日志级别是 notice

logfile "" # 生成日志的文件位置名

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

always-show-logo yes # 是否总是显示logo
  • 快照

Redis是一个内存数据库,如果不持久化的话会丢失数据

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

save 900 1 # 如果900秒内,至少有一个key进行过修改,我们就进行持久化操作

save 300 10 # 如果300秒内,至少有十个key进行过修改,我们就进行持久化操作

save 60 10000 # 如果60秒内,至少有一万个key进行过修改,我们就进行持久化操作

# 我们之后学习持久化,会自定义持久化规则来测试

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

rdbcompression yes # 是否压缩rdb文件,需要消耗一些CPU的资源

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

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


  • REPLICATION(复制),我们后面讲解主从复制的时候再进行讲解
# replicaof <masterip> <masterport>

# masterauth <master-password> # 如果主机有密码,把密码配上即可
  • SECURITY(安全)

Redis密码设置:在配置文件中可以设置密码,也可以在命令行中设置密码

在命令行中设置密码时一次性的,配置文件中设置密码时永久性的

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass # 获取Redis的密码,一开始默认是没有密码的
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置Redis的密码
OK
127.0.0.1:6379> config get requirepass
(error) NOAUTH Authentication required.
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # 需要进行密码的验证,才能进行相关命令的操作
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> 

  • CLIENTS(客户端)
# maxclients 10000 # 设置能连接上Redis的客户端的最大数量
  • MEMORY MANAGEMENT(内存管理)
# maxmemory <bytes> # Redis设置最大的内存容量

# maxmemory-policy noeviction # 内存到达上限的处理策略
    # 六种处理策略
    1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
    2、allkeys-lru : 删除lru算法的key   
    3、volatile-random:随机删除即将过期key   
    4、allkeys-random:随机删除   
    5、volatile-ttl : 删除即将过期的   
    6、noeviction : 永不过期,返回错误
  • APPEND ONLY MODE(AOF的配置)
appendonly no # 默认是不开启AOF模式的,默认是使用RDB进行持久化的,在大部分的情况下,RDB完全够用了

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

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

具体的配置,我们在Redis持久化中去给大家详细讲解

Redis持久化

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

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

RDB(Redis DataBase)

什么是RDB?

在这里插入图片描述

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

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

RDB的缺点是最后一次持久化后的数据可能丢失

我们默认的就是RDB,一般情况下不需要修改这个配置

有时候在生产环境我们会将这个rdb文件进行备份

**RDB保存的文件是 dump.rdb,**都是在Redis配置文件的快照中进行配置的!

在这里插入图片描述

测试一下:(测试完后,恢复默认配置)

在这里插入图片描述

RDB的触发机制

  • save的规则满足的情况下,会自动触发rdb规则,产生 .rdb 文件
  • 执行了flushall命令,也会触发rdb规则,产生 .rdb 文件
  • 退出Redis,也会触发rdb规则,产生 .rdb 文件

在这里插入图片描述

如何恢复rdb文件

  • 只需要将rdb文件放在我们的redis的启动目录下就可以了,redis启动的时候会自动检查rdb文件,恢复其中的数据

  • 查看需要存放的位置

    127.0.0.1:6379> config get dir
    1) "dir"
    2) "/usr/local/bin" # 如果在这个目录下存在rdb文件,启动就会自动恢复其中的数据
    127.0.0.1:6379> 
    

RDB的优点

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

RDB的缺点

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

AOF(Append Only File)

通俗来讲:就是将我们的所有命令都记录下来,恢复的时候就把所有命令重新执行一遍,以此来实现数据的持久化

AOF是什么?

在这里插入图片描述

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

AOF保存的文件是 appendonly.aof

AOF默认是不开启的,我们需要手动配置开启**(开启AOF测试完后,需要关闭AOF,因为用RDB进行持久化已经足够了)**

在这里插入图片描述

重启Redis即可看到生成的aof文件

在这里插入图片描述

当AOF文件被破坏

在这里插入图片描述

当aof文件被破坏的时候,redis启动会失败
在这里插入图片描述

如果aof文件有错误,这时候redis是无法启动的!我们需要修复这个aof文件

Redis给我们提供了一个工具 redis-check-aof ,可以修复aof文件

在这里插入图片描述

redis-check-aof --fix appendonly.aof

在这里插入图片描述

修复aof后,即可重启Redis,修复过aof后可能会有一些数据丢失

在这里插入图片描述

AOF的相关配置

appendonly no # 默认是不开启AOF模式的,默认是使用RDB进行持久化的,在大部分的情况下,RDB完全够用了

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

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

在这里插入图片描述

如果aof文件大于64m,会fork一个新的进程,来将我们的文件进行重写。aof默认就是文件的无限追加,文件会越来越大!

AOF的优点

  • 每一次修改都同步,文件的完整性会更加好!
  • aof数据完整性比rdb好

AOF的缺点

  • 相对于数据文件来说,aof占内存远远大于rdb,修复的速度也比rdb慢
  • aof运行效率也要比rdb慢,所以说Redis默认的配置就是用rdb来进行持久化的

扩展

在这里插入图片描述

在这里插入图片描述

Redis订阅发布

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

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

订阅/发布消息图:

消息发布者—>频道—>消息订阅者

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

测试,开启两个redis,一个redis作为消息发布者,一个redis作为消息订阅者

# 消息发布者
127.0.0.1:6379> publish kuangshenshuo "hello,kuangshen" # 发布者发布消息到频道
(integer) 1
127.0.0.1:6379> publish kuangshenshuo "hello,redis"
(integer) 1
127.0.0.1:6379> 

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

# 消息订阅者
127.0.0.1:6379> subscribe kuangshenshuo # 订阅一个频道,kuangshenshuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kuangshenshuo"
3) (integer) 1
1) "message" # 等待读取推送的信息
2) "kuangshenshuo" # 消息来自的频道
3) "hello,kuangshen" # 消息的具体内容
1) "message"
2) "kuangshenshuo"
3) "hello,redis"

Redis订阅发布的原理

在这里插入图片描述

使用的场景

  • 实时消息系统
  • 实时聊天(把频道当做聊天室即可)
  • 订阅,关注系统都是可以的

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

Redis主从复制

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

主从复制的概念

在这里插入图片描述

在这里插入图片描述

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

Redis集群环境搭建

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

127.0.0.1:6379> info replication # 查看当前库信息
# Replication
role:master # 角色:master
connected_slaves:0 # 没有从机
master_replid:e7c838117c5704cbf5294f4860d60c77ea6c547e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379> 

在这里插入图片描述

集群环境搭建

先复制三份配置文件

[root@iZwz9cj1ytrolpgw50tiksZ bin]# cd kconfig/
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# ls
redis.conf
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# cp redis.conf redis79.conf
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# cp redis.conf redis80.conf
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# cp redis.conf redis81.conf
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# ls
redis79.conf  redis80.conf  redis81.conf  redis.conf

在这里插入图片描述

修改redis79.conf配置文件

logfile "6379.log"

dbfilename dump6379.rdb

修改redis80.conf配置文件

port 6380 # 端口号

pidfile /var/run/redis_6380.pid # pid

logfile "6380.log" # 日志文件名字

dbfilename dump6380.rdb # rdb文件名字

修改redis81.conf配置文件

port 6381

pidfile /var/run/redis_6381.pid

logfile "6381.log"

dbfilename dump6381.rdb

修改完毕之后,分别启动79、80、81这三个redis服务

redis-server kconfig/redis79.conf

redis-server kconfig/redis80.conf

redis-server kconfig/redis81.conf

在这里插入图片描述

一主二从

默认情况下,每台Redis服务器都是主节点。我们一般情况下,只需要配置从机

我们来测试一下:一主(79当主机)二从(80,81当从机)

下面是通过命令行配置从机的,这是暂时的。真实的主从配置应该是在配置文件中配置的,这样才是永久生效的!!!

配置80当从机

127.0.0.1:6380> slaveof 127.0.0.1 6379 # 80认79做老大,即80是79的从机
OK
127.0.0.1:6380> info replication # 查看当前库信息
# Replication
role:slave # 当前80角色:从机
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a53a023d382aa8db227a75882a098cda5f1ec491
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
127.0.0.1:6380> 

配置81当从机

127.0.0.1:6381> slaveof 127.0.0.1 6379 # 81认79做老大,即81是79的从机
OK
127.0.0.1:6381> info replication # 查看当前库信息
# Replication
role:slave # 当前81角色:从机
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:476
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a53a023d382aa8db227a75882a098cda5f1ec491
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:476
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:477
repl_backlog_histlen:0
127.0.0.1:6381> 

此时来查看一下79的信息

127.0.0.1:6379> info replication # 查看当前库信息
# Replication
role:master # 当前79角色:主机
connected_slaves:2 # 
slave0:ip=127.0.0.1,port=6380,state=online,offset=616,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=616,lag=1
master_replid:a53a023d382aa8db227a75882a098cda5f1ec491
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:616
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:616
127.0.0.1:6379> 

细节

**主机负责写。从机负责读,从机不能写!!!**主机中的所有信息和数据,都会自动被从机保存!

在这里插入图片描述

在这里插入图片描述

重启主机:

  • 当主机断开连接后,从机依旧能连接到主机,但是没有写操作了(因为主机断开了连接)。当主机重新连接后,从机依旧可以获取主机写的信息。

重启从机:

  • 如果是使用命令行配置主从,这个时候如果重启了从机,那这个从机就会变为主机。只要重新变成从机,那么立马就能从主机中获取值!

复制原理

Slave启动成功连接到Master后,会发送一个sync(同步数据)命令

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

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

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

只要是重新连接Master,将会自动执行一次全量复制。我们的数据一定可以在从机中看到!

层层链路的主从复制

主机—>从机—>从机—>从机—>从机—>从机—>…—>从机

这个时候也可以完成我们的主从复制

在这里插入图片描述

主机宕机后手动配置主机

如果主机断开了连接,我们可以手动输入命令让自己变成主机。其它节点就可以手动连接到最新的这个主机,变成从机

slaveof no one # 让自己变成主机

哨兵模式

(自动选举主机)

概述

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

哨兵模式

  • 谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
  • 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例

在这里插入图片描述

这里的哨兵有两个作用

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

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

在这里插入图片描述

假设主服务器宕机了,哨兵1先检测到这个结果,系统并不会马上进行failover(故障转移),仅仅是哨兵1主观地认为主服务器不可用,这个现象称为主观下线

当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票(投票算法),投票的结果由其中一个哨兵发起,进行failover(故障转移)操作,重新选举出一个主机后,就会通过发布订阅者模式,让各个哨兵把自己监控的从服务器重新切换,连接到新选举出来的主机。这个过程称为客观下线

测试

配置哨兵

[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# ll
total 256
-rw-r--r-- 1 root root 61810 Jul 28 11:28 redis79.conf
-rw-r--r-- 1 root root 61810 Jul 28 11:39 redis80.conf
-rw-r--r-- 1 root root 61810 Jul 28 11:43 redis81.conf
-rw-r--r-- 1 root root 61798 Jul 27 17:48 redis.conf
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# vim sentinel.conf # 新建并且配置哨兵配置文件
[root@iZwz9cj1ytrolpgw50tiksZ kconfig]# 

sentinel.conf的文本内容如下

# sentinel monitor 被监控的名称 host port 后面的这个数字1,代表主机挂了需要在从机中重新投票选出一个主机
sentinel monitor myredis 127.0.0.1 6379 1

启动哨兵

[root@iZwz9cj1ytrolpgw50tiksZ bin]# ls
6379.log  appendonly.aof  cloud-init      dump6380.rdb  easy_install      jsondiff     jsonschema       redis-check-aof  redis-sentinel
6380.log  chardetect      cloud-init-per  dump6381.rdb  easy_install-3.6  jsonpatch    kconfig          redis-check-rdb  redis-server
6381.log  cloud-id        dump6379.rdb    dump.rdb      easy_install-3.8  jsonpointer  redis-benchmark  redis-cli
[root@iZwz9cj1ytrolpgw50tiksZ bin]# redis-sentinel kconfig/sentinel.conf # 启动哨兵

在这里插入图片描述

此时关闭79服务器,过一会我们可以发现80或者81其中一个会自动变成主机。就算后面79服务器回来了,也只能当从机,乖乖地连接到重新投票选举出来的主机上。

在这里插入图片描述

哨兵进程的内容

在这里插入图片描述

哨兵模式的优点:

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

哨兵模式的缺点:

  • Redis不容易在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦

  • 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

  • 哨兵模式的全部配置非常地多

    # Example sentinel.conf
    # 哨兵sentinel实例运行的端口 默认26379
    port 26379
    
    # 哨兵sentinel的工作目录
    dir /tmp
    
    # 哨兵sentinel监控的redis主节点的 ip port
    # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
    # quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
    # sentinel monitor <master-name> <ip> <redis-port> <quorum>
    sentinel monitor mymaster 127.0.0.1 6379 2
    
    # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
    # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
    # sentinel auth-pass <master-name> <password>
    sentinel auth-pass mymaster MySUPER--secret-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无法正常启动成功。
    
    #通知脚本
    # shell编程
    # sentinel notification-script <master-name> <script-path>
    sentinel notification-script mymaster /var/redis/notify.sh
    
    # 客户端重新配置主节点参数脚本
    # 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
    # 以下参数将会在调用脚本时传给脚本:
    # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
    # 目前<state>总是“failover”,
    # <role>是“leader”或者“observer”中的一个。
    # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
    # 这个脚本应该是通用的,能被多次调用,不是针对性的。
    # sentinel client-reconfig-script <master-name> <script-path>
    sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置!
    

社会目前是程序员(初级和中级)饱和!高级程序员是重金难求的!(努力提升自己)

Redis的缓存穿透、雪崩、击穿(面试高频,工作常用)

在这里插入图片描述

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

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

缓存穿透(缓存查不到)

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

缓存穿透的解决方案:

  • 布隆过滤器

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

    在这里插入图片描述

  • 缓存空对象

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

    在这里插入图片描述

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

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

缓存击穿(缓存集中查询量太大,缓存过期)

概述:这里需要注意和缓存穿透的区别,缓存击穿只要指的是一个key非常热点,在不停地扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大

缓存击穿的解决方案:

  • 设置热点数据永不过期

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

  • 加互斥锁

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

缓存雪崩(缓存集中过期)

概念:缓存雪崩是指,在某一个时间段,缓存集中过期失效

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

缓存雪崩的解决方案:

  • redis高可用

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

  • 限流降级

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

  • 数据预热

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值