菜鸟的redis学习总结

java 专栏收录该内容
25 篇文章 12 订阅

说明

更新时间:2020/6/27 16:26,更新了五大数据库类型的底层原理
更新时间:2020/6/27 12:37,更新了缓存雪崩和java实现发布订阅功能
更新时间:2020/6/26 22:30,更新了缓存穿透和缓存击穿
更新时间:2020/6/25 22:50,更新了redis单机多集群和哨兵模式
更新时间:2020/6/23 22:15,更新了redis持久化和redis发布订阅
本文主要整理了非关系型数据库redis的相关知识,本文会持续更新,不断地扩充

本文仅为记录学习轨迹,如有侵权,联系删除

一、Nosql和Mysql

mysql作为目前使用人数比较多的一种数据库属于关系型数据库,有关系型数据库就有非关系型数据库,也就是Nosql(Not Only Sql),在所有的非关系型数据库中,用的最多的就是redis

两者的区别
像平时我们所用到的MSSQL Server、Mysql等是关系型数据库,它们是建立在关系模型基础上的数据库,依靠表、字段等关系模型,结合集合代数等数学方法来处理数据。而非关系型数据库,Nosql,Not only sql,是以Key-Value形式进行存储的,用来解决文档方面数据的存储。

也就是说最直观的区别就是两者之前的数据存储方式不同,关系型数据库存储方式是用的表结构,通过表的一行一行的方式来存储数据,而像redis这种非关系型数据库,存储方式就比较简单粗暴,直接通过键值对的方式存储,它没有行、列的概念,集合就相当于“表”,文档就相当于“行”。下面给出一张在网上看到的一张图
在这里插入图片描述
一句话总结就是:MySQL是一个基于表格设计的关系数据库,而NoSQL本质上是非关系型的基于文档的设计

两者优缺点比较
(1)MySQL中创建数据库之前需要详细的数据库模型,而在NoSQL数据库类型的情况下不需要详细的建模。
(2)MySQL的严格模式限制并不容易扩展,而NoSQL可以通过动态模式特性轻松扩展。
(3)MySQL提供了大量的报告工具,可以帮助应用程序有效,而NoSQL数据库缺少用于分析和性能测试的报告工具。
(4)MySQL是一个关系数据库,其设计约束灵活性较低;而NoSQL本质上是非关系型的,与MySQL相比,它提供了更灵活的设计。
(5)MySQL中使用的标准语言是SQL;而NoSQL中缺乏标准的查询语言。
(6)Mysql在进行CURD操作时,会用到I/O操作,读写效率较慢,而redis数据是存储在缓存中的,读写效率自然比mysql高,尤其是在高并发的情况下。
在这里插入图片描述
在上面这张图中,个人觉得最重要的一点就是高并发时数据的读取,因为像redis数据可以存储在缓存中,缓存的读取速度快,能够大大的提高运行效率,redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value 数据库,但是保存时间有限。此外就是redis虽然是存储在缓存中,但也是可以做数据持久化的。

两者的应用场景
这两种数据库的优缺点就决定了它们的使用场景,mysql的使用场景就不多说了,可以说现在的开发基本离不开这种关系型数据,但是考虑到mysql的读写效率,如果是数据量较少的情况下,mysql够用了,但如果涉及到一些大数据的分析处理等,就必须借助redis的力量,可以说它们两个是相辅相成的,关系型数据库为主,非关系型数据库为辅。下面给出redis的常见的使用常景

使用场景说明
缓存缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。
排行榜很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
计数器什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
分布式会话集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
分布式锁在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
社交网络点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
最新列表Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
消息系统消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

上面的这些redis使用场景是整理的网络上的,有些自己也不是完全懂,但是自己想了一下,redis数据既然存在内存中,所以用来做缓存的话是最佳的选择,然后像排行榜,点赞关注等功能,则是因为redis的数据存储类型,所以用来做这些相关的功能会很方便,同时效率也高,但是有一个场景是不建议用redis的,像是经常改动的数据,这种频繁变动的数据就不适合用redis,弄不好可能造成数据的丢失。

总结如下
(mysql)关系型数据库适合存储结构化数据,如用户的帐号、地址等
(1)这些数据通常需要做结构化查询,比如join,这时候,关系型数据库就要胜出一筹
(2)这些数据的规模、增长的速度通常是可以预期的
(3)保证数据的事务性、一致性要求。

(redis)NoSQL适合存储非结构化数据,如发微博、文章、评论
(1)这些数据通常用于模糊处理,如全文搜索、机器学习
(2)这些数据是海量的,而且增长的速度是难以预期的,
(3)根据数据的特点,NoSQL数据库通常具有无限(至少接近)伸缩性
(4)按key获取数据效率很高,但是对join或其他结构化查询的支持就比较差

目前许多大型互联网项目都会选用MySQL(或任何关系型数据库) + NoSQL的组合方案。

二、Nosql常见类型及比较

这里整理了一下网上常见的4种Nosql类型:列式、文档、图形和内存键值。
在这里插入图片描述
对于这4种类型的Nosql,之前在网上看到一张图
在这里插入图片描述
redis就是属于内存键值的Nosql类型。

三、简介

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets)有序集合(sorted sets)等类型,是当下热门的 NoSQL 技术之一!

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

特性
(1)多样的数据类型
(2)持久化
(3)集群
(4)事务
(5)…

五大基本数据类型
(1)String
(2)List
(3)Set
(4)Hash
(5)Zset

三种特殊数据类型
(1)geo
(2)hyperloglog
(3)bitmap

四、入门系列

(1)性能测试

按照官方的介绍,redis的性能是非常高的,每秒可以处理超过 10 万次读写操作。对与redis的性能,我们可以用它自带的工具redis-benchmark进行性能测试,测试的方式也简单,具体如下

redis-benchmark 相关命令参数

参数如下表
在这里插入图片描述

测试案例(测试100个并发数,每个请求数10万)
命令:redis-benchmark -h localhost -p 6379 -c 100 -n 100000

用进入redis的安装目录,可以看redis-benchmark测试工具
在这里插入图片描述
先开启redis的服务,用命令提示符的方式进入该目录,并输入上面的测试的命令
在这里插入图片描述

(2)String类型

基本的使用

redis 127.0.0.1:6379> keys *     #获取所有的key
(empty list or set)
redis 127.0.0.1:6379> set name Tony  #设置值
OK
redis 127.0.0.1:6379> keys *
1) "name"
redis 127.0.0.1:6379> get name  #获取值
"Tony"
redis 127.0.0.1:6379> exists name  #判断某一个key是否存在,存在就返回1,否则就返回0
(integer) 1
redis 127.0.0.1:6379> exists name1
(integer) 0
redis 127.0.0.1:6379> append name " is boy" #往一个key追加值,追加字符串
(integer) 11
redis 127.0.0.1:6379> get name   
"Tony is boy"
redis 127.0.0.1:6379> strlen name  #获取一个key对应值的字符串长度
(integer) 11
redis 127.0.0.1:6379> get name
"Tony is boy"
redis 127.0.0.1:6379> getrange name 0 2  #截取key对应的值范围,下标范围[0,2]
"Ton"
redis 127.0.0.1:6379> getrange name 0 -1  #截取key对应的值范围,下标范围[0,-1]-1表示截取到最后一个字符串
"Tony is boy"
redis 127.0.0.1:6379> setrange name 8 student   #替换指定位置开始的字符串! 
(integer) 15
redis 127.0.0.1:6379> get name
"Tony is student"
redis 127.0.0.1:6379> setex k1 10  v1  #设置k1 的值为 v1,10秒后过期 
OK
redis 127.0.0.1:6379> ttl k1  #查看key还有多久过期
(integer) 4
redis 127.0.0.1:6379> get k1
"v1"
redis 127.0.0.1:6379> get k1
(nil)
redis 127.0.0.1:6379> setnx mykey "redis"   #如果mykey 不存在,则创建mykey 
(integer) 1
redis 127.0.0.1:6379> keys *
1) "mykey"
2) "username"
redis 127.0.0.1:6379> setnx mykey "mysql"  #如果mykey存在则创建失败
(integer) 0
redis 127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3  #批量创建键值对
OK
redis 127.0.0.1:6379> keys *
1) "mykey"
2) "username"
3) "k2"
4) "k3"
5) "k1"
redis 127.0.0.1:6379> mget k1 k2 j3  #批量获取键值对
1) "v1"
2) "v2"
3) (nil)
redis 127.0.0.1:6379> msetnx k1 v1 k4 v4  # msetnx 是一个原子性的操作,要么一起成功,要么一起 失败! 
(integer) 0

注意区分创建值的其中两种方式,set和setnx
(1)set创建值时,如果key不存在就创建值,如果存在则会覆盖掉之前的值
(2)setnx创建值时,如果key不存在就会创建值,如果存在就创建失败

对象操作
在用java进行开发的时候,对象是接触得最多的,对象经常用来封装数据,然后存储,在redis里面存储对象,有两种方式,一种是把对象转成json,再用String类型存储;另一种是用user:{id}:{filed} 的方式存储,这是redis支持的语法

假设要存储一个对象user

public class user{
	private Integer id;
	private String username;
	private Integer age;
	private String password;
}

jsond的方式存储

redis 127.0.0.1:6379> set user:2 {username:zs,age:19,password:123}
OK
redis 127.0.0.1:6379> get user:2
"{username:zs,age:19,password:123}"

user:{id}:{filed} 的方式存储
存储:mset user : {id} : username 值 user : {id} : age 值 user : {id} : password 值
获取:mget user : {id} : username user : {id} :age user : {id} : password

redis 127.0.0.1:6379> mset user:1:username Mike user:1:age 18 user:1:password 123
OK
redis 127.0.0.1:6379> mget user:1:username  user:1:age  user:1:password
1) "Mike"
2) "18"
3) "123"

高级使用,统计浏览量

redis 127.0.0.1:6379> set views 0   #初始化浏览量为0
OK
redis 127.0.0.1:6379> keys *
1) "name"
2) "views"
redis 127.0.0.1:6379> incr views  #浏览量自增1
(integer) 1
redis 127.0.0.1:6379> incr views
(integer) 2
redis 127.0.0.1:6379> get views
"2"
redis 127.0.0.1:6379> incr views
(integer) 3
redis 127.0.0.1:6379> get views
"3"
redis 127.0.0.1:6379> decr views  #浏览量自减1
(integer) 2
redis 127.0.0.1:6379> get views
"2"
redis 127.0.0.1:6379> incrby views 10  #浏览量增加10
(integer) 12
redis 127.0.0.1:6379> get views
"12"
redis 127.0.0.1:6379> decrby views 5 #浏览量减少5
(integer) 7
redis 127.0.0.1:6379> get views
"7"
redis 127.0.0.1:6379>

(3)List类型

在redis里面,list类型可以呗当成 栈、队列、阻塞队列等结构来使用

基本使用

redis 127.0.0.1:6379> lpush list1 one    # 将一个值或者多个值,插入到列表头部 (左)
(integer) 1
redis 127.0.0.1:6379> lpush list1 two
(integer) 2
redis 127.0.0.1:6379> lpush list1 three
(integer) 3
redis 127.0.0.1:6379> lrange list1 0 -1   #通过区间的方式获取list1的值
1) "three"
2) "two"
3) "one"
redis 127.0.0.1:6379> rpush list2 one   # 将一个值或者多个值,插入到列表位部 (右) 
(integer) 1
redis 127.0.0.1:6379> rpush list2 two
(integer) 2
redis 127.0.0.1:6379> rpush list2 three
(integer) 3
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
redis 127.0.0.1:6379> lpop list2  #移除最左边的元素
"one"
redis 127.0.0.1:6379> rpop list2 #移除最右边的元素
"three"
redis 127.0.0.1:6379> lrange list2 0 -1
1) "two"
redis 127.0.0.1:6379> lpush list2 one
(integer) 2
redis 127.0.0.1:6379> rpush list2 three
(integer) 3
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
redis 127.0.0.1:6379> lindex list2 0
"one"
redis 127.0.0.1:6379> lindex list2 2
"three"
redis 127.0.0.1:6379> llen list2   #获取列表的元素个数
(integer) 3
redis 127.0.0.1:6379> rpush list2 three
(integer) 4
redis 127.0.0.1:6379> rpush list2 three
(integer) 5
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
4) "three"
5) "three"
redis 127.0.0.1:6379> lrem list2 3 three  #移除3个值等于three的元素
(integer) 3
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
redis 127.0.0.1:6379> rpush list2 three
(integer) 3
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
redis 127.0.0.1:6379> rpush list2 four
(integer) 4
redis 127.0.0.1:6379> rpush list2 five
(integer) 5
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
5) "five"
redis 127.0.0.1:6379> ltrim list2 1 3  # 通过下标截取指定的长度,这个list已经被改变了,截断了 只剩下截取的元素! 
OK
redis 127.0.0.1:6379> lrange list2 0 -1
1) "two"
2) "three"
3) "four"
redis 127.0.0.1:6379> lpush list2 one
(integer) 4
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
redis 127.0.0.1:6379> rpoplpush list2 list3   # 移除列表的后一个元素,将他移动到新的列表中!

"four"
redis 127.0.0.1:6379> lrange list3 0 -1
1) "four"
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
redis 127.0.0.1:6379> exists list2    #判断列表是否存在
(integer) 1
redis 127.0.0.1:6379> lset list2 1 number2   #将列表中指定下标的值替换为另外一个值,更新操作
OK
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "number2"
3) "three"
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "number2"
3) "three"
redis 127.0.0.1:6379> lset list2 1 two
OK
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "three"
redis 127.0.0.1:6379> linsert list2 before three number3 # 将某个具体的value插入到列把你中某个元素的前面

(integer) 4
redis 127.0.0.1:6379> linsert list2 after three four    # 将某个具体的value插入到列把你中某个元素的后面!

(integer) 5
redis 127.0.0.1:6379> lrange list2 0 -1
1) "one"
2) "two"
3) "number3"
4) "three"
5) "four"

注意:list类型的值是可以重复的
小结
(1)他实际上是一个链表,before Node after , left,right 都可以插入值
(2)如果key 不存在,创建新的链表
(3)如果key存在,新增内容
(4)如果移除了所有值,空链表,也代表不存在!
(5)在两边插入或者改动值,效率高! 中间元素,相对来说效率会低一点~

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

(4)Set集合

#######################################################################
127.0.0.1:6379> sadd myset "hello"   # set集合中添加匀速 
(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     # 查看指定set的所有值 
1) "hello" 
2) "lovekuangshen" 
3) "kuangshen" 
127.0.0.1:6379> SISMEMBER myset hello    # 判断某一个值是不是在set集合中! 
(integer) 1 
127.0.0.1:6379> SISMEMBER myset world 
(integer) 0
#######################################################################
127.0.0.1:6379> scard myset  # 获取set集合中的内容元素个数! 
(integer) 4
#######################################################################
127.0.0.1:6379> srem myset hello  # 移除set集合中的指定元素 
(integer) 
1 127.0.0.1:6379> scard myset 
(integer) 3 
127.0.0.1:6379> SMEMBERS myset 
1) "lovekuangshen2" 
2) "lovekuangshen" 
3) "kuangshen"
#######################################################################
# set 无序不重复集合。抽随机!
127.0.0.1:6379> SMEMBERS myset 
1) "lovekuangshen2" 
2) "lovekuangshen" 
3) "kuangshen" 
127.0.0.1:6379> SRANDMEMBER myset  # 随机抽选出一个元素 
"kuangshen" 
127.0.0.1:6379> SRANDMEMBER myset 
"kuangshen" 
127.0.0.1:6379> SRANDMEMBER myset 
"kuangshen" 
127.0.0.1:6379> SRANDMEMBER myset 
"kuangshen" 
127.0.0.1:6379> SRANDMEMBER myset 2  # 随机抽选出指定个数的元素 
1) "lovekuangshen" 
2) "lovekuangshen2" 
127.0.0.1:6379> SRANDMEMBER myset 2 
1) "lovekuangshen" 
2) "lovekuangshen2" 
127.0.0.1:6379> SRANDMEMBER myset      # 随机抽选出一个元素 
"lovekuangshen2"
#######################################################################
# 删除定的key,随机删除key!
127.0.0.1:6379> SMEMBERS myset 
1) "lovekuangshen2" 
2) "lovekuangshen" 
3) "kuangshen" 
127.0.0.1:6379> spop myset  # 随机删除一些set集合中的元素! 
"lovekuangshen2" 
127.0.0.1:6379> spop myset 
"lovekuangshen" 
127.0.0.1:6379> SMEMBERS myset 
1) "kuangshen"
#######################################################################
# 将一个指定的值,移动到另外一个set集合! 
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" # 将一个指定的值,移动到另外一个set集 合! 
(integer) 1 
127.0.0.1:6379> SMEMBERS myset 
1) "world" 
2) "hello" 
127.0.0.1:6379> SMEMBERS myset2 
1) "kuangshen" 
2) "set2"

高级应用,求共同好友
微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!
共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)

redis 127.0.0.1:6379> smembers k1
1) "a"
2) "d"
3) "b"
4) "c"
redis 127.0.0.1:6379> smembers k2
1) "f"
2) "d"
3) "e"
4) "c"
redis 127.0.0.1:6379> sdiff k1 k2    # 差集 
1) "a"
2) "b"
redis 127.0.0.1:6379> sinter k1 k2   # 交集   共同好友就可以这样实现 
1) "d"
2) "c"
redis 127.0.0.1:6379> sunion k1 k2  # 并集 
1) "a"
2) "f"
3) "b"
4) "c"
5) "e"
6) "d"

(5)Hash类型

Map集合,key-map! 时候这个值是一个map集合! 本质和String类型没有太大区别,还是一个简单的 key-vlaue!

基本应用

########################################################################## 
127.0.0.1:6379> hset myhash field1 kuangshen  # set一个具体 key-vlaue 
(integer) 1 
127.0.0.1:6379> hget myhash field1  # 获取一个字段值 
"kuangshen" 
127.0.0.1:6379> hmset myhash field1 hello field2 world   # set多个 key-vlaue 
OK 
127.0.0.1:6379> hmget myhash field1 field2   # 获取多个字段值 
1) "hello" 
2) "world" 
127.0.0.1:6379> hgetall myhash   # 获取全部的数据, 
1) "field1" 
2) "hello" 
3) "field2" 
4) "world" 
127.0.0.1:6379> hdel myhash field1  # 删除hash指定key字段!对应的value值也就消失了! 
(integer) 1 127.0.0.1:6379> hgetall myhash 
1) "field2" 
2) "world" #######################################################################
### hlen
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> HEXISTS myhash field1  # 判断hash中指定字段是否存在!
 (integer) 1
127.0.0.1:6379> HEXISTS myhash field3 
(integer) 0
#######################################################################
### # 只获得所有field # 只获得所有value 
127.0.0.1:6379> hkeys myhash  # 只获得所有field 
1) "field2" 
2) "field1"
127.0.0.1:6379> hvals myhash  # 只获得所有value 
1) "world" 
2) "hello" #######################################################################
### incr   decr
127.0.0.1:6379> hset myhash field3 5    #指定增量! 
(integer) 1 
127.0.0.1:6379> HINCRBY myhash field3 1 
(integer) 6 
127.0.0.1:6379> HINCRBY myhash field3 -1 
(integer) 5 
127.0.0.1:6379> hsetnx myhash field4 hello  # 如果不存在则可以设置 
(integer) 1 
127.0.0.1:6379> hsetnx myhash field4 world  # 如果存在则不能设置 
(integer) 0

(6)Zset有序集合

在set的基础上,增加了一个值score,在做排序等操作时需要用到这个score值:zset k1 score1 v1

基本操作

redis 127.0.0.1:6379> zadd salary 8000 xm  #添加用户xm,工资8000
(integer) 1
redis 127.0.0.1:6379> zadd salary 9000 xh
(integer) 1
redis 127.0.0.1:6379> zadd salary 10000 zs
(integer) 1
redis 127.0.0.1:6379> zadd salary 8800 ls
(integer) 1
redis 127.0.0.1:6379> zrangebyscore salary -inf +inf  # 显示全部的用户 从小到大! 
1) "xm"
2) "ls"
3) "xh"
4) "zs"
redis 127.0.0.1:6379> zrevrange salary 0 -1   # 显示全部的用户 从大到小! 
1) "zs"
2) "xh"
3) "ls"
4) "xm"
redis 127.0.0.1:6379> zrangebyscore salary -inf +inf withscores  # 显示全部的用户从小到大并且附带成 绩
1) "xm"
2) "8000"
3) "ls"
4) "8800"
5) "xh"
6) "9000"
7) "zs"
8) "10000"
redis 127.0.0.1:6379> zrangebyscore salary -inf 9000 withscores  # 显示工资小于9000员工的升 序排序! 
1) "xm"
2) "8000"
3) "ls"
4) "8800"
5) "xh"
6) "9000"
redis 127.0.0.1:6379> zrange salary 0 -1  #查询所有用户
1) "xm"
2) "ls"
3) "xh"
4) "zs"
redis 127.0.0.1:6379> zrem salary ls  #移除用户
(integer) 1
redis 127.0.0.1:6379> zcard salary   # 获取有序集合中的个数 
(integer) 3
redis 127.0.0.1:6379> zadd myzset 1 hello 2 world 3 nice  #批量增加用户
(integer) 3
redis 127.0.0.1:6379> zcount myzset 1 2  # 获取指定区间的成员数量! 
(integer) 2

(7)Geospatial 地理位置

朋友的定位,附近的人,打车距离计算?这些功能都可以用reids来实现,Redis 的 Geo 在Redis3.2 版本就推出了! 这个功能可以推算地理位置的信息,两地之间的距离,方圆 几里的人!

常见命令

GEOHASH:该命令将返回11个字符的Geohash字符串!
GEOPOS:获得当前定位:一定是一个坐标值!
GEODIST:获取两个城市之间的直线距离
GEORADIUS:以给定的经纬度为中心, 找出某一半径内的元素
GEOADD:添加key的经纬度坐标及名称
GEORADIUSBYMEMBER:找出位于指定元素周围的其他元素

GEOADD
添加key的经纬度坐标及名称:GEOADD key 经度 纬度 名称

#添加中国几个城市的经纬度坐标
127.0.0.1:6379>  geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 113.38 22.52 zhongshan
(integer) 1
127.0.0.1:6379> geoadd china:city 113.28 23.12 guangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 114.08 22.54 shengzhen
(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 108.94 34.26 xianshi
(integer) 1

GEOPOS
获得当前定位,一定是一个坐标值:GEOPOS key 名称

#获取上海和北京的定位,前提是这些城市要提前添加到相应的key(china:city)里面
127.0.0.1:6379> GEOPOS china:city shanghai
1) 1) "121.47000163793564"
   2) "31.229999039757836"
127.0.0.1:6379> GEOPOS china:city beijing
1) 1) "116.39999896287918"
   2) "39.900000091670925"

GEODIST
获取两个城市之间的直线距离:GEODIST key 城市1 城市2 单位
m 表示单位为米。 km 表示单位为千米。 mi 表示单位为英里。 ft 表示单位为英尺

#获取中山到广州的距离,单位是km
127.0.0.1:6379> GEODIST china:city zhongshan guangzhou km
"67.5185"
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1067.3788"

GEORADIUS
以给定的经纬度为中心, 找出某一半径内的元素:GEORADIUS key 经度 维度 半径 单位

127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km
1) "xianshi"
2) "zhongshan"
3) "shengzhen"
4) "guangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1) "xianshi"
127.0.0.1:6379>

GEORADIUSBYMEMBER
找出位于指定元素周围的其他元素! :GEORADIUSBYMEMBER key 城市 距离 单位

127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km
1) "beijing"
2) "xianshi"

GEOHASH
将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!

# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近! 
127.0.0.1:6379> geohash china:city beijing chongqi 
1) "wx4fbxxfke0" 
2) "wm5xzrybty0"

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

127.0.0.1:6379> zrange china:city 0 -1  #获取所有的值
1) "xianshi"
2) "zhongshan"
3) "shengzhen"
4) "guangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city xianshi  #移除值
(integer) 1
127.0.0.1:6379> zcard china:city  #查看值的个数
(integer) 5
127.0.0.1:6379>

(8)Hyperloglo基数技术

基数
在数学上,基数(cardinal number)也叫势(cardinality),指集合论中刻画任意集合所含元素数量多少的一个概念。比如:集合A={1,2,3,4,5,5,5,6},集合B={3,4,5,6,7},基数(两个集合不重复的元素) = 7

优点
占用的内存是固定,2^64 不同的元素的技术,只需要花费 12KB内存!如果要从内存角度来比较的 话 Hyperloglog 首选!

使用场景
网页的 UV (一个人访问一个网站多次,但是还是算作一个人!),传统的方式, set 保存用户的id,然后就可以统计 set 中的元素数量作为标准判断 ! 这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id;根据官方给的数据,Hyperloglo会有0.81% 错误率! 统计UV任务,可以忽略不计的!

127.0.0.1:6379> PFADD H1 A B C  # # 创建第一组元素 H1
(integer) 1
127.0.0.1:6379> PFADD H2 C B A A D E G
(integer) 1
127.0.0.1:6379> PFADD H3 Q W E R
(integer) 1
127.0.0.1:6379> PFCOUNT H1 # 统计 H1元素的基数数量 
(integer) 3
127.0.0.1:6379> PFMERGE H H1 H2 H3  # 合并三组 H1 H2 H3 => H 并集 
OK
127.0.0.1:6379> PFCOUNT H
(integer) 9

(9)Bitmap位存储

Bitmap存储的数据只有0和1,利用0和1可以用来做一些状态的记录,前提是这种状态只有两种情况

应用场景
统计用户信息,活跃,不活跃! 登录 、 未登录! 打卡,365天打卡! 两个状态的,都可以使用 Bitmaps!Bitmap 位图,数据结构! 都是操作二进制位来进行记录,就只有0 和 1 两个状态! 365 天 = 365 bit 1字节 = 8bit 46 个字节左右

应用场景一:一周的打卡记录
0到6表示周一到周天,打卡状态由0和1记录,0代表缺勤,1代表已打卡

127.0.0.1:6379> setbit sign 0 1  #设置打卡状态
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 1
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
127.0.0.1:6379> getbit sign 0  #查看某一天的打卡状态
(integer) 1
127.0.0.1:6379> getbit sign 4
(integer) 0
127.0.0.1:6379> bitcount sign  #统计这一周的打卡情况
(integer) 5

(10)事务

Redis事务的概念
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务没有隔离级别的概念
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。

Redis不保证原子性
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的三个阶段
(1)开始事务(multi)
(2)命令入队
(3)执行事务(exec)

正常执行事务

127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set k1 v1  #执行的命令都不会立刻执行,只会先放入队列
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec   #执行事务,只有执行了exec命令后,队列里面的命令才会执行
1) OK
2) OK
3) 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 k3 v3
QUEUED
127.0.0.1:6379> getsdfsafsaf   #随便输入不存在的命令,遇到编译时异常
(error) ERR unknown command 'getsdfsafsaf'
127.0.0.1:6379> set k4 v4    #再设置命令
QUEUED
127.0.0.1:6379> exec   #执行事务,发现会失败,事务中所有的命令都不会被执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1   
(nil)
127.0.0.1:6379>

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

127.0.0.1:6379> multi    #开启事务
OK
127.0.0.1:6379> set k1 "hello"  #执行的命令都不会立刻执行,只会先放入队列
QUEUED
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> exec   #执行事务,发现除了那一条异常的命令,其余都执行成功了
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) OK
127.0.0.1:6379> keys *
1) "H2"
2) "k3"
3) "k1"
4) "sign"
5) "k2"
6) "china:city"
7) "H1"
8) "H"
9) "H3"
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> discard   #放弃事务
OK
127.0.0.1:6379> keys *
1) "H2"
2) "sign"
3) "china:city"
4) "H1"
5) "H"
6) "H3"
127.0.0.1:6379>

(11)watch监控实现乐观锁

悲观锁:顾名思义就是很“悲观”,无论执行什么操作都觉得会出问题,所以在执行任何操作的时候都会加上锁,这样就会影响性能
乐观锁:顾名思义就是很“乐观”,认为什么时候都不会出问题,所以不会上锁! 在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

redis利用watch的监控功能可以实现乐观锁,正常实现乐观锁的流程就是利用版本号比较机制,只是在读数据的时候,将读到的数据的版本号一起读出来,当对数据的操作结束后,准备写数据的时候,再进行一次数据版本号的比较,若版本号没有变化,即认为数据是一致的,没有更改,可以直接写入,若版本号有变化,则认为数据被更新,不能写入,防止脏写

watch监控测试

127.0.0.1:6379> set money 1000   #假设现有1000块钱
OK
127.0.0.1:6379> set out 0   #消费了0块钱
OK
127.0.0.1:6379> watch money   #开启监控,会监控money值的变化
OK
127.0.0.1:6379> multi   #开启事务
OK
127.0.0.1:6379> decrby money 100   #花费了100块钱,剩下900
QUEUED
127.0.0.1:6379> incrby out 100   #消费余额100块钱
QUEUED
127.0.0.1:6379> exec    #执行事务,单线程情况下,watch没有监控到其余线程对money进行操作,正常执行
1) (integer) 900
2) (integer) 100
127.0.0.1:6379>

多线程修改值 , 利用watch 实现redis的乐观锁操作
上面的例子由于是单线程的,watch没有监控到其余线程对money进行操作,所以正常执行,如果watch监控到有其余线程已经对money进行了修改,则会执行失败,这就是redis利用watch的监控功能实现乐观锁操作的原理

在这里插入图片描述

(12)通过Jedis操作redis

简单说一下Jedis,Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码。

String类型

    public static void main(String[] args) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);

        /**设置值*/
        jedis.set("k1", "v1");//设置一个值
        jedis.mset("k2","v2","k3","v3","k4","v4","k5","v5");//设置多个值
        jedis.append("k1","hello");//再k1后面再追加值“hello”
        jedis.setnx("k6","v6");//注意与set的区别,setnx表示如果不存在才创建
        jedis.setnx("k5","v5");//setnx如果存在就创建失败,set的话则会覆盖
        jedis.setex("k7",10,"v7");//设置一个值,10秒后过期


        /**获取值*/
        String k1 = jedis.get("k1");//获取一个值
        List<String> mgetlist = jedis.mget("k2", "k3", "k4","k5","k6","k7");//获取多个值
        System.out.println("k1 = "+k1);
        for (int i = 0; i <mgetlist.size(); i++) {
            System.out.println("k"+(i+2)+" = "+mgetlist.get(i));
        }



        /**删除值*/
        jedis.del("k1");//删除单个值
        jedis.del("k2","k3","k4","k5","k6","k7");//删除多个值

        jedis.close();//关闭连接
    }

List类型

    public static void main(String[] args) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);

        /**设置值*/
        jedis.lpush("list1","java");//设置一个值,左边插入
        jedis.lpush("list1","c","c++","python");//设置多个值,左边插入
        jedis.rpush("list1","php");//设置一个值,右边插入
        jedis.rpush("list1","c#","js","jq");//设置多个值,右边插入



        /**获取值*/
        List<String> list1 = jedis.lrange("list1", 0, -1);//通过区间的方式获取list1的值
        String str = jedis.lindex("list1", 0);//获取指定下标的元素
        Long len = jedis.llen("list1");//获取key的所有元素的长度
        for (int i = 0; i < list1.size(); i++) {
            System.out.println("list1["+i+"] = "+list1.get(i));
        }
        System.out.println("list1[0] = "+str);
        System.out.println("list1长度 = "+len);

        /**修改值*/
        jedis.lset("list1",1,"new Value");//修改某一个key对应下标的值


        /**删除值*/
        jedis.lrem("list1",1,"c");//删除一个list1中名为”c“的值,如果存在重复元素,可以删除多个
        jedis.ltrim("list1",0,2);//截取下标为0-2的元素,其余的删掉
        jedis.lpop("list1");//出栈,左边
        jedis.rpop("list1");//出栈,右边

        jedis.close();

    }

Set类型

    public static void main(String[] args) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);

        /**设置值*/
        jedis.sadd("set1","a","b","c","c");//设置多个值
        jedis.sadd("set2","a","c","e","u","q");

        /**获取值*/
        jedis.smembers("set1");//获取所有的值
        jedis.scard("set1");//获取元素中包含的个数
        jedis.sismember("set1","a");//判断set1元素中是否包含有a这个元素


        /**删除值*/
        jedis.srem("set1","a");//删除值”a“
        jedis.srem("list1","b","c");//删除多个值



        /**集合运算*/
        jedis.sinter("set1","set2");//set1与set2交集
        jedis.sunion("set1","set2");//set1与set2并集
        jedis.sdiff("set1","set2");//set1与set2差集
        jedis.sinterstore("set3","set1","set2");//求set1与set2交集并赋值给set3


        jedis.close();

    }

Hash类型

    public static void main(String[] args) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);

        Map<String,String> map = new HashMap<>();
        map.put("k1","v1");
        map.put("k2","v2");
        map.put("k3","v3");

        /**添加值*/
        jedis.hmset("hash1",map);//添加map
        jedis.hset("hash1","k4","v5");//向hash1中添加key为k5,value为v5的元素


        /**获取值*/
        Map<String, String> map1 = jedis.hgetAll("hash1");//获取hash1的所有的键值对
        Set<String> keys = jedis.hkeys("hash1");//hash1的所有键
        List<String> values = jedis.hvals("hash1");//hash1的所有值
        List<String> hash1 = jedis.hmget("hash1", "k1");//获取hash1中的值
        Long len = jedis.hlen("hash1");//hash1的长度
        Boolean hexists = jedis.hexists("hash1", "k8");//判断hash1中是否含有k8


        /**删除值*/
        jedis.hdel("hash1","k1","k2","k3");

        jedis.close();
    }

Zset类型

    public static void main(String[] args) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);

        /**添加值*/
        jedis.zadd("salary",8000,"xm");
        jedis.zadd("salary",10000,"zs");
        jedis.zadd("salary",9000,"ls");
        jedis.zadd("salary",9800,"xh");


        /**获取值*/
        Set<String> salary1 = jedis.zrangeByScore("salary",0 , 10001);//将salary从小到大排序
        Set<String> salary2 = jedis.zrevrange("salary", 0, -1);//倒序输出,因为上面已经排过序了,所以倒序就是从大到小输出!
        Set<String> salary3 = jedis.zrange("salary", 0, -1);//顺序输出,已经排过序了
        Long len = jedis.zcard("salary");//集合的元素个数
        System.out.println("从小到大输出");
        salary1.forEach(System.out::println);
        System.out.println("从大到小输出");
        salary2.forEach(System.out::println);
        System.out.println("顺序输出");
        salary3.forEach(System.out::println);
        System.out.println("长度");
        System.out.println(len);

        /**删除值*/
        jedis.zrem("salary","ls");//移除用户

        jedis.close();
    }

事务(重点)

    public static void main(String[] args) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","zs");
        jsonObject.put("age","12");
        jsonObject.put("sex","雄");
        String user = jsonObject.toString();


        //开启事务
        Transaction multi = jedis.multi();

        try{
            multi.set("user1",user);
            int i = 1/0;//遇到运行时异常
            multi.set("user2",user);
            multi.exec();//执行事务
        }catch (Exception e){
            multi.discard();//放弃事务
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();
        }
        
    }

运行结果
在这里插入图片描述

(13)springboot整合redis

在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce? jedis采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接 池! 更像 BIO 模式, lettuce采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据 了,更像 NIO 模式。

说明:关于lettuce本人只是简单了解了一下,也不是很懂

(1)源码分析

在这里插入图片描述
我这边的springboot版本是2.2.6,从上面的图可以看到springboot2.x用的redis的java客户端是lettuce

redis自动配置类(autoconfiguration)
在这里插入图片描述
对应的配置文件RedisProperties,里面的参数就是我们可以配置的
在这里插入图片描述

(2)使用配置好模板RedisTemplate

获取RedisTemplate的bean对象,之后就可以直接像调用redis那样直接调用接口,api就跟正常操作redis的命令一样
在这里插入图片描述
使用上面配置好的RedisTemplate模板来操作redis,但是会出现一个问题,那就是序列化的问题,比如下面的例子,用该模板存储几个值,发现在idea里面可以正常获取,但是一打开redis-cil查看却发现,key前面多了很多转义字符
在这里插入图片描述
原因分析
在查询了一些资料之后,发现任何数据存进redis里面都必须先序列化,用哪种序列化方式可以自己设置,如果不设置的话,默认使用JdkSerializationRedisSerializer进行数据序列化。

所以很明显,那些转义字符都是JdkSerializationRedisSerializer进行序列化时,加上去的,此外还有其他序列化的方式,具体如下
(1)StringRedisSerializer
(2)Jackson2JsonRedisSerialize
(3)JdkSerializationRedisSerializer
(4)GenericToStringSerializer
(5)OxmSerializer

给出一张表,不同的序列化后的值前后对比
在这里插入图片描述
所以一般不会用这个默认的配置模板,而是会自己写一个模板,并且采用其他序列化的方式,这样就不会有上面的问题。

拓展
这让我想起一件事,就是之前在学习面向接口编程的时候,有一条规则,就是所有的实体类pojo创建完后必须实现序列化接口,如果我们不实现序列化接口,直接就将用户类储存进redis,他会报一个序列化错误的异常,如果实现了序列化接口,可以直接存redis

(3)使用自定义模板RedisTemplate

如果使用自定义的模板RedisTemplate,会出现序列化的问题,所以才需要自己写一个RedisTemplate来供自己操作redis

/**
 * redis配置类
 */
@Configuration
public class RedisConfig {

    /**redis配置模板,可直接copy使用**/
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
            throws UnknownHostException {

        //为了开发的方便,一般直接使用<String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);

        //json的序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        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;
    }
}

采用了string和json序列化的方式,成功解决上面的问题
在这里插入图片描述

(4)使用RedisUtil

自定义的模板看起来很好,但是还有一种方式更好,那就是将操作redis的命令封装成一个工具类,让我们能跟直接操作redis的api一样,这样在使用redis的java客户端时就能实现无缝切换,因为封装好的工具类的接口就跟直接操作redis的接口一样

package com.zsc.util;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

/**
 *
 * Redis工具类,一般企业开发常用的一个工具类,不会去用原生的redis配置类
 *
 */

@Component
public final class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================

    /**
     * 26
     * 指定缓存失效时间
     * 27
     *
     * @param key  键
     *             28
     * @param time 时间(秒)
     *             29
     * @return 30
     */

    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;

        }

    }

    /**
     * 44
     * 根据key 获取过期时间
     * 45
     *
     * @param key 键 不能为null
     *            46
     * @return 时间(秒) 返回0代表为永久有效
     * 47
     */

    public long getExpire(String key) {

        return redisTemplate.getExpire(key, TimeUnit.SECONDS);

    }

    /**
     * 53
     * 判断key是否存在
     * 54
     *
     * @param key 键
     *            55
     * @return true 存在 false不存在
     * 56
     */

    public boolean hasKey(String key) {

        try {

            return redisTemplate.hasKey(key);

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 67
     * 删除缓存
     * 68
     *
     * @param key 可以传一个值 或多个
     *            69
     */

    @SuppressWarnings("unchecked")

    public void del(String... key) {

        if (key != null && key.length > 0) {

            if (key.length == 1) {

                redisTemplate.delete(key[0]);

            } else {

                redisTemplate.delete(CollectionUtils.arrayToList(key));

            }

        }

    }

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

    /**
     * 83
     * 普通缓存获取
     * 84
     *
     * @param key 键
     *            85
     * @return 值
     * 86
     */

    public Object get(String key) {

        return key == null ? null : redisTemplate.opsForValue().get(key);

    }

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

    public boolean set(String key, Object value) {

        try {

            redisTemplate.opsForValue().set(key, value);

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

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

    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;

        }

    }

    /**
     * 130
     * 递增
     * 131
     *
     * @param key   键
     *              132
     * @param delta 要增加几(大于0)
     *              133
     * @return 134
     */

    public long incr(String key, long delta) {

        if (delta < 0) {

            throw new RuntimeException("递增因子必须大于0");

        }

        return redisTemplate.opsForValue().increment(key, delta);

    }

    /**
     * 143
     * 递减
     * 144
     *
     * @param key   键
     *              145
     * @param delta 要减少几(小于0)
     *              146
     * @return 147
     */

    public long decr(String key, long delta) {

        if (delta < 0) {

            throw new RuntimeException("递减因子必须大于0");

        }

        return redisTemplate.opsForValue().increment(key, -delta);

    }

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

    /**
     * 157
     * HashGet
     * 158
     *
     * @param key  键 不能为null
     *             159
     * @param item 项 不能为null
     *             160
     * @return 值
     * 161
     */

    public Object hget(String key, String item) {

        return redisTemplate.opsForHash().get(key, item);

    }

    /**
     * 167
     * 获取hashKey对应的所有键值
     * 168
     *
     * @param key 键
     *            169
     * @return 对应的多个键值
     * 170
     */

    public Map<Object, Object> hmget(String key) {

        return redisTemplate.opsForHash().entries(key);

    }

    /**
     * 176
     * HashSet
     * 177
     *
     * @param key 键
     *            178
     * @param map 对应多个键值
     *            179
     * @return true 成功 false 失败
     * 180
     */

    public boolean hmset(String key, Map<String, Object> map) {

        try {

            redisTemplate.opsForHash().putAll(key, map);

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 192
     * HashSet 并设置时间
     * 193
     *
     * @param key  键
     *             194
     * @param map  对应多个键值
     *             195
     * @param time 时间(秒)
     *             196
     * @return true成功 false失败
     * 197
     */

    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;

        }

    }

    /**
     * 212
     * 向一张hash表中放入数据,如果不存在将创建
     * 213
     *
     * @param key   键
     *              214
     * @param item  项
     *              215
     * @param value 值
     *              216
     * @return true 成功 false失败
     * 217
     */

    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;

        }

    }

    /**
     * 229
     * 向一张hash表中放入数据,如果不存在将创建
     * 230
     *
     * @param key   键
     *              231
     * @param item  项
     *              232
     * @param value 值
     *              233
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     *              234
     * @return true 成功 false失败
     * 235
     */

    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;

        }

    }

    /**
     * 250
     * 删除hash表中的值
     * 251
     *
     * @param key  键 不能为null
     *             252
     * @param item 项 可以使多个 不能为null
     *             253
     */

    public void hdel(String key, Object... item) {

        redisTemplate.opsForHash().delete(key, item);

    }

    /**
     * 259
     * 判断hash表中是否有该项的值
     * 260
     *
     * @param key  键 不能为null
     *             261
     * @param item 项 不能为null
     *             262
     * @return true 存在 false不存在
     * 263
     */

    public boolean hHasKey(String key, String item) {

        return redisTemplate.opsForHash().hasKey(key, item);

    }

    /**
     * 269
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * 270
     *
     * @param key  键
     *             271
     * @param item 项
     *             272
     * @param by   要增加几(大于0)
     *             273
     * @return 274
     */

    public double hincr(String key, String item, double by) {

        return redisTemplate.opsForHash().increment(key, item, by);

    }

    /**
     * 280
     * hash递减
     * 281
     *
     * @param key  键
     *             282
     * @param item 项
     *             283
     * @param by   要减少记(小于0)
     *             284
     * @return 285
     */

    public double hdecr(String key, String item, double by) {

        return redisTemplate.opsForHash().increment(key, item, -by);

    }

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

    /**
     * 292
     * 根据key获取Set中的所有值
     * 293
     *
     * @param key 键
     *            294
     * @return 295
     */

    public Set<Object> sGet(String key) {

        try {

            return redisTemplate.opsForSet().members(key);

        } catch (Exception e) {

            e.printStackTrace();

            return null;

        }

    }

    /**
     * 306
     * 根据value从一个set中查询,是否存在
     * 307
     *
     * @param key   键
     *              308
     * @param value 值
     *              309
     * @return true 存在 false不存在
     * 310
     */

    public boolean sHasKey(String key, Object value) {

        try {

            return redisTemplate.opsForSet().isMember(key, value);

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 321
     * 将数据放入set缓存
     * 322
     *
     * @param key    键
     *               323
     * @param values 值 可以是多个
     *               324
     * @return 成功个数
     * 325
     */

    public long sSet(String key, Object... values) {

        try {

            return redisTemplate.opsForSet().add(key, values);

        } catch (Exception e) {

            e.printStackTrace();

            return 0;

        }

    }

    /**
     * 336
     * 将set数据放入缓存
     * 337
     *
     * @param key    键
     *               338
     * @param time   时间(秒)
     *               339
     * @param values 值 可以是多个
     *               340
     * @return 成功个数
     * 341
     */

    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;

        }

    }

    /**
     * 355
     * 获取set缓存的长度
     * 356
     *
     * @param key 键
     *            357
     * @return 358
     */

    public long sGetSetSize(String key) {

        try {

            return redisTemplate.opsForSet().size(key);

        } catch (Exception e) {

            e.printStackTrace();

            return 0;

        }

    }

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

    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=================================

    /**
     * 386
     * 获取list缓存的内容
     * 387
     *
     * @param key   键
     *              388
     * @param start 开始
     *              389
     * @param end   结束 0 到 -1代表所有值
     *              390
     * @return 391
     */

    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;

        }

    }

    /**
     * 402
     * 获取list缓存的长度
     * 403
     *
     * @param key 键
     *            404
     * @return 405
     */

    public long lGetListSize(String key) {

        try {

            return redisTemplate.opsForList().size(key);

        } catch (Exception e) {

            e.printStackTrace();

            return 0;

        }

    }

    /**
     * 416
     * 通过索引 获取list中的值
     * 417
     *
     * @param key   键
     *              418
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     *              419
     * @return 420
     */

    public Object lGetIndex(String key, long index) {

        try {

            return redisTemplate.opsForList().index(key, index);

        } catch (Exception e) {

            e.printStackTrace();

            return null;

        }

    }

    /**
     * 431
     * 将list放入缓存
     * 432
     *
     * @param key   键
     *              433
     * @param value 值
     *              434
     * //@param time  时间(秒)
     *              435
     * @return 436
     */

    public boolean lSet(String key, Object value) {

        try {

            redisTemplate.opsForList().rightPush(key, value);

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */

    public boolean lSet(String key, Object value, long time) {

        try {

            redisTemplate.opsForList().rightPush(key, value);

            if (time > 0)

                expire(key, time);

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 467
     * 将list放入缓存
     * 468
     *
     * @param key   键
     *              469
     * @param value 值
     *              470
     * //@param time  时间(秒)
     *              471
     * @return 472
     */

    public boolean lSet(String key, List<Object> value) {

        try {

            redisTemplate.opsForList().rightPushAll(key, value);

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 484
     * 将list放入缓存
     * 485
     * <p>
     * 486
     *
     * @param key   键
     *              487
     * @param value 值
     *              488
     * @param time  时间(秒)
     *              489
     * @return 490
     */

    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;

        }

    }

    /**
     * 504
     * 根据索引修改list中的某条数据
     * 505
     *
     * @param key   键
     *              506
     * @param index 索引
     *              507
     * @param value 值
     *              508
     * @return 509
     */

    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;

        }

    }

    /**
     * 521
     * 移除N个值为value
     * 522
     *
     * @param key   键
     *              523
     * @param count 移除多少个
     *              524
     * @param value 值
     *              525
     * @return 移除的个数
     * 526
     */

    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;

        }

    }
}

使用的方式就跟直接操作redis一样在这里插入图片描述
真正实现了无缝切换,事实上这个也是使用最多的方式之一。

五、五大数据类型底层原理

注:关于这一部分(底层原理)的内容大部分来自这篇博文,里面的东西讲得挺好的,虽然里面有些东西过于深奥,自己也未能完全理解

(1)String类型

Redis 的字符串叫着「SDS」,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组,Redis没有直接使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的可以被修改的抽象类型,并将SDS用作Redis的默认字符串表示。

在这里插入图片描述
结构定义如下

struct SDS {
	T capacity; // 数组容量
	T len; // 数组长度
	byte flags; // 特殊标识位,不理睬它
	byte[] content; // 数组内容
}

SDS 结构使用了范型 T,这是因为当字符串比较短时,len 和 free可以使用 byte 和 short 来表示,Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。

(2)list类型

list它的底层实现实际是个双向链表,通过对链表的操作实现list效果,更加具体的来说,它的底层实现可以分为ZipList(压缩链表)和QuickList(快速链表),当数据量少的时候用的压缩链表,数据量比较多的时候切换成快速链表。

压缩链表结构:
在这里插入图片描述

struct ziplist {
	int32 zlbytes; // 整个压缩列表占用字节数
	int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
	int16 zllength; // 元素个数
	T[] entries; // 元素内容列表,挨个挨个紧凑存储
	int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

快速链表结构:
在这里插入图片描述

struct quicklistNode {
	quicklistNode* prev;
	quicklistNode* next;
	ziplist* zl; // 指向压缩列表
	int32 size; // ziplist 的字节总数
	int16 count; // ziplist 中的元素数量
	int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
	...
}
struct quicklist {
	quicklistNode* head;
	quicklistNode* tail;
	long count; // 元素总数
	int nodes; // ziplist 节点的个数
	int compressDepth; // LZF 算法压缩深度
	...

(3)hash类型

使用字典作为存储结构:

dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。
在这里插入图片描述
典数据结构的精华就落在了 hashtable 结构上了。hashtable 的结构和 Java 的 HashMap 几乎是一样的,都是通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。

结构如下:

struct dictEntry {
	void key;
	void val;
	dictEntry* next; // 链接下一个 entry
}
struct dictht {
	dictEntry** table; // 二维
	long size; // 第一维数组的长度
	long used; // hash 表中的元素个数
	...
}

(4)set类型

Set的实现参考哈希的底层实现,Set就是Value都为空的Hash。

(5)zset类型

它的内部实现用的是一种叫着「跳跃链表」的数据结构。
因为 zset 要支持随机的插入和删除,所以它不好使用数组来表示(数组的随机插入删除效率太低)。所以肯定要用链表。但zset的特点就是有一个Score值,Zset又是有序的,每次插入新元素要插入到适当的位置而不是无脑追加到末尾,也就是插入前肯定要定位。二分查找法对象只能是数组(因为有索引下标),这时候跳跃链表(skipList)就闪亮登场了。
关于跳跃链表,《Redis深度历险》这本书举得例子挺好,我就直接粘过来了:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

六、redis配置文件详解

在学习这一部分的内容时,我查询了大量的资料,现将查询到的相关知识记录在下面

配置文件位于redis的安装目录下,本人这里用的win10系统,配置文件如下图
在这里插入图片描述
为了方便解读,我将配置文件的内容复制了一份,并且在idea项目里面新建一个文本,将内容粘贴在该文本,这么做的目的主要是我的idea有安装英语的翻译插件,方便解读英语的注释

首先是一开始的说明
在这里插入图片描述
INCLUDES(包括)
在这里插入图片描述
NETWORK(网络)
在这里插入图片描述
在这里插入图片描述
GENERAL(通用)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
SNAPSHOTTING(快照)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

SECURITY(安全)
在这里插入图片描述

LIMITS(限制)
在这里插入图片描述
在这里插入图片描述

APPEND ONLY MODE
在这里插入图片描述
除此之外还有其他配置,上面列的是一些相对重要的配置。

七、持久化

上面说到redis是一个内存数据库,数据都是保存在内存里面的,断电即失,但redis也是支持持久化的,一般下载redis的时候,它的配置文件就先配置好了持久化,就是上面配置里面提到的rdb和aop,默认是开启的rdb,aop默认是关闭的。

(1)RDB

作为默认开启的持久化方案,在一启动redis的时候,在同级目录下会自动生成一个
dump.rdb文件,持久化存储数据
在这里插入图片描述
配置参数
在这里插入图片描述
rdb的配置在SNAPSHOTTING(快照)那一部分里面,一般采用默认的设置即可

原理
在这里插入图片描述
(1)redis根据配置自己自动生成rdb快照文件
(2)fork一个子进程出来,此时父进程负责客户端的请求,子进程负责持久化数据,子进程共享父进程的内存数据
(3)父进程响应客户端请求,子进程尝试将数据dump到临时的rdb快照文件中
(4)子进程完成rdb快照文件的生成之后,就替换之前的旧的快照文件,每次生成一个新的快照,都会覆盖之前的老快照

触发持久化的机制
(1)满足配置文件的相关配置时会自动触发rdb规则
(2)执行 flushall 命令,也会触发我们的rdb规则!
(3)退出redis,也会产生 rdb 文件! 备份就自动生成一个 dump.rdb

优点
(1)RDB文件是紧凑的二进制文件,比较适合做冷备,全量复制的场景。
(2)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速;
理由:AOF,存放的指令日志,做数据恢复的时候,其实是要回放和执行所有的指令日志,来恢复出来内存中的所有数据的;
RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可;
RDB的时候,Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可;
(3)RDB使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了Redis的高性能 ;

缺点
(1)如果遇到redis进程宕机等意外,redis根据配置文件会有少量数据的丢失
(2)RDB无法实现实时或者秒级持久化,RDB是间隔一段时间进行持久化,如果持久化之间Redis发生故障,会发生数据丢失。

(2)AOF

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

简单来说就是将我们的所有命令都记录下来,恢复的时候就把之前记录的命令重新执行一遍以恢复数据。

原理
在这里插入图片描述
简单理解就是,AOF持久化功能的实现可以分为命令追加、文件写入、文件同步三个步骤,在执行命令时,服务器会将命令内容先追加到这个缓冲区,然后再将命令从缓冲区中写入AOF文件。

文件写入与同步
就像上面说的,在执行命令后,aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
通过这样的方式将命令从缓冲区中写入AOF文件。

触发持久化的机制
(1)在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程调用的, 所以它不会引起服务器主进程阻塞。

(2)在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。

优点
(1)每一次修改都同步,文件的完整会更加好!
(2)每秒同步一次,可能会丢失一秒的数据

缺点
(1)因为恢复的时候涉及到IO操作,所以在大量数据的情况下,效率较慢
(2)相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!

八、redis发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、 微博、关注系统!具体的可以参考菜鸟教程的相关教程
在这里插入图片描述

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
在这里插入图片描述
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
在这里插入图片描述

redis发布订阅常用命令
在这里插入图片描述
使用场景
(1)实时消息系统!
(2)事实聊天!(频道当做聊天室,将信息回显给所有人即可!)
(3)订阅,关注系统都是可以的!

(1)redis实现发布与订阅

在这里插入图片描述

(2)java实现发布与订阅

配置类

package com.zsc.config;


import com.zsc.server.ReceiverServer01;
import com.zsc.server.ReceiverServer02;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisListenerConfig {
    /**
     * redis消息监听器容器
     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
     * @param connectionFactory
     * @param listenerAdapter1
     * @return
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter1,
                                            MessageListenerAdapter listenerAdapter2)
    {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);

        //添加适配器和订阅频道
        container.addMessageListener(listenerAdapter1, new PatternTopic("Test"));//Test频道
        container.addMessageListener(listenerAdapter2, new PatternTopic("Welcome"));//Welcome频道


        return container;
    }


    /**
     * 订阅频道关联1:ReceiverService01
     * 消息监听器适配器,用于添加订阅频道及其方法
     * @param redisReceiver01
     * @return
     */
    @Bean
    MessageListenerAdapter listenerAdapter1(ReceiverServer01 redisReceiver01) {
        System.out.println("消息适配器:关联频道1");
        return new MessageListenerAdapter(redisReceiver01, "onMessage");
    }

    /**
     * 订阅频道关联2:ReceiverService02
     * 消息监听器适配器,用于添加订阅频道及其方法
     * @param redisReceiver02
     * @return
     */
    @Bean
    MessageListenerAdapter listenerAdapter2(ReceiverServer02 redisReceiver02) {
        System.out.println("消息适配器:关联频道2");
        return new MessageListenerAdapter(redisReceiver02, "onMessage");
    }



}

订阅频道
这里写了两个订阅频道

Test频道

package com.zsc.server;

import org.springframework.stereotype.Service;

@Service
public class ReceiverServer01 {


    public void onMessage(Object message) {
        // 缓存消息是序列化的,需要反序列化。然而new String()可以反序列化,但静态方法valueOf()不可以
        System.out.println("这里是订阅频道:Test");
        System.out.println("主题发布:" + message);
        System.out.println("=================================================================");
    }
}

Welcome频道

package com.zsc.server;

import org.springframework.stereotype.Service;

@Service
public class ReceiverServer02 {


    public void onMessage(Object message) {
        // 缓存消息是序列化的,需要反序列化。然而new String()可以反序列化,但静态方法valueOf()不可以
        System.out.println("这里是订阅频道:Welcome");
        System.out.println("主题发布:" + message);
        System.out.println("=================================================================");
    }
}

发布者

package com.zsc.server;

import com.zsc.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PublishService {
    @Autowired
    private RedisUtil redisUtil;
    /**
     * 发布方法
     * @param channel 消息发布订阅 主题
     * @param message 消息信息
     */
    public void publish(String channel, Object message) {
        redisUtil.convertAndSend(channel, message);//发布消息
    }

}

测试

package com.zsc;

import com.zsc.server.PublishService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootRedis4ApplicationTests {

    @Autowired
    private PublishService publishService;

    @Test
    void contextLoads() {
        publishService.publish("Test","欢迎来到Test频道");//向频道Test发送信息
        publishService.publish("Welcome","欢迎来到Welcome频道");//向频道Welcome发送信息
    }

}

发布者向特定的频道发送消息,只要有订阅对应频道就会接收到现应消息,为此我还特地封装了一下RedisUtil工具类的convertAndSend方法。

九、redis主从复制

概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

简单原理
在这里插入图片描述
这种一主多从的架构方式除了可以大大的缓解redis的压力之外,最大的好处就是如果主节点(master)意外挂掉的话,还可以从从节点(slave)恢复数据,一般来说,主节点(master)用于处理写请求,从节点(slave)用于处理读请求,因为大部分请求都是“读请求”大于“写请求”,所以一般从节点需要两个以上,也就是最基本的“一主二从”的架构。

详细原理
Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。

  • 全量同步
    Redis全量复制一般发生在从节点(slave)初始化阶段,这时从节点(slave)需要将主节点(master)上的所有数据都复制一份。具体步骤如下:
    (1)从服务器连接主服务器,发送SYNC命令;
    (2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
    (3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
    (4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
    (5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
    (6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲

在这里插入图片描述
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

  • 增量同步
    Redis增量复制是指从节点(slave)初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
    增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,从节点(slave)在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。如果多个从节点(slave)断线了,需要重启的时候,因为只要从节点(slave)启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。

(1)window10搭建redis单机多节点集群

环境配置
下面配置一个”一主二从“的单机多节点集群,也叫单机多集群,配置的时候只配置从库,不用配置主

将默认的配置拷贝两份,并重新命名以区分
在这里插入图片描述
依次修改6380和6381对应的配置文件,这两个是从库的配置文件,需要将配置文件的一些端口号等参数配置好
在这里插入图片描述
注意:如果是linux系统的话,还需要修改pid名字加以区分

总结下来就是要修改以下配置
(1)端口
(2)pid 名字(linux系统需要配,win系统不用配)
(3)log文件名字
(4)dump.rdb 名字

将两个从库的配置文件都配置好之后,环境配置就完成了,下面就可以开始测试了

启动服务
启动3个redis服务,即一主二从
在这里插入图片描述
查看这3个进程是否有在运行
在这里插入图片描述
启动对应的3个客户端,并且查看主从节点信息
在这里插入图片描述
可以看到在没开始配置节点之前,每一个都是一个独立的主节点。

主从节点配置
默认情况下,每台Redis服务器都是主节点, 我们一般情况下只用配置从机就好了!配置的方式也比较简单粗暴,就是给对应端口的从节点服务器设置一个"主人"即可,一主 (79)二从(80,81)
在这里插入图片描述
节点配置完查看主节点的服务器,可以发现已经绑定了两个从节点
在这里插入图片描述
此时如果查看从节点的主从信息,可以发现信息为slave(从节点)

主从节点配置的另一种方式
处了上面的配置方式外,还有一种配置方式,就是直接在配置文件里面配置主从节点
在这里插入图片描述

使用

  • 读写分离
    这里有一个重点,一旦配置要主从节点(这里配置了“一主二从”),主节点(master)只能处理写请求,从节点(slave)只能处理读请求
    在这里插入图片描述
    同时,只要主节点有读写操作,就会同步到其余的从节点,在主节点写入数据,可以在从节点获取到对应的数据,通过这种读写分开的方式,可以大大的缓解redis服务器的压力。

  • 模拟主机意外宕机
    在这里插入图片描述
    主机意外宕机,断开连接后,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依 旧可以直接获取到主机写的信息!

(2)Linux搭建redis单机多节点集群

这里采用docker进行redis的单机多节点集群搭建,docker安装redis,以及如何配置文件请参考我的另一篇博文:菜鸟学习Docker实例

前提:docker下载好了redis,并且配置好相应的配置文件

用xshell连接服务器,运行3个redis实例

cainiao@cainiao-virtual-machine:~$ docker run -p 6381:6379 --name redis-6381 -v $PWD/redis/conf/redis.conf:/etc/redis/redis.conf  -v $PWD/redis/data:/data -d redis:latest redis-server /etc/redis/redis.conf --appendonly yes
eef1dc53c4ab92cffed0710646671073ccea386d1fb54b4aa32d014dea14b5e8

cainiao@cainiao-virtual-machine:~$ docker run -p 6380:6379 --name redis-6380 -v $PWD/redis/conf/redis.conf:/etc/redis/redis.conf  -v $PWD/redis/data:/data -d redis:latest redis-server /etc/redis/redis.conf --appendonly yes
ea833ef57a5f1127943e0bb2717b7baf1f4c30b5cc3d8dde257b929c88e65b45

cainiao@cainiao-virtual-machine:~$ docker run -p 6379:6379 --name redis-6379 -v $PWD/redis/conf/redis.conf:/etc/redis/redis.conf  -v $PWD/redis/data:/data -d redis:latest redis-server /etc/redis/redis.conf --appendonly yes
7566d9511df563ea582a20064ff21c22fa9bf4422a851aa98de6cfb352444440

必须要确保这3个实例都有在运行
在这里插入图片描述
同时为了方便模拟单机集群的一主二从,这里用xshell建立了4个会话
在这里插入图片描述
查看主库(主节点)ip

docker inspect 容器ID 或 容器名称

在这里插入图片描述
这里配置redis-6379为主库(主节点),ip:172.17.0.5

配置从库1:redis-6380
运行对应redis客户端:docker exec -ti redis-6380 redis-cli
认redis-6379为主库(主节点):SLAVEOF 172.17.0.5 6379
在这里插入图片描述
配置从库2:redis-6381
运行对应redis客户端:docker exec -ti redis-6381 redis-cli
认redis-6379为主库(主节点):SLAVEOF 172.17.0.5 6379
在这里插入图片描述

查看主节点
查看主节点是否配置了两个从节点:info replication
在这里插入图片描述
至此配置成功,当然,网上还有一种docker-compose的搭建方式,据说十分简单,有时间再更新出来。

注意:这里说一下自己在测试时遇到的一个玄学问题,上面docker的一主二的从搭建,是在配置文件没有设置密码的情况下操作的,如果有设置密码的话,自己在测试的过程中,从节点配置好之后,主节点没有显示连接成功,也就是搭建失败,这个原因以后自己解决了之后会再更新。

十、哨兵模式

在上面的一主二从的搭建中,经过测试发现,如果主节点挂掉的话,虽然从节点依旧可以用,但是就不会再有主节点,如果要再选出一个主节点的话,需要人工手动执行 SLAVEOF no one 代码让自己变成主机!其他的节点就可以手动连 接到新的这个主节点(手动)!如果这个时候之前的那个主节点修复了,那就重新连接!

那么有没有一种方式,如果主节点挂掉后,系统自动选出一个新的主节点接替那个挂掉的主节点,不用人工干预呢,要想实现这种效果,就必须使用所谓的哨兵模式

什么是哨兵模式

Redis 的 Sentinel 系统(哨兵模式)用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务(具体介绍可参考 sentinel):

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

哨兵模式原理
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工 干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑 哨兵模式。

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

搭建哨兵模式
下面在主从复制的基础上搭建1个哨兵,即一主二从1哨兵
sentinel-6379哨兵监控redis-6379

配置文件
跟上面配置主从复制的时候一样,再redis/conf文件下创建一个配置文件sentinel.conf文件,这里创建了一个哨兵配置文件sentinel-6379.conf
在这里插入图片描述
sentinel-6379.conf

# 哨兵sentinel实例运行的端口 默认26379 
port 26379
# 关闭保护模式
protected-mode no

# 不限制访问
bind 0.0.0.0

# 配置监听的主服务器
# 这里sentinel monitor代表监控
# redis-master 代表服务的名称,可以自定义
# IP + Port 代表监控的主服务器
# 1 代表只有一个或一个以上的哨兵认为主服务器不可用的时候,才会进行故障转移
# 比如,我这里的例子一共是 3 个 sentinel,当主的挂了,剩下两个只要有一个认为主的出问题,即可转移
sentinel monitor redis-master 172.17.0.3 6379 1

# 日志文件路径指定
logfile "/sentinel-6379.log"

# 主 redis 密码(如果主从密码不一样,那就用 bind 或者 关闭保护模式)
#sentinel auth-pass redis-master 111111

# redis 在一定时间内无法应答哨兵,视为下线,默认为3000030秒)
sentinel down-after-milliseconds redis-master 5000

# 指定故障切换允许的毫秒数,超过这个时间,就认为故障切换失败,默认为3分钟
sentinel failover-timeout redis-master 60000

# 指定可以有多少个Redis服务同步新的主机
# 一般而言,这个数字越小同步时间越长
# 越大,则对网络资源要求越高
sentinel parallel-syncs redis-master 1   

启动redis-sentinel

docker run -p 26379:26379 --name sentinel-6379 -v $PWD/redis/conf/sentinel-6379.conf:/etc/redis/sentinel.conf  -v $PWD/redis/data:/data -d redis:latest redis-sentinel /etc/redis/sentinel.conf          

在这里插入图片描述

查看哨兵是否有在运行
在这里插入图片描述
运行redis客户端查看哨兵的监控情况
在这里插入图片描述
测试
在这里插入图片描述

断开主节点,此时哨兵检测到后,会通过投票算法选出一个新的主节点,自动配置好,不用人工干预
在这里插入图片描述

十一、缓存穿透

正常查询流程
一般来说正常的查询流程就是一个请求进来后,先查询redis缓存,如果没有才去查询数据库,如果数据库查到有这个值就会将该值写入redis缓存,没有的话就不会写入redis缓存。

什么叫缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,当处于高并发的情况下,如果大量数据都要去查询持久层的数据库的话,这样的话就会造成缓存穿透,即穿透缓存直接查询数据库,这也容易被一些不法分子利用,如果有人故意大量查询一些本就不存在的值,就容易造成缓存穿透的现象。

解决方案一:空缓存对象
在这里插入图片描述
造成缓存穿透的原因就是大量访问了本就不存在的值,造成每一次都要去查询数据库的情况,为此解决的方案之一就是让缓存中有值即可,当用户查询一个不存在的值的时候,可以将该数据库不存在的值通过键值对的方式写入缓存,值为空即可,同时出于性能的考虑,必须将该空值的键设定一个短时间的过期时间,10s或20秒之类的,这样下次还查询该值时就可以直接从缓存中查询。

    @Test
    void  CacheTest(){
        Integer uId01 = -1;//要查询用户id
        String key = "user_" + uId01;//对应用户的key

        if(redisUtil.hasKey(key)) {//缓存中存在要查询的值,查询缓存
            System.out.println("缓存中存在要查询的值,查询缓存");
            User user = JSONObject.parseObject(redisUtil.get(key).toString(), User.class);//json转User对象
            System.out.println("user:"+user);

        }else {//缓存中不存在要查询的值,查询数据库
            System.out.println("缓存中不存在要查询的值,查询数据库");
            User user = userDao.findUserById(uId01);
            System.out.println("user:"+user);
            redisUtil.set(key,JSON.toJSONString(user));

            if (user == null){
                redisUtil.expire(key,30);
            }

        }

    }

该方式虽然可以处理redis的缓存穿透,但是存在以下缺点:
(1)空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

(2)缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

解决方案二:布隆过滤器
在这里插入图片描述

概念
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为 O(n),O(log n),O(n/k),这个时候就可以用布隆过滤器解决,虽然会存在有一定的误差。

原理
具体深入的原理本人没有去深究过,布隆过滤器的大致原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本原理。

使用场景
(1)解决缓存穿透,快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。
(2)判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中
(3)邮箱的垃圾邮件过滤、黑名单功能
(4)去重:比如爬给定网址的时候对已经爬取过的 URL 去重。

使用方式
我们可以提前将真实正确的商品Id或者用户id或要查询的id,在添加完成之后便加入到过滤器当中,每次再进行查询时,先确认要查询的Id是否在过滤器当中,如果不在,则说明Id为非法Id,则不需要进行后续的查询步骤了。

解决缓存穿透
这里引入hutool工具包,里面有集成了布隆过滤器

    @Test
    void BitMapBloomFilterTest(){
        Integer userCount = userDao.findUserCount();//所有用户的数量
        List<User> allUser = userDao.findAllUser();//所有用户

        // 初始化
        BitMapBloomFilter filter = new BitMapBloomFilter(userCount);

        //提前将所有的用户id存进布隆过滤器
        for (int i = 0; i < userCount; i++) {
            filter.add(allUser.get(i).getId().toString());
        }

        Integer uId01 = 2;//要查询用户id
        boolean hasUser = filter.contains(uId01.toString());//查询布隆过滤器中是否有该值

        if(hasUser){//用户输入的id存在,即数据合法
            System.out.println("布隆过滤结果:用户数据合法");
            String key = "user_" + uId01;//对应用户的key

            if(redisUtil.hasKey(key)) {//缓存中存在要查询的值,查询缓存
                System.out.println("缓存中存在要查询的值,查询缓存");
                User user = JSONObject.parseObject((String) redisUtil.get(key), User.class);//json转User对象
                System.out.println("user:"+user);

            }else {//缓存中不存在要查询的值,查询数据库
                System.out.println("缓存中不存在要查询的值,查询数据库");
                User user = userDao.findUserById(uId01);
                System.out.println("user:"+user);
                redisUtil.set(key,JSON.toJSONString(user));

                if (user == null){
                    redisUtil.expire(key,30);
                }

            }

        }else {//用户输入的id不存在,即数据不合法
            System.out.println("布隆过滤结果:用户数据不合法");
        }

        
    }

提前将所有用户id存进布隆过滤器,在用户来请求时,先经过布隆过滤器,验证用户请求的用户id是否合法,是则进行下一步查询,不是则直接结束。

缺点
bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性

(1)存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。

(2)删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。

十二、缓存击穿

概念
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,给人的感觉就像同时所有人都在攻击同一个点,然后将其击穿的感觉。

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

案例
最常见的电商的秒杀项目

解决方案一
发送这种缓存击穿的情况一般是缓存时间到期,同时用户对某一个“爆款”的疯狂抢购,才会造成击穿现象,个人觉得要实现这个击穿的效果,一定是大量数据的高并发导致,而且这种情况应该很少遇到,因为要实现这么大的流量就很困难。

解决方案也很简单,直接设置的缓存永不过期即可,一劳永逸,要是不设置缓存过期,导致内存不够的话,建议用集群。

解决方案二
除了设置缓存永不过期之外,还可以用互斥锁(mutex key)的方式处理缓存击穿,让同时的大量请求,只允许一个请求去查询数据库,然后回写缓存即可,实现互斥锁的方式有很多,而redis刚好有一个命令可以用来实现互斥锁,那就是setnx命令,只有key不存在的情况下才能创建成功,否则创建失败。

    /**
     * 缓存击穿解决方案
     */
    private User CacheBreakdownGet(Integer id){
        String key = "user_" + id;//对应用户的key

        if(redisUtil.hasKey(key)) {//缓存未过期
            System.out.println("缓存中存在要查询的值,查询缓存");
            User user = JSONObject.parseObject(redisUtil.get(key).toString(), User.class);//json转User对象
            return user;

        }else {//缓存过期

            //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
            if(redisUtil.setNx("key_mutex",1,3*60)){//创建key_mutex,相当于上锁
                User user = userDao.findUserById(id);

                //TODO 这里还可以做缓存穿透处理:缓存空对象等操作

                redisUtil.set(key,JSON.toJSONString(user));
                redisUtil.del("key_mutex");//删除key_mutex,相当于解锁
                return user;

            }else {//这个时候代表同时候的其他线程已经load db并回设到缓存了
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                CacheBreakdownGet(id);

            }
        }

        return null;
    }

为了能更好的使用setnx命令,我还特地去重写了几个redisUtils工具类的几个方法。

十三、缓存雪崩

概念
缓存雪崩是指缓存在一段时间内突然集体失效,或者其他原因导致所有的请求都去请求持久层的数据库,这样的话后台的数据库压力就会很大,当达到数据库能够承受的极限时,就会引起数据库服务器的瘫痪,接着一些列的雪崩效应。

应用场景
在这里插入图片描述
(1)流量激增:比如异常流量、用户重试导致系统负载升高;
(2)缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;
(3)程序有Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;
(4)硬件故障:比如宕机,机房断电,光纤被挖断等。
(5)数据库严重瓶颈,比如:长事务、sql超时等。
(6)线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;

解决方案
因为造成缓存雪崩的原因是多方面的,所以没法直接给出代码,只能给出一些网络上常见的解决方案的一些秒速,首先可以简单分为两类情况,即雪崩前和雪崩后的处理。

雪崩后的处理

(1)熔断模式
这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。

总之,除了cpu、内存、线程数外,重点监控数据库端的长事务、sql超时等,绝大多数应用服务器发生的雪崩场景,都是来源于数据库端的性能瓶颈,从而先引起数据库端大量瓶颈,最终拖累应用服务器也发生雪崩,最后就是大面积的雪崩。

在熔断的设计主要参考了hystrix的做法。其中最重要的是三个模块:熔断请求判断算法、熔断恢复机制、熔断报警

  • 熔断请求判断机制算法:使用无锁循环队列计数,每个熔断器默认维护10个bucket,每1秒一个bucket,每个blucket记录请求的成功、失败、超时、拒绝的状态,默认错误超过50%且10秒内超过20个请求进行中断拦截。
  • 熔断恢复:对于被熔断的请求,每隔5s允许部分请求通过,若请求都是健康的(RT<250ms)则对请求健康恢复。
  • 熔断报警:对于熔断的请求打日志,异常请求超过某些设定则报警。

重点监控的机器性能指标

  • cpu(Load) cpu使用率/负载
  • memory 内存
  • mysql监控长事务(这里与sql查询超时是紧密结合的,需要重点监控)
  • sql超时
  • 线程数等

(2)隔离模式

这种模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。

例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。

隔离的方式一般使用两种

  • 线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
  • 信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)

超时机制设计

  • 超时分两种,一种是请求的等待超时,一种是请求运行超时。
  • 等待超时:在任务入队列时设置任务入队列时间,并判断队头的任务入队列时间是否大于超时时间,超过则丢弃任务。
  • 运行超时:直接可使用线程池提供的get方法。

雪崩前的处理
这一步的处理要求提前预防雪崩,所以这是很重要的一步,首先要做的就是上面讲过的缓存穿透和缓存击穿的相应处理,这是必须要做的,该加布隆过滤器的加布隆过滤器,该加互斥锁的加互斥锁,还有就是缓存不要设置同一时间过期,防止大量缓存同时失效造成雪崩,还有就是可以加二级缓存,如果一级缓存失效了了可以使用二级缓存,这些基本的处理都做了之后,也并不一定能保证一定不会雪崩,这个时候就需要用到一种限流的思想,对所有的请求都设置一个阈值,如果当请求数量接近这个阈值或者达到这个阈值的之前,不再响应请求,所以的请求直接返回服务器繁忙即可,当服务器的压力没那么大的时候再响应请求。

  • 1
    点赞
  • 3
    评论
  • 22
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:创作都市 设计师:CSDN官方博客 返回首页

打赏作者

最强菜鸟

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值