Redis学习笔记
该笔记大部分搬运B站遇见狂神说的Redis,顺便把图文合并记录,便于回顾
视频地址: 狂神Redis视频,记得三连!
1. Redis知识点
- nosql 概述
- 阿里巴巴架构演进
- nosql 数据模型
- Nosql 四大分类
- CAP
- BASE
- Redis 入门
- Redis安装 (Window & Linux服务器)
- 五大基本类型
- String
- List
- Set
- Hash
- Zset
- 三种特殊数据类型
- geo
- hyperloglog
- bitmap
- Redis 配置详解
- Redis 持久化
- RDB
- AOF
- Redis 事物操作
- Redis 实现订阅发布
- Redis 主从复制
- Redis 哨兵模式
- 缓存穿透及其解决方案
- 缓存雪崩及其解决方案
- 基础API之Jedis详解
- SpringBoot集成Redis操作
- Redis的实践分析
2.Nosql概述
为什么要使用Nosql
1.单机MySql模式
90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!当时,更多的去使用静态网页HTML服务器根本没有太大的压力!
当时网站的瓶颈:
- 数据量如果太大,一个机器放不下了!
- 数据的索引(B+ Tree),一个机器的内存也放不下
- 访问量( 读写混合 ),一个服务器承载不了~
只要出现了三种情况之一,那么你就必须要晋级!
- Memcached(缓存) + Mysql + 垂直拆分
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以我们希望减轻数据库的压力,我们可以使用缓存来保证效率!
- 分库分表 + 水平拆分 + MySql集群
技术和业务在发展的同时对人的要求也越来越高!
本质:数据库(读,写)
早些年MyISAM:表锁,十分影响效率!高并发下就会出现严重的问题
现在 Innodb: 行锁
慢慢的就开始使用分库分表来解决写的压力! MySql在那个年代推出了表分区!这个并没有多少公司使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylym7Uvo-1664799492826)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220914172802206.png)]
4, 如今最近的年代
2010–2020十年之间,世界已经发生了翻天覆地的变化;(定位,也是一种数据,音乐,热榜!)
MySql等关系型数据库就不够用了! 数据量很多,变化很快~!
MySql有的使用它来存储一些比较大的文件,博客,图片!数据库表很大.效率就低了!如果有一种数据库来专门处理这种数据,MySql压力就变得十分小了(研究如何处理这些问题!)大数据IO压力下,表几乎没法更大!
目前的一个基本互联网项目
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RgVRFb8K-1664799492828)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220914180235758.png)]
为什么要使用NoSQL!
用户的个人信息,社交网络,地理位置.用户自己产生的数据,用户日志等等爆发式增长!
这个时候我们就需要使用NoSQL数据库,NoSQL可以很好的处理以上情况
什么是NoSQL
NoSQL
NoSQL=Not Only SQL(不仅仅是SQL)
关系型数据库: 表格,行,列
泛指非关系数据库的,随着Web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的并发社区!暴露出来很多难以克服的问题,NoSQL在当今大数据环境下的发展十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一门技术!
很多的数据类型用户的个人信息,社交网络,地理位置,这些数据类型的存储不需要一个固定式的格式!不需要多余的操作就可以横向扩展! Map<String,Object> 使用键值对!
NoSQL特点
解耦!
- 方便扩展(数据之间没有关系,很好扩展!)
- 大数据量高性能(Redis一秒写8万次,读取11万,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样的! (不需要事先设计数据库!随取随用!如果是数据量十分大的表,很多人就无法设计了!)
- 传统RDBMS和NoSQL
- 传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中
- 操作,操作数据定义语言
- 严格的一致性
- 基础的事务
- …
- NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理 和 BASE (异地多活!)
- 高性能,高可用,高可扩
- …
- 传统的 RDBMS
了解:3V + 3高
大数据时代的3V:主要是描述问题的
- 海量Volume
- 多样Variety
- 实时Velocity
大数据时代的3高:主要是对程序的要求
- 高并发
- 高可扩
- 高性能
真正在公司中的实践: NoSQL + RDBM5 一起使用才是最强的,阿里巴巴的架构演进!
NoSQL的四大分类
KV键值对:
- 新浪: Redis
- 美团: Redis + Tair
- 阿里,百度: Redis + memecache
文档数据库(bson格式和json一样):
- MongoDB
- MongoDB是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档!
- MongoDB是一个介于关系型数据库和非关系型数据库中中间的产品! MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的!
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
- 他不是存图形,放的是关系,比如:朋友圈的社交网络,广告推荐!
- Neo4j,InfoGrid;
分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值对(Key-Value) | tokyo Cabinet/Tyrant,Redis,Voldemort,Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等 | key指向Value的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra,HBase,Riak | 分布式文件系统 | 以列簇式存储,将同一列数据存储在一起 | 查找速度快可扩展能力强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB,MongoDB | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对Value为结构化数据 | 数据结构要求下严格,表结构可变,不需要像关系型数据库一样需要预先定义表的结构 | 查询性能不高,而且缺乏统一查询语句 |
图形(Graph)数据库 | Neo4j,InfoGrid,Infinite Graph | 社交网络,推荐系统等,专注于构建关系图普 | 图结构 | 利用图结构相关算法,比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图形做计算才能得出需要的信息,而且这种机构不太好做分布式的集群方案 |
3. Redis入门
概述:
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务,
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
免费和开源!是当下最热门的NoSQL技术之一!也被人们称之为结构化数据库!
Redis能干嘛?
- 内存存储,持久化,内存中是断电即失,所以持久化很重要
- 效率高,可用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器,计数器(浏览量!)
- …
特性
-
多样的数据类型
-
持久化
-
集群
-
事务
…
Redis下载:
官网: https://redis.io/
教程: https://www.redis.net.cn/
Windows版本:
需要在github进行下载:https://github.com/MicrosoftArchive/redis/releases
下载完毕后得到压缩包解压如:
Redis的端口号默认是6379
双击redis-server.exe则就可以进行连接了
后在打开redis-cli.exe输入ping命令检查十分成功如:
Linux下载:
下载地址: https://github.com/redis/redis
下载完后上传到服务器上如:
进行压缩如:
tar -zxvf 压缩文件
如:
后安装基本的环境如:
yum install gcc-cc++
make
make install
redis默认安装路径: usr/local
之后在usr/local/bin运行redis如:
redis默认不是后台启动的
将配置文件(redis.conf)改为yes
再运行redis客户端进行操作如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H6VpM4gr-1664799492838)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220915203421294.png)]
性能测试
redis-benchmark是一个官方自带的压力(性能)测试工具
redis-benchmark命令参数如下:
简单测试如下:
# 测试:100个并发连接 100000个请求
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
# 进行写入测试
====== SET ======
100000 requests completed in 1.32 seconds #对10万个请求进行写入处理
100 parallel clients #100个并发客户端
3 bytes payload #每次写入3个字节
keep alive: 1 #只有一台服务器处理这些请求
host configuration "save": 3600 1 300 100 60 10000
host configuration "appendonly": no
multi-thread: no
# 延迟分布
Latency by percentile distribution:
0.000% <= 0.255 milliseconds (cumulative count 1) #表示在第 0.255秒时处理了0.000%
50.000% <= 0.839 milliseconds (cumulative count 50166) #表示在第 0.839秒时处理了50.000%
75.000% <= 1.103 milliseconds (cumulative count 75057) #以此类推
87.500% <= 1.383 milliseconds (cumulative count 87816)
93.750% <= 1.527 milliseconds (cumulative count 94065)
96.875% <= 1.599 milliseconds (cumulative count 96911)
98.438% <= 1.647 milliseconds (cumulative count 98439)
99.219% <= 1.711 milliseconds (cumulative count 99231)
99.609% <= 1.783 milliseconds (cumulative count 99628)
99.805% <= 1.855 milliseconds (cumulative count 99814)
99.902% <= 2.167 milliseconds (cumulative count 99903)
99.951% <= 2.399 milliseconds (cumulative count 99952)
99.976% <= 2.583 milliseconds (cumulative count 99976)
99.988% <= 2.679 milliseconds (cumulative count 99988)
99.994% <= 2.719 milliseconds (cumulative count 99995)
99.997% <= 2.751 milliseconds (cumulative count 99999)
99.999% <= 2.759 milliseconds (cumulative count 100000)
100.000% <= 2.759 milliseconds (cumulative count 100000)
# 延迟累计分布
Cumulative distribution of latencies:
0.000% <= 0.103 milliseconds (cumulative count 0)
0.004% <= 0.303 milliseconds (cumulative count 4)
0.111% <= 0.407 milliseconds (cumulative count 111)
2.336% <= 0.503 milliseconds (cumulative count 2336)
14.110% <= 0.607 milliseconds (cumulative count 14110)
31.201% <= 0.703 milliseconds (cumulative count 31201)
46.330% <= 0.807 milliseconds (cumulative count 46330)
57.323% <= 0.903 milliseconds (cumulative count 57323)
67.095% <= 1.007 milliseconds (cumulative count 67095)
75.057% <= 1.103 milliseconds (cumulative count 75057)
80.139% <= 1.207 milliseconds (cumulative count 80139)
84.125% <= 1.303 milliseconds (cumulative count 84125)
88.922% <= 1.407 milliseconds (cumulative count 88922)
93.056% <= 1.503 milliseconds (cumulative count 93056)
97.263% <= 1.607 milliseconds (cumulative count 97263)
99.170% <= 1.703 milliseconds (cumulative count 99170)
99.704% <= 1.807 milliseconds (cumulative count 99704)
99.856% <= 1.903 milliseconds (cumulative count 99856)
99.874% <= 2.007 milliseconds (cumulative count 99874)
99.893% <= 2.103 milliseconds (cumulative count 99893)
100.000% <= 3.103 milliseconds (cumulative count 100000)
#总结
Summary:
throughput summary: 75585.79 requests per second #每秒处理了75585.79个请求
latency summary (msec):
avg min p50 p95 p99 max
0.920 0.248 0.839 1.551 1.687 2.759
基础知识
redis默认有16个数据库,配置文件如下:
默认使用的是第0个数据库
可以使用select 来切换数据库如:
可以使用dbsize命令来查看数据库的大小如:
# 切换数据库
select 数据库
# get获取值
get [key]
#设置值
set [key] [value]
# 查看全部key
keys *
# 查看数据库数量
dbsize
# 清空当前数据库数据
flushdb
Redis 是单线程的!
明白Redis是很快的,官方表示,Redis是基于内存操作的,CPU不是Redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了,所以就使用了单线程!
Redis是C语言写的,官方提供的数据 100000+ 的QPS(每秒查询速度),完全不比同样使用key-value的Memecache差!
Redis为什么单线程还怎么快?
误区:
- 高性能的服务器是多线程的?
- 多线程(如果只有一核会上下切换)一定比单线程效率高?
官方所叙述:Redis的操作都是基于内存的,CPU不是 Redis性能瓶颈,,Redis的瓶颈是机器内存和网络带宽。
核心: redis是将所有的数据放在内存中的,所以说使用单线程操作效率就是最高的.对于内存系统来说如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳方案!
4.五大数据类型
全段翻译:
Redis是一个开源(BSD许可)的,内存中的数据结构存系统,它可以用作数据库,缓存和消息中间件MQ。他支持多种类型的数据结构,如,字符串(String),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets) 与范围查询,bitmaps,hyperloglogs和地理空间(geospatial)索引半径查询.Redis内置了复制(replication),LUA脚本(Luascripting),LRU驱动事件(LRU eviction),事务(transactions)和不同级别的 磁盘持久化(persistence),并通过Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability).
Redis-Key
-- 查询所有key键
keys *
-- 查询某个key是否存在,存在返回1不存在返回0
exists [key]
-- 删除某个键
move [key] 1 --最后的1代表当前数据库
-- 定时某个键指定时间过期
expire [key] [秒] --如 expire name 10
-- 查看某个键的剩余过期时间
ttl [key]
--查看值的类型
type [key]
String(字符串)
-- 向值中添加字符串如
append [key] [value]
-- 获取字符串长度
strlen [key]
-- 执行加一操作
incr [key]
-- 执行减一操作
decr [key]
-- 设置步长(指定长度)
incrby [key] [长度]
-- 截取字符串
getrange [key] [开始的长度] [截取到的长度]
-- 截取全部字符串
getrange [key] 0 -1
-- 修改字符串值
strange [key] [从第几个开始] [需要修改的字符串]
-- 设置过期时间
setex [value] [秒]
-- 不存在该键则设置
setnx [key] [value]
-- 批量设置值(是一个原子性的操作,要么一起成功要么一起失败)
mset [key] [value] [key] [value] ......
-- 批量设置值不存在才设置
msetnx [key] [value] [key] [value] ......
-- 批量获取值
mget [key] [key] [key]......
-- 一般设置对象存储如:
set user:1 {name:zhangsan,age:3} #设置了一个user:1的key 值为json字符串
mset user:1:name xiaobu user:1:age 16 ## 推荐这样设计
-- 先获取值在设置值
getset [key] [value]
数据结构都是相同的!
使用场景:
- 计数器
- 统计多单位数量
List(列表)
基本的数据单位 列表
在redis中,我们可以把list用成堵塞队列
所有的list命令都是用l开头的,Redis不区分大小写命令!
-- 将一个或多个值插入列表的头部
lpush list 1
-- 将一个或多个值插入列表的头部
lpush list 2
-- 查询列表全部值
lrange list 0 -1
-- 将一个或者多个值插入列表的尾部
rpush list 3
-- 移除第一个值
lpop list
-- 移除最后一个值
rpop list
--指定获取list第几个值(通过下标获取list中某一个值)
lindex list 0
-- 查看list长度
llen list
-- 移除指定值 移除list中 1个值为1的值
lrem list 1 1
-- 修剪: 修减list中的值(这里表示保留mylist的值从第0个到第3个)
ltrim mylist 0 3
-- 移除列表最后一个元素并且将它添加到新的列表(组合命令)
rpoplpush mylist othrelist (这里表示移除mylist的最后一个元素添加到othrelist的第一个)
-- 更新列表的值(只能更新存在的值)
lset list 0 myname
-- 将值插入到某一个元素之前
linsert mylist before "world" "hei" (这里表示将hei插入到world的前面)
--将某一个值插入到元素之后
linsert mylist after world new (这里表示将new插入到world之后)
小结
- 它实际上是一个链表,before node after left right 都可以插入值
- 如果key存在不存在创建新的链表
- 如果key存在新增内容
- 如果移除了所以值! 空链表 ,也代表不存在!
- 在两边插入或改动值,效率最高! 中间元素,相对来说效率会低一点
消息排队!消息队列(lpush rpop) ,栈(lpush lpop) !
set(集合)
set中的值是不能重复的!
-- 添加set值
sadd list hello
-- 查看set值
smembers list
-- 判断某一个值是不是在set中
sismember list hello1 # 这里表示判断list中有没有hello1有返回1否则0
-- 指定移除set中某一个值
srem list hello1
-- 查看剩余元素数量
scard list
-- 随机抽取元素
srandmember list 1 # 表示随机抽取list中的一个元素
-- 随机删除元素
spop list
-- 删除元素移动到新的list
smove list list2 hello1 # 这里表示将hello1从list中删除移动到list2中
-- 获取差集
sdiff list1 list2 # 获取两个集合的差集也就是list1中不包含list2的值
-- 获取交集
sinter list1 list2
-- 获取并集
sunion list1 list2
使用场景:
微博,将所有关注的人放在一个集合中!将它的粉丝也放在一个集合中!
共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)
Hash(哈希)
Map集合,key-map! 时候这个值是一个map集合!
所以的hash命令是以h开头的!
-- 设置值
hset myhash name xiaobu # 表示在myhash集合中设置key为name值为小步
-- 获取值
hget myhash name
-- 设置多个值
hmset myhash age1 16 age2 14
-- 获取多个值
hmget myhash age1 age2
-- 获取全部数据
hgetall myhash
-- 删除指定key
hdel myhash age2
-- 获取map字段数量
hlen myhash
-- 判断字段是否存在
hexists myhash name
-- 获取所有key
hkeys myhash
-- 获取所有values
hvals myhash
-- 字段值自增
hincrby myhash age 1 #表示age字段的值自增1
-- 字段不存在则可以设置
hsetnx myhash test test
使用场景:
hash变更数据user name age 尤其是用户信息之类的,经常变动的信息! hash更适合于对象的存储,String更适合字符串的存储!
Zset(有序集合)
在set的基础上,增加了一个值,set k1 v1 而zset k1 score1 v1
-- 向集合中添加
zset myset 1 one # 向集合中添加标志为1的值为one ,该集合会根据标志排序
-- 便利所有的值(从小到大排序)
zrangebyscore salary -inf +inf #表示查看salary中的正无穷到负无穷的数
—— 便利所有的值并且带上标志(从小到大排序)
zrangebyscore salary -inf +inf withscores
—— 便利指定的值并且带上标志(从小到大排序)
zrangebyscore salary 0 10000 withscores # 表示便利标识最小值0~10000直接的值
-- 移除元素
zrem salary qiqi
-- 获取元素数量
zcard salary
-- 获取全部元素倒叙排列
zrevrange salary 0 -1
-- 获取指定区间的成员数量
zcount myset 1 3
实际场景:
set 需要用到排序的地方如:班级成绩,工资排序
普通消息是1 重要消息为2 带权重判断
排行榜实现
5. 三种特殊基本类型
geospatial(地理位置)
实际场景:
朋友的定位,附近的人,打车距离计算?Redis的Geo在Redis3.2版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人.
经纬度查询网站地址:http://www.jsons.cn/lngcode/
只有六个命令:
- GEOADD
- GEODIST
- GEOHASH
- GEOPOS
- GEORADIUS
- GEORADIUSBYMEMBER
geoadd
-- 添加地理位置 规则:两极无法直接添加 我们一般通过java程序直接导入
geoadd china:city 114.298572 30.584355 hubei # 添加地理位置 china:city 表示中国的城市 114.298572 30.584355 表示纬度与经度 hubei 表示地理位置名
geopos
-- 获取指定名称的维度和精度
geopos china hubei
geodist
获取两个地址之间的距离!
单位:
- m表示米
- km表示千米
- ml表示英里
- ft表示英尺
-- 查看两地之间相隔多钱千米
geodist china beijing shanghai km # 表示查看beijing到shanghai 之间直线距离有多少km
georadius
找附近的人?(获取所有附近的人地址,定位!)通过半径来查询!
获取指定数量的人:200
所有的数据应该都录入:china:city ,才会让结果更加精确
-- 寻找半径为500km以内的人
georadius china 110 30 500 km # 表示位置(经纬度)110 30开始向外半径500km以内的人
-- 寻找半径为500km以内的人 显示距离 显示经纬度 指定数量
georadius china 110 30 500 km withdist withcoord count 2 # 表示位置(经纬度)110 30开始向外半径500km以内的人withdist表示显示距离,withcoord显示经纬度,count 2表示只显示两个
georadiusbymember
-- 以当前指定录入的坐标找到周围指定范围的坐标
georadiusbymember china shanghai 5000 km
gephash 命令 返回一个或多个位置元素的Geohash表示
该命令将返回11个字符的Geohash字符串
-- 将经纬度转换为字符串(将二维的经纬度转换为一维的字符串,字符串越接近则越近)
gephash china beijing # 将beijing的经纬度转换为11个字符的Geohash字符串
> wx4fbxxfke0
GEO的底层原理其实就是zset! 我们可以使用zset命令来操作geo!
Hyperloglog
什么是基数?
A{1,3,5,7,8,7}
B{1,3,5,7,8}
基数(不重复的元素) =5,可以接受误差
简介
Redis 2.8.9版本就更新了Hyperloglog数据结构!
Redis Hyperloglo g 基数统计的算法!
优点:占用的内存是固定的,2^64不同元素的基数只需要12kb的内存!
网页的UV(一个人访问一个网站多次,还是算作一个人!)
传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标志判断!
这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id!
-- 向集合中添加元素(向后追加)(元素不重复)
pfadd userid 1 2 13 46
-- 查看元素数量
pfcount userzx
-- 合并元素
pfmergeuser userhb userzx1 userzx2 #表示合并userzx1和userzx2到userhb中
如果允许容错,那么可以使用Hyperloglog!
如果不允许容错,就使用set或者自己的数据类型即可!
Bitmap
位运算
统计用户信息,活跃,不活跃! 登录,未登录!打卡,365打卡! 有两个状态的都可以使用Bitmaps!
Bitmaps位图,数据结构!都是存在二进制位来进行操作,就只有0和1两个状态!
365天=365bit 1字节=8bit 46个字节左右!
-- 设置打卡数据
setbit sign 0 1 #表示设置第0天打卡了
-- 获取打卡数据
getbit sign 0 #获取第0天的打卡数据1
-- 统计打卡天数
bitcount sign #表示有1的个数有多少个
6. 事务
Redis 事务本质:一组命令的集合! 一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行!
一次性,顺序性,排他性!执行一系列的命令!
------ 队列 set set set 执行-------
Redis事务没有隔离级别的概念!
所有的命令在事务中,没有直接被执行!只有发起执行命令的时候才会执行! Exec
Redis单条命令是保存原子性的,但是事务不保证原子性!
redis的事务:
- 开启事务( multi )
- 命令入队( … )
- 执行事务( exec )
-- 开启事务
multi
-- 命令入队
set k1 k1
set v1 v1
get k1
get v1
-- 执行命令
exec
结果:
1) OK
2) OK
3) "k1"
4) "v1"
-- 或者不执行命令直接放弃事务
discard
编译型异常(代码有问题! 命令有错!) 事务中所有的命令都不会被执行!
运行时异常 如(1/0) 如果事务中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
Redis乐观锁 监控!
悲观锁:
- 很悲观, 什么时候都会出现问题,无论做什么都会加锁!
乐观锁:
- 很乐观,认为什么时候都不会出现问题,所以不会上锁! 更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
Redis监测测试
正常执行成功!
set money 100
set out 0
watch money #监视money对象 如果下面执行事务发现money修改过了则事务会执行失败
multi
decrby money 20 # 减去20
incrby out 20 #增加20
exec
如:
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
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
注意: 一旦事务执行成功后监视就会自动取消掉
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money #监视money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec #如果执行事务的时候另外一个线程去修改了监视的money则会执行失败
7.Jedis
我们要使用Java来操作Redis
什么是Jedis?
Jedis是Redis官方推荐的java连接开发工具!使用java操作Redis的中间件! 如果你要使用java操作redis,那么一定要对Jedis十分的熟悉!
Jejis测试:
-
创建项目
-
导入依赖
<!-- jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.3.0-m2</version> </dependency>
-
测试代码(记得开启RedisService服务端)
import redis.clients.jedis.Jedis; import java.util.Set; public class RedisStudy01 { public static void main(String[] args) { //1.创建一个Jedis对象即可其中包含了Redis全部的指令 Jedis jedis = new Jedis("127.0.0.1",6379); // 2.测试指令十分成功!(记得打卡Redis服务端口!) System.out.println(jedis.ping()); //3. 关闭连接 jedis.close(); } }
基本方法测试
Redis-Key
import redis.clients.jedis.Jedis;
import java.util.Set;
public class RedisStudy01 {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试 Redis-Key
// 1. 查询所有Key
Set<String> keys = jedis.keys("*");
//2.查询某个key是否存在
boolean name = jedis.exists("name");
// 3.定时某个键指定时间过期
long whetherSucceed = jedis.expire("name", 10);
//4.查看查看某个键的剩余过期时间
long timeLeft = jedis.ttl("name");
// 5.查看字段类型
String type = jedis.type("age");
jedis.close();
}
}
String
import redis.clients.jedis.Jedis;
import java.util.List;
public class RedisStudy01 {
public static void main(String[] args) throws ClassNotFoundException {
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试 String
//1. 向值中添加字符串(返回字符串长度)
long whetherSucceed = jedis.append("test1", "123");
//2. 获取字符串长度
long strLen = jedis.strlen("test1");
//3. 执行字符串数字加一操作(返回加一完成后的结果)
long incr = jedis.incr("age");
//4. 执行减一的结果
long decr = jedis.decr("age");
//5.设置步长(指定增加一次长度,返回加完后结果)
long incrBy = jedis.incrBy("age", 24);
// 6. 截取字符串(返回截取的长度)(原本的字符串不会改变)
String getRange1 = jedis.getrange("test1", 1, 5);
// 截取全部字符串
String getRange2 = jedis.getrange("test1", 0, -1);
//7. 设置过期时间(如果键存在则替换值)不存在则创建
String setEx = jedis.setex("test1", 10, "test");
// 8.不存在该键则设置
long setNx = jedis.setnx("name", "xb");
// 9.批量设置值(是一个原子性的操作,要么一起成功要么一起失败)
String mSet = jedis.mset("age1", "14", "age2", "15");
// 10.批量设置值不存在才设置
long mSetNx = jedis.msetnx("age1", "14", "age2", "15");
// 11. 批量获取值
List<String> mGet = jedis.mget("age1", "age2");
// 12. 先获取值在设置值(返回获取的值)
String age1 = jedis.getSet("age1", "18");
jedis.close();
}
}
List
import redis.clients.jedis.Jedis;
import redis.clients.jedis.args.ListPosition;
import java.util.List;
public class RedisStudy01 {
public static void main(String[] args) throws ClassNotFoundException {
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试 List
// 1. 将一个或多个值插入列表的头部
long lPush = jedis.lpush("list", "Hello", "World");
// 2. 查询列表全部值
List<String> lRange = jedis.lrange("list", 0, -1);
// 3. 将一个或者多个值插入列表的尾部
long rPush = jedis.rpush("list", "test1", "test2");
// 4. 移除第一个值
String lPop = jedis.lpop("list");
// 5. 移除最后一个值
String rPop = jedis.rpop("list");
// 6. 指定获取list第几个值
String lIndex = jedis.lindex("list", 2);
//7. 获取list长度
long lLen = jedis.llen("list");
//8. 修剪: 修减list中的值(这里表示保留list的值从第0个到第3个)
String lTrim = jedis.ltrim("list", 0, 3);
//9. 移除列表最后一个元素并且将它添加到新的列表(组合命令)
String rPopLPush = jedis.rpoplpush("list", "list2");
//10. 更新列表的值(只能更新存在的值)
String lSet = jedis.lset("list", 0, "test");
//11. 将值插入到某一个元素之前
long lInsert1 = jedis.linsert("list", ListPosition.BEFORE, "Hello", "Hi");
//12. 将某一个值插入到元素之后
long lInsert2 = jedis.linsert("list", ListPosition.AFTER, "Hello", "Hi");
jedis.close();
}
}
Set
import redis.clients.jedis.Jedis;
import java.util.Set;
public class RedisStudy01 {
public static void main(String[] args) throws ClassNotFoundException {
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试 Set
// 1.添加一个或多个set值
long sAdd = jedis.sadd("set", "Hello", "Set");
System.out.println(sAdd);
// 2.查看set值
Set<String> set = jedis.smembers("set");
// 3.判断某一个值是不是在set中
boolean sMember = jedis.sismember("set", "Hello");
// 4.指定移除set中某一个值
long sRem = jedis.srem("set", "Hello");
// 5.查看剩余元素数量
long sCard = jedis.scard("set");
// 6. 随机抽取元素
String sRandMember = jedis.srandmember("set");
// 7. 随机删除元素
String sPop = jedis.spop("set");
// 8.删除元素移动到新的list
long sMove = jedis.smove("set", "set2", "Hello");
// 9. 获取差集
Set<String> sDiff = jedis.sdiff("set", "set2");
// 10. 获取交集
Set<String> sInter = jedis.sinter("set", "set2");
// 11. 获取并集
Set<String> sUnion = jedis.sunion("set", "set2");
jedis.close();
}
}
Hash
import redis.clients.jedis.Jedis;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RedisStudy01 {
public static void main(String[] args) throws ClassNotFoundException {
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试 Hash
// 1.设置值
long hSet = jedis.hset("hash", "name", "xb");
// 2.获取值
String hGet = jedis.hget("hash", "name");
// 3.获取全部数据
Map<String, String> hGetAll = jedis.hgetAll("hash");
// 4.删除指定key
long hDel = jedis.hdel("hash", "name");
// 5.获取map字段数量
long hLen = jedis.hlen("hash");
//6.判断字段是否存在
boolean hExists = jedis.hexists("hash", "name");
// 7.获取所有key
Set<String> hKeys = jedis.hkeys("hash");
// 8.获取所有values
List<String> hValues = jedis.hvals("hash");
// 9.字段值自增
long l = jedis.hincrBy("hash", "age", 1);
// 10. 字段不存在则可以设置
long hsetnx = jedis.hsetnx("hash", "test", "test");
}
}
Zset
import redis.clients.jedis.Jedis;
import redis.clients.jedis.resps.Tuple;
import java.util.List;
public class RedisStudy01 {
public static void main(String[] args) throws ClassNotFoundException {
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试 Zset
//1.向集合中添加
long zAdd = jedis.zadd("zSet", 1, "Hello1");
//2. 便利所有的值(从小到大排序)
List<String> zSet = jedis.zrangeByScore("zSet", "-inf", "+inf");
// 3.便利指定的值并且带上标志(从小到大排序)
List<Tuple> zSet1 = jedis.zrangeByScoreWithScores("zSet", "0", "1000");
// 4.移除元素
long sRem = jedis.srem("zSet", "Hello1");
// 5.获取元素数量
long zSet2 = jedis.zcard("zSet");
// 6.获取全部元素倒叙排列
List<String> zSet3 = jedis.zrevrange("zSet", 0, -1);
// 7.获取指定区间的成员数量
long zSet4 = jedis.zcount("zSet", 1, 3);
}
}
事务
Jedis中操作事务:
import org.json.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* Redis事务测试
*/
public class RedisSW {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("test1","Hello");
jsonObject.put("test2","World");
//开启事务
Transaction multi = jedis.multi();
try {
multi.set("test1",jsonObject.toString());
multi.set("test2",jsonObject.toString());
System.out.println(1/0); //代码抛出异常事务结束失败
multi.exec(); //执行事务
}catch (Throwable error){
multi.discard(); //放弃事务
error.printStackTrace();
}finally {
System.out.println(jedis.get("test1"));
System.out.println(jedis.get("test2"));
multi.close();
}
}
}
8.SpringBoot整合
SpringBoot操作数据: Spring-data jpa jdbc mongodb redis!
SpringData也是和SpringBoot齐名的项目!
说明: 在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce
jedis: 采用的直连,多个线程操作的话是不安全的,如果想要避免不安全的使用,使用jedis pool 连接池! BIO模式!
lettuce: 采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
Maven依赖:
<-- SpinrBoot中Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果想要寻找redis的文件可以进行查看源码,在依赖项中找到
org.springframework.boot:spring-boot-autoconfigure:x.x.x
如图:
找到它的配置文件:
点开Ctrl+F搜索Redis就可以找到他的配置文件地址:
点击进入它的类就可以找到导入的Redis配置文件了:
如:
源码分析:
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") //我们可以自定义一个redisTemplate
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的RedisTemplate 没有过多的设置,Redis对象都是需要序列化!
//两个泛型都是 Object,Object的类型,我们一般使用需要强制转换<Strng,Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean //由于Spring是Redis中最常用的类型,所以单独提出来了一个bean!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
测试
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置yml
server: port: 8080 spring: redis: host: 127.0.0.1 database: 1
-
测试:
package com.example.redisstudy02springboot; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; @SpringBootTest class RedisStudy02SpringBootApplicationTests { @Autowired RedisTemplate<Object, Object> redisTemplate; @Test void contextLoads() { /** * redisTemplate 操作不同的数据类型 * opsForValue 操作字符串数据类型 * opsForList 操作List数据类型 * opsForSet 操作Set数据类型 * opsForHash 操作Hash数据类型 * opsForZSet 操作zset数据类型 * opsForGeo 操作地图数据类型 * opsForHyperLogLog 操作Hyperloglog数据类型 * * 除了基本的操作我们常用的方法也可以通过redisTemplate进行直接操作 */ // 获取Redis的连接对象 RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.flushDb(); // 进行操作 redisTemplate.opsForValue().set("name","xiaobu"); } }
如果存储对象需要进行序列化
注意我们所有存储的对象都需要序列化,如果版本较老的Redis会抛出未序列化的异常,较新的版本则使用默认的的序列化
也可以通过writeValueAsString手动序列化如:
@Test
void test1() throws JsonProcessingException {
//一般的开发使用Json来传递对象
User user = new User("小步", 16);
//序列化对象
String s = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
通用自定义RedisTemplate
//自定义 redisTemplate
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
//json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//取objectmapper做转译器
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 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);
return template;
}
在企业中开发一般会写一个RedisUtil将基本常用的方法都封装在其中在注入Spring进行操作!
9.RedisConfig详解
Redis启动时就通过Redis配置文件来进行启动!
单位
- 配置文件unit单位对大小写不敏感
包含
可以把多个配置文件配置进来,同Spring Improt类似include
网络
bind 127.0.0.1 #绑定的ip地址可写*表示所有
protected-mode yes #保护模式
port 6379 #Redis端口配置
通用 GENERAL配置
daemonize yes # 以守护线程的方式运行默认是no,我们需要自己开启为yes!
pidfile /var/run/redis_6379.pid #如果以后台方式运行就需要指定一个pid文件
# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice #设置日志级别
logfile "" #日志的文件位置名
databases 16 #数据库数量 默认是16个数据库
always-show-logo yes #是否总是显示Redis的logo
快照
持久化,在规定时间内,执行了多少次操作,则会持久化到文件 .rdb .aof
redis是内存数据库,如果没有持久化,那么数据断电及失!
# 表示如果在3600秒内有一个key进行了操作,300秒内有一个100进行了操作,60秒内有一个10000进行了操作则进行持久化操作
save 3600 1 300 100 60 10000
# 之后会进行测试!
stop-writes-on-bgsave-error yes #表示持久化出错了还是否需要继续工作
rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源
rdbchecksum yes #保存rdb文件时,进行错误的检查校验!
dir ./ #rdb 文件保存的目录
SECURITY 安全
可以在这里设置redis的密码
config set requirepass "密码" # 在redis-cli设置密码 则下次登录就需要验证了
auth 98526 #进行验证密码
通过配置文件进行修改:
requirepass 98526 #设置密码
CLIENTS(客户机)
maxclients 10000 #设置客户机的最大连接数量
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模式 aof配置
appendonly no #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况xia,rdb完全够用!
appendfilename "appendonly.aof" #持久化文件的名字
# appendfsync always # 每次修改都会sync.消耗性能
appendfsync everysec # 每秒执行一次 sync ,可能会丢失这ls的数据!
# appendfsync no # 不执行 sync,这个时候操作系统自己同步数据
10.Redis持久化
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据状态也会消失.所以Redis提供了持久化功能!
RDB
什么是RDB
在主从复制中,rdb就是备用了!从机上面!
在指定时间间隔内将内存中的数据集写入磁盘,也就是Snapshot快照,它恢复时是将文件直接读到内存里
Redis会单独创建(fork)一个子进程来进行持久化,会先将书局写入到一个临时文件中,待持久化过程都结束了。再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF更加高效,RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置。
RDB保存的文件是dump.rdb都是在我们配置文件中的快照中进行配置的
我们进行删除它然后进行修改配置文件自定义dump保存机制则:
重新启动服务之后,就可以成功了!
触发机制
- save的规则满足的情况下会自动触发rdb规则
- 执行flushall命令,也会触发我们的rdb规则!
- 退出redis,也会产生rdb文件!
备份就自动生成一个 dump.rdb
如何恢复rdb文件!
-
只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据!
-
查看需要存在的位置
config get dir "dir" "/usr/local/bin" #如果在这个目录下存在dump.rdb文件启动时会自动恢复其中的数据
几乎就他自己默认的配置就够用了,但是我们还需要去学习!
优点:
- 适合大规模的数据恢复!
- 对数据完整性要不高!
缺点:
- 需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有了!
- gork进程的时候,会占用一定的内容空间!
AOF(Append Only File)
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部都在执行一遍!
以日志的形式来记录每个写操作,将Redis执行过程的所有指令记录下来(读操作不记录)只许追加文件但不可以改写文件,redis启动之初会读取该文件重新 构建的数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
Aof保存的是 appendonly.aof 文件
默认是不开启的,我们需要手动进行配置! 我们只需要将appendonly改为yes就开启了aof !重启,redis 就可以生效了!
aof默认就是文件的无限追加,文件会越来越大!
如果aof文件大于64m,太大了! fork一个新的进程来将我们的文件进行重写!
如果这时redis有错误,redis是启动不起来的,我们需要修复这个aof文件,redis给我们提供了一个工具redis-check-aof
使用如下:
redis-check-aof --fix aof文件路径
优点缺点
优点:
- 每一次修改都同步,文件的完整会更加好!
- 每秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高的!
缺点:
- 相对于数据文件来说,aof远远大于rdb,修复速度也比rdb慢!
- Aof运行效率也比rdb慢,所以我们redis默认的配置是rdb而不是aof!
扩展:
- RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
- AOF
- AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重
写,使得AOF文件的体积不至于过大。 - 只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
- 同时开启两种持久化方式
- 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
- RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。
- 性能建议
- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这条规则。
- 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite
的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。 - 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。
11.发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式: 发送者(pub)发送消息,订阅者(sub)接受消息.微信,博客,关注系统!
Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图:
第一个:消息发送者, 第二个:频道 第三个:消息订阅者!
下图展示了channel1,以及订阅这个频道的三个客户端 – client2 , client5 和 client1 之间的关系!
当有新消息通过 PUBLISH命令发送给频道 channel1时,这个消息会被发送给订阅他的三个客户端
命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播,实时提醒等.
测试
# 订阅一个频道
subscribe xiaobu #表示订阅了xiaobu的频道
# 频道发送消息
publish xiaobu test
原理
Redis是使用c实现的,通过分析Redis源码中的pubsub.c文件,了解发布和订阅机制的底层实现,借此加深了对Redis的理解.Redis通过PUBLISH,SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能.
通过SUBSCRIBE命令订阅某个频道后,redis-server里维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端.SUBSCRI命令的关键,就是将客户端添加到给定channel的订阅链表中.
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者.
Pub/Sub从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息.这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能.
12.Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器.前者称为主节点(master/leader),后者称为从节点(slave/follower); 数据的复制是单向的,只能由主节点到从节点.Master以写为主,Slave以读为主.
默认情况下,每台Redis服务器是主节点;且一个节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点.
主从复制作用主要包括:
- 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式.
- 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速故障恢复;实际上是一种服务的冗余.
- 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读取服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的情况下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量.
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的急促,因此说主从复制是Redis高可用的基础.
一般来说,要将Redis运用与工程项目中,只使用一台Redis是万万不能的,原因如下:
-
从结构上来说,单个Redis服务器会发生单点故障,并且一台服务器要处理所有的请求负载,压力较大;
-
从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G.
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点就是"读多少写".
对于这种场景我们可以使用如下架构:
主从复制, 读写分离! 80% 的情况下都是在进行读操作!减缓服务器压力!架构中经常使用!
环境配置
只配置从库,不用配置主库!
127.0.0.1:6379> info replication #查看当前库信息
# Replication
role:master #角色 master
connected_slaves:0 #没有从机
master_failover_state:no-failover
master_replid:b7028ee91a76d38a056a6cc41e4a288a89fa3ada
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
复制三个配置文件然后修改对应信息:
- 端口
- pid名字
- log文件名字
- dump.rdb名字
修改完成后启动服务集群,通过进程信息查看
一主二从
默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了!
如需需要从,我们只需要进行认老大就好了! 一主(79) 二从(80,81)
# 表示认127.0.0.1的6379为老大,则6379就有了一个从节点
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
# 也可以通过修改配置文件中的值进行配置
masterauth <ip> <host> #配置老大的地址
这里可以分别用命令进行配置,但真实的主从配置应该在配置文件中配置,这样的话是永久的.我们这里使用的是命令!是暂时的!
细节
主机可以写,从机不能写只能读!主机中所有的信息和数据都会自动被从机保存!
如果主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候主机如果回来了,从机依旧可以直接获取到主机写的信息!
如果使用的是命令行,来配置的主从,这个时候如果重启了,就会变回主机,只要变回从机立马就会获取主机的信息!
复制原理
Slave启动成功连接到master后会发送一个sync命令
Master接到命令,启动后台的存盘进程,同时收集所有接受到的用于修改数据集的命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步.
全量复制:而slave服务在接受到数据库文件数据后,将其存盘并加载到内存中.
增量复制:Master继续将新的所有收集到的修改命令以此传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
层层链路
上一个M连接下一个S!
这时候也可以完成我们的主从复制!
如果没有老大了,这个时候能不能选择一个老大出来呢? 手动!
谋朝篡位
如果主机断开了连接我们可以使用 slaveofno one命令让自己变成主机!其他的节点就可以手动连接到最新的这个主节点(手动)!如果这个时候老大修复了那就只能重新配置!
哨兵模式
(自动选举老大的模式)
概述
主从切换技术的方法是: 当主服务器宕机后,需要手动把一台服务器切换为主服务器,这就需要人工干预,费事,费力,还会造成一段时间内服务器不可用.这不是一种推荐的方式,更多时候,我们优先虑哨兵模式.Redis从2.8开始正式提供了Sentionel(哨兵)架构来解决这个问题
谋朝篡位的自动版,能够后台监视主机是否故障,如果故障了更具投票数,自动将从库转换为主库.
哨兵模式是一种特殊的模式,首先Reis提供了哨兵命令,哨兵是一个独立的进程,作为进程他会独立运行,其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例.
这里的哨兵有两个作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器.
- 当哨兵检测到master宕机会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让他们切换主机.
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此我们可以使用多个哨兵进行监控.各个哨兵之间还会进行监控,这样就形成了多哨兵模式.
假设主服务器宕机哨兵1先检测这个结果,系统并不会马上进行failover(故障转移)过程,仅仅是哨兵1主观的认为服务器不可用,这个现象称为主观下线.当后面的哨兵也检测到了主服务器不可用,并且数量达到一定值时,那么哨兵之间会进行一次投票,投票结果由一个哨兵发起,进行failover[故障转移]操作.切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机.这个过程称为客观下线.
测试!
我们现在的状态是一主二从!
-
配置哨兵配置文件sentinel.conf
#sentinel(哨兵) monitor(监视器) myxiaobu(哨兵名) 12.0.0.1(监视主机) 6379(监视端口) 1(如果挂了给谁投票) sentinel monitor myxiaobu 12.0.0.1 6379 1
-
启动哨兵
# 启动哨兵模式 redis-sentinel xiaobu_config/sentinel.conf 3934:X 03 Oct 2022 15:12:17.064 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 3934:X 03 Oct 2022 15:12:17.064 # Redis version=7.0.4, bits=64, commit=00000000, modified=0, pid=3934, just started 3934:X 03 Oct 2022 15:12:17.064 # Configuration loaded 3934:X 03 Oct 2022 15:12:17.065 * monotonic clock: POSIX clock_gettime _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 7.0.4 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in sentinel mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 26379 | `-._ `._ / _.-' | PID: 3934 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | https://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' 3934:X 03 Oct 2022 15:12:17.065 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 3934:X 03 Oct 2022 15:12:17.070 * Sentinel new configuration saved on disk 3934:X 03 Oct 2022 15:12:17.070 # Sentinel ID is 8eaeb626bb3ebe18abeba81b9ca99ae98cb7116b 3934:X 03 Oct 2022 15:12:17.070 # +monitor master myxiaobu 12.0.0.1 6379 quorum 1 3934:X 03 Oct 2022 15:12:47.106 # +sdown master myxiaobu 12.0.0.1 6379 3934:X 03 Oct 2022 15:12:47.106 # +odown master myxiaobu 12.0.0.1 6379 #quorum 1/1 3934:X 03 Oct 2022 15:12:47.106 # +new-epoch 1 3934:X 03 Oct 2022 15:12:47.106 # +try-failover master myxiaobu 12.0.0.1 6379 3934:X 03 Oct 2022 15:12:47.108 * Sentinel new configuration saved on disk 3934:X 03 Oct 2022 15:12:47.108 # +vote-for-leader 8eaeb626bb3ebe18abeba81b9ca99ae98cb7116b 1 3934:X 03 Oct 2022 15:12:47.108 # +elected-leader master myxiaobu 12.0.0.1 6379 3934:X 03 Oct 2022 15:12:47.108 # +failover-state-select-slave master myxiaobu 12.0.0.1 6379 3934:X 03 Oct 2022 15:12:47.170 # -failover-abort-no-good-slave master myxiaobu 12.0.0.1 6379 3934:X 03 Oct 2022 15:12:47.254 # Next failover delay: I will not start a failover before Mon Oct 3 15:18:47 2022
如果Master节点断开了,这个时候就会从从机中选择一个服务器!(这里面有一个投票算法!)
哨兵日志:
如果主机此时回来了,只能归并到新的主机下,当作从机,这就是哨兵规则.
哨兵模式
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
- Redis不好在线扩容的.集群一旦达到上限,在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
哨兵模式的全部配置!
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
13.Redis缓存穿透和雪崩
服务的高可用问题~
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面.但同时,它也带来了一些问题.其中,最要害的问题,就是数据的一致性问题,从严格意义上来讲,这个问题无解.如果对数据的要求性很高,那么就不能使用缓存.
另外就是一些典型的问题,缓存穿透,缓存雪崩和缓存击穿.目前也有比较流行的解决方案.
缓存穿透(查不到)
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现Redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询.发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层的数据库.这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透.
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的压力查询;
但是这种方法存在两个问题:
- 如果空值能够被存储起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多空值的键;
- 即时对空值设置了过期时间,还是会存在缓存层和存储层数据会有一段时间窗口的不一致,这对需要保持一致性的业务会有影响.
缓存击穿(量太大,缓存过期!)
概述
这里需要注意和缓存击穿的区别,缓存击穿是指一个key非常热点,在不停的抗这大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大量并发就穿破缓存,直接请求数据库,就像在屏幕上凿开了一个洞.
当某个key在过期的瞬间,有大量请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库压力瞬间过大.
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题.
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可.这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大.
缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效.Redis宕机!
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
- redis高可用
- 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
- 限流降级
- 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 数据预热
- 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。