【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili笔记资料交流都在我们的平台:www.kuangstudy.com秦疆老师Java全栈系列课程之Redis讲解从Nosql聊起,深入redis基本类型使用,拓展特殊类型连接jedis,使用springboot集成,上手实战开发事务、配置文件详解、发布订阅、持久化机制、主从复制、哨兵模式、缓存穿透和雪崩处理给你带来最全面的Redis讲解,学Redis,这一套课就够了!狂神说Java系列,努力打造通俗易, 视频播放量 1788422、弹幕量 59113、点赞数 41815、投硬币枚数 51385、收藏人数 50991、转发人数 3471, 视频作者 遇见狂神说, 作者简介 编程是爱好,恭喜你发现宝藏男孩一枚~希望你们关注我是因为喜欢我,嘿嘿! 微信:kuangshenya 没事勿扰,不接商单,相关视频:【尚硅谷】Redis 6 入门到精通 超详细 教程,尚硅谷超经典Redis教程,redis实战,阳哥版从入门到精通,JAVA培训班出来入职两个月,我被开了,,【尚硅谷】Golang入门到实战教程丨一套精通GO语言,当男程序员和女程序员开始交往后.....,【狂神说Java】Docker最新超详细版教程通俗易懂,【狂神聊基金】最通俗易懂的基金理财教学,【狂神说Java】JavaScript最新教程通俗易懂,【狂神说Java】HTML5完整教学通俗易懂,【狂神说Java】Vue最新快速上手教程通俗易懂https://www.bilibili.com/video/BV1S54y1R7SB?p=1&vd_source=0bf649a191ae6a8e0d99c90c680f7c61 笔记是根据上述视频结合自身理解进行整理(视频讲解通俗易懂,建议有一点Linux知识,学习起来更加容易上手)。
前言:
一、NoSQL概述
1、为什么要用NoSQL ?
2、什么是Nosql
3、Nosql特点
4、Nosql的四大分类
编辑
二、Redis入门
1、概述
2、Redis能干什么
3、Redis的特点
4、环境搭建
5、redis性能测试
6、Redis基础知识(必须熟练掌握)
三、五大数据类型(熟练掌握为后面编程打下基础)
1、Redis-key(键值对)
2、String(字符串)
3、List(列表)
4、Hash(哈希)
5、Zset(有序集合)
四、三种特殊数据类型
1、Geospatial(地理位置)
2、Hyperloglog(基数统计)
3、BitMap(位图)
五、事务
六、Redis监控(watch)
七、Jedis
八、SpringBoot整合Redis
九、自定义Redis工具类
十、Redis.config
十一、Redis持久化
1、RDB(重点)
什么是RDB?
工作原理:
触发机制:
如何恢复rdb文件
优点:
缺点:
2、AOF(重点)
什么是AOF?
工作原理:
如何使用?
配置文件说明:
优点:
缺点:
扩展:
3、如何选择 RDB和AOF
十二、Redis发布与订阅
十三、Redis主从复制
1、概念
2、作用
3、为什么使用集群
4、为什么要应用主从复制
5、环境配置
6、一主二从配置
测试:
十四、哨兵模式(自动投票选举老大)
1、概述
2、哨兵模式的作用
3、优点:
4、缺点:
5、 哨兵模式的全部配置!
十五、缓存穿透
什么是缓存穿透?
解决方案:
1、布隆过滤器
2、缓存空对象
十六、缓存击穿(访问量太大,缓存过期!)
1、概念
2、解决方案
十七、缓存雪崩
1、概念
2、解决方案:
(1)redis高可用
(2)限流降级
(3)数据预热
为了拿捏 Redis 数据结构,我画了 40 张图(完整版)火力全开!https://mp.weixin.qq.com/s/MGcOl1kGuKdA7om0Ahz5IA
1、90年代,一个网站的访问量一般不会太大,单个数据库完全够用,数据库也没有什么压力。但是,现在我们处于大数据时代;大数据顾名思义数据量很多,那么应用传统的关系型数据库就有可能会出现如下问题:
- 数据量增加到一定程度,单机数据库就放不下了。
- 数据的索引(B+ Tree),一个机器内存也存放不下。
- 访问量变大后(读写混合),一台服务器承受不住。
只要发生如上情况之一,那么我们的数据库就必须要晋级
2、Memcache(缓存) + Mysql + 垂直拆分(读写分离)
80%的网站都是在进行读操作,这样的话每次都要去数据库查询就非常麻烦,同时为了能够减轻数据库、服务器的压力,我们可以使用缓存来保证效率!
缓存的出现经历了以下几个过程:
- 优化数据库的数据结构和索引(难度大)。
- 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了。
- MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,这样我们的访问效率就会大大提升。
3、分库分表 + 水平拆分 + MySQL集群
早些年MyISAM: 表锁,十分影响效率!高并发下就会出现严重的锁问题;
于是转战Innodb:行锁
慢慢的就开始使用分库分表来解决写的压力! MySQL 在哪个年代推出 了表分区!这个并没有多少公使用!
MySQL 的 集群,很好满足哪个年代的所有需求!
4、如今最近的年代
MySQL 等关系型数据库就不够用了!数据量很多,变化很快!MySQL 有的使用它来存储一些比较大的文件,博客,图片!数据库表很大,效率就比较低了!
如果有一种据库来专门处理这种数据,MySQL压力就变得十分小(研究如何处理这些问题!)大数据的IO压力下,表几乎没法更大! Nosql的出现就是一种非常好的选择!
5、目前一个基本的互联网项目!
用户的个人信息,社交网络,地理位置,自己产生的数据,日志等等爆发式增长!传统的关系型数据库已无法满足这些数据处理的要求,这时我们就需要使用NoSQL数据库,它可以很好的处理上述的情况!
NoSQL = Not Only SQL(不仅仅是SQL)-----泛指非关系型数据库
这里有两个概念:关系型数据库和菲关系型数据库
NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。
- 方便扩展(数据之间没有关系,很好扩展!)
- 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的是缓存记录级的,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样型的!(不需要事先设计数据库,随取随用!如果是数据量十分大的表,很多人很难根据三大范式将其完整的设计出来)
传统的 RDBMS(关系型) 和 NoSQL
传统的 RDBMS(关系型数据库):
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 :行+列
- 操作,数据定义语言
- 严格的一致性
- 基础的事务操作
- ...
Nosql:
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展
- ...
了解:3V + 3高
大数据时代的3V :主要是描述问题的
- 海量Velume
- 多样Variety
- 实时Velocity
大数据时代的3高 : 主要是对程序的要求
- 高并发
- 高可扩
- 高性能
真正在公司中的实践:NoSQL + RDBMS(关系型) 一起使用才是最强的。
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且还实现了master-slave(主从)同步。
Windows下安装非常简单,这里就不过多赘述,不过windows版本的Redis已经停更很久了,推荐使用Linux服务器进行学习。这里简单说一下Linux服务器环境下如何安装配置redis环境。
(1)去官网下载安装包,下载最新版即可(redis-7.0.2.tar.gz)
(2)解压Redis的安装包!程序一般放在/opt目录下。
(3)安装基本环境
[root@zlk opt]# yum install gcc-c++
# 然后进入redis目录下
[root@zlk opt]# cd redis-7.0.2
# 执行make命令
[root@zlk redis-7.0.2]# make
# 然后执行make install
[root@zlk redis-7.0.2]# make install
(4)redis默认安装路径 /usr/local/bin
(5)在该目录下创建一个文件夹 zlkconfig,将redis的配置文件复制到/usr/local/bin/zlkconfig
[root@zlk bin]# cp /opt/redis-7.0.2/redis.conf zlkconfig
(6)redis默认不是后台启动的,需要修改配置文件[root@zlk zlkconfig]# vim redis.conf
(7)通过刚刚新定义在 zlkconfig 里面的配置文件启动redis服务
(8)查看redis进程是否开启
[root@zlk bin]# ps -ef|grep redis
(9)关闭Redis服务 shutdown,并通过exit退出
在 /usr/local/bin 文件夹下存在Redis官方提供的性能测试工具 redis-benchmark;
基本命令:redis-benchmark [option] [option value]
可选参数如下所示:
序号 | 选项 | 描述 | 默认值 |
---|---|---|---|
1 | -h | 指定服务器主机名 | 127.0.0.1 |
2 | -p | 指定服务器端口 | 6379 |
3 | -s | 指定服务器 socket | |
4 | -c | 指定并发连接数 | 50 |
5 | -n | 指定请求数 | 10000 |
6 | -d | 以字节的形式指定 SET/GET 值的数据大小 | 2 |
7 | -k | 1=keep alive 0=reconnect | 1 |
8 | -r | SET/GET/INCR 使用随机 key, SADD 使用随机值 | |
9 | -P | 通过管道传输 |
1 |
10 | -q | 强制退出 redis。仅显示 query/sec 值 | |
11 | --csv | 以 CSV 格式输出 | |
12 | -l(L 的小写字母) | 生成循环,永久执行测试 | |
13 | -t | 仅运行以逗号分隔的测试命令列表。 | |
14 | -I(i 的大写字母) | Idle 模式。仅打开 N 个 idle 连接并等待。 |
测试:
100个并发连接 100000请求,实例中主机为 127.0.0.1,端口号为 6379;
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
(1)常用的基本操作命令
[root@zlk bin]# vim zlkconfig/redis.conf ,默认使用第0个。可以使用select n
切换到DB n,dbsize
可以查看当前数据库的大小,与key数量相关。
通过配置文件查看数据库的数量:
flushdb:清空当前数据库中的键值对。
flushall
:清空所有数据库的键值对。
(2)Redis是单线程的,并且是基于内存操作。所以说。redis所遇到的瓶颈并不是CPU,而是机器的内存和网络的带宽。
(3)在没有接触redis之前,我们大都会有这样的误区:
事实上并不完全是这样,我们的Redis是将所有的数据放在内存中。这样的话使用单线程去操作效率就是最高的,而多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。
keys * | 查看当前数据库所有的key |
exists key | 判断键是否存在 |
del key | 删除键值对 |
move key db | 将键值对移动到指定的的数据库 |
expire key second | 设置键值对过期时间 |
type key | 查看键值对中value的数据类型 |
rename key newkey | 修改key的名称 |
renamenx key newkey | 仅当newkey不存在时改名为newkey |
上述 是redis-key中相对来说比较常用的一些命令,更多命令学习可以参考:redis命令手册
append key value | 向指定key的value后追加字符串 |
decr/incr key | 将指定key的value数值进行+1/-1(value必须是数字的情况下) |
inerby/decrby key n | 按照指定的步长对数值进行加减 |
incrbyfloat key n | 为value数值加上浮点型数值 |
strlen key | 获取key保存值的字符串长度 |
getrange key start end | 按起止位置获取字符串(闭区间,起止位置都取) |
setrange key offset value | 用指定的value替换key中offset开始的值 |
getset key value | 将给定key的值设为value,并返回key的旧值 |
setnx key value | 仅当key不存在时进行set |
SETEX key seconds value |
set 键值对并设置过期时间 |
MSET key1 value1 [key2 value2..] |
批量set键值对 |
MSETNX key1 value1 [key2 value2..] |
批量设置键值对,仅当参数中所有的key都不存在时执行 |
MGET key1 [key2..] |
批量获取多个key保存的值 |
PSETEX key milliseconds value |
和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间 |
getset key value |
如果不存在值,则返回nil;反之,获取原来的值,并设置新的值 |
String中的value除了是字符串还可以是数字,它更加适合字符串存储,应用场景举例:
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。如下Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类。
----LPUSH---RPUSH---LRANGE--从左边/右边向列表中push值(1个或多个)----
#从左边/右边向列表中push值(1个或多个)
127.0.0.1:6379> LPUSH mylist k1 # LPUSH mylist=>{1}
(integer) 1 #返回push的值
127.0.0.1:6379> LPUSH mylist k2 # LPUSH mylist=>{2,1}
(integer) 2
127.0.0.1:6379> RPUSH mylist k3 # RPUSH mylist=>{2,1,3}
(integer) 3
127.0.0.1:6379> get mylist # 普通的get是无法获取list值的
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> LRANGE mylist 0 1 # LRANGE 获取起止位置范围内的元素【默认返回key】(索引从左往右递增)
1) "k2"
2) "k1"
127.0.0.1:6379> LRANGE mylist 0 -1 # 获取全部元素
1) "k2"
2) "k1"
3) "k3"
----LPUSHX---RPUSHX---向已存在的列名中push值(1个或多个)----
127.0.0.1:6379> LPUSHX list v1 # list不存在 LPUSHX失败
(integer) 0
127.0.0.1:6379> LPUSHX mylist k4 k5 # 向mylist中 左边 PUSH k4 k5
(integer) 5
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k5"
2) "k4"
3) "k2"
4) "k1"
5) "k3"
----LINSERT key before|after pivot value---在指定列表的元素后面插入value----
127.0.0.1:6379> LINSERT mylist after k2 name# 在k2元素后 插入name
(integer) 6
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k5"
2) "k4"
3) "k2"
4) "name"
5) "k1"
6) "k3"
----LLEN KEY---查看列表长度----
127.0.0.1:6379> LLEN mylist # 查看mylist的长度
(integer) 6
----LINDEX key index---通过索引获取列表元素----
127.0.0.1:6379> LINDEX mylist 3 # 获取下标为3的元素
"name"
127.0.0.1:6379> LINDEX mylist 0
"k5"
----LSET key index value---通过索引为元素设值----
127.0.0.1:6379> LSET mylist 3 k6 # 将下标3的元素 set值为k6
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k5"
2) "k4"
3) "k2"
4) "k6"
5) "k1"
6) "k3"
----LPOP--RPOP----从最左边/最右边移除值并返回----
127.0.0.1:6379> LPOP mylist # 左侧(头部)弹出
"k5"
127.0.0.1:6379> RPOP mylist # 右侧(尾部)弹出
"k3"
----RPOPLPUSH source destination---将列表的尾部最后一个值弹出并返回,然后加到另一个列表的头部----
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k4"
2) "k2"
3) "k6"
4) "k1"
127.0.0.1:6379> RPOPLPUSH mylist newlist # 将mylist的最后一个值(k1)弹出,加入到newlist的头部
"k1"
127.0.0.1:6379> LRANGE newlist 0 -1
1) "k1"
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k4"
2) "k2"
3) "k6"
----LTRIM key start end---通过下标截取指定范围内的列表----
127.0.0.1:6379> LTRIM mylist 0 1 # 截取mylist中的 0~1部分
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k4"
2) "k2"
# 初始 mylist: k2,k2,k2,k2,k2,k2,k4,k2,k2,k2,k2
----LREM key count value---list中是允许value重复的,count>0:从头部开始搜索,然后删除指定的value,最多删除count个,count<0:从尾部开始搜索;count=0:删除列表中所有的指定的value----
127.0.0.1:6379> LREM mylist 3 k2 # 从头部开始搜索 至多删除3个 k2
(integer) 3
# 删除后:mylist: k2,k2,k2,k4,k2,k2,k2,k2
127.0.0.1:6379> LREM mylist -2 k2 #从尾部开始搜索 至多删除2个 k2
(integer) 2
# 删除后:mylist: k2,k2,k2,k4,k2,k2
----BLPOP/BRPOP key1 [key2] timeout移出并获取列表中的第一个/最后一个元素,如果列表中没有元素会阻塞列表直到等待超时或发现可弹出元素为止----
----mylist初始key: k2,k2,k2,k4,k2,k2----
----newlist初始key: k1----
127.0.0.1:6379> BLPOP newlist mylist 30 # 从newlist中弹出第一个值,mylist作为候选
1) "newlist" # 弹出
2) "k1"
127.0.0.1:6379> BLPOP newlist mylist 30
1) "mylist" # 由于newlist空了 从mylist中弹出
2) "k2"
127.0.0.1:6379> BLPOP newlist 30
(30.10s) # 由于newlist空了,无可弹出的元素,请求超时
127.0.0.1:6379> BLPOP newlist 30 # 我们连接另一个客户端向newlist中push了test, 阻塞被解决。
1) "newlist"
2) "test"
(12.54s)
小结:
list实际上是一个链表,before after , left, right 都可以插入值
如果key不存在,则创建新的链表
如果key存在,则表示新增内容
如果移除了所有值,那么list就变成了空链表,也代表不存在
对于链表,在两边插入或者改动值,效率最高!修改中间元素,效率相对较低
集合中的元素是唯一的,这就意味着集合中不能出现重复的数据。在Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。Hash更适合于对象的存储。
----HSET key field value,将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个field会覆盖,返回0----
127.0.0.1:6379> HSET student name zlk # 将student作为一个对象,设置name为zlk
(integer) 1
127.0.0.1:6379> HSET student name zlk # 重复设置field进行覆盖,并返回0
(integer) 0
127.0.0.1:6379> HSET student age 20 # 设置student的age为20
(integer) 1
----HMSET key field1 value1 [field2 value2..],同时将多个 field-value (域-值)对设置到哈希表 key 中----
127.0.0.1:6379> HMSET student sex 1 tel 123456789 # 设置sex为1,tel为123456789
OK
----HSETNX key field value,只有在字段 field 不存在时,设置哈希表字段的值----
127.0.0.1:6379> HSETNX student name kuangshen # HSETNX 设置已存在的field
(integer) 0 # 失败
127.0.0.1:6379> HSETNX student email [email protected]
(integer) 1 # 成功
----HEXISTS key field,查看哈希表 key 中,指定的字段是否存在----
127.0.0.1:6379> HEXISTS student name # name字段在student中是否存在
(integer) 1 # 存在
127.0.0.1:6379> HEXISTS student addr
(integer) 0 # 不存在
----HGET key field value,获取存储在哈希表中指定字段的值----
127.0.0.1:6379> HGET student name # 获取student中name字段的value
"zlk"
----HMGET key field1 [field2..],获取所有给定字段的值----
127.0.0.1:6379> HMGET student name age tel # 获取student中name、age、tel字段的value
1) "zlk"
2) "20"
3) "123456789"
----HGETALL key,获取在哈希表key 的所有字段和值----
127.0.0.1:6379> HGETALL student # 获取student中所有的field及其value
1) "name"
2) "zlk"
3) "age"
4) "20"
5) "sex"
6) "1"
7) "tel"
8) "123456789"
9) "email"
10) "[email protected]"
----HKEYS key,获取哈希表key中所有的字段----
127.0.0.1:6379> HKEYS student # 查看student中所有的field
1) "name"
2) "age"
3) "sex"
4) "tel"
5) "email"
----HLEN key,获取哈希表中字段的数量----
127.0.0.1:6379> HLEN student # 查看student中的字段数量
(integer) 5
----HVALS key,获取哈希表中所有值----
127.0.0.1:6379> HVALS student # 查看student中所有的value
1) "zlk"
2) "20"
3) "1"
4) "123456789"
5) "[email protected]"
----HDEL key field1 [field2..],删除哈希表key中一个/多个field字段----
127.0.0.1:6379> HDEL student sex tel # 删除student 中的sex、tel字段
(integer) 2 #返回删除的字段个数
127.0.0.1:6379> HKEYS student
1) "name"
2) "age"
3) "email"
----HINCRBY key field n,为哈希表 key 中的指定字段的整数值加上增量n,并返回增量后结果 一样只适用于整数型字段----
127.0.0.1:6379> HINCRBY student age 1 # student的age字段数值+1(仅用于数值)
(integer) 21
127.0.0.1:6379> HINCRBY student name 1 # 非整数字型字段不可用
(error) ERR hash value is not an integer
----HINCRBYFLOAT key field n----为哈希表 key 中的指定字段的浮点数值加上增量 n----
127.0.0.1:6379> HINCRBYFLOAT student weight 0.6 # weight字段增加0.6
"90.8"
每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。如果score相同的情况下则按照字典顺序排序。有序集合的成员是唯一的,但分数(score)却可以重复。
----ZADD key score1 member1 [score2 member2],向有序集合中添加一个或多个成员,或者更新已存在的成员分数----
127.0.0.1:6379> ZADD myzset 1 m1 2 m2 3 m3 # 向有序集合myzset中添加成员m1 score=1 以及成员m2 score=2......
(integer) 3
----ZCARD key,获取有序集合的成员数----
127.0.0.1:6379> ZCARD myzset # 获取有序集合的成员数
(integer) 3
----ZCOUNT key min max,计算在有序集合中指定区间score的成员数----
127.0.0.1:6379> ZCOUNT myzset 0 1 # 获取score在 [0,1]区间的成员数量
(integer) 1
127.0.0.1:6379> ZCOUNT myzset 0 2
(integer) 2
----ZINCRBY key n member,有序集合中对指定成员的分数加上增量 n----
127.0.0.1:6379> ZINCRBY myzset 5 m2 # 将myzset中成员m2的score +5
"7"
----ZSCORE key member,返回有序集中,成员的分数值----
127.0.0.1:6379> ZSCORE myzset m1 # 获取myzset中成员m1的score
"1"
127.0.0.1:6379> ZSCORE myzset m2
"7"
----ZRANK key member,返回有序集合中指定成员的索引----
127.0.0.1:6379> ZRANK myzset m1 # 获取成员m1的索引,索引按照score排序,score相同索引值按字典顺序顺序增加
(integer) 0
127.0.0.1:6379> ZRANK myzset m2
(integer) 1
----ZRANGE key start end,通过索引区间返回有序集合成指定区间内的成员----
127.0.0.1:6379> ZRANGE myzset 0 1 # 获取索引在 0~1的成员
1) "m1"
2) "m2"
127.0.0.1:6379> ZRANGE myzset 0 -1 # 获取全部成员
1) "m1"
2) "m2"
3) "m3"
----ZRANGEBYLEX key min max,通过字典区间返回有序集合的成员----
#testset=>{abc,add,amaze,apple,back,java,redis} score均为0
127.0.0.1:6379> ZRANGEBYLEX testset - + # 返回所有成员
1) "abc"
2) "add"
3) "amaze"
4) "apple"
5) "back"
6) "java"
7) "redis"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 0 3 # 分页 按索引显示查询结果的 0,1,2条记录
1) "abc"
2) "add"
3) "amaze"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 3 3 # 显示 3,4,5条记录
1) "apple"
2) "back"
3) "java"
127.0.0.1:6379> ZRANGEBYLEX testset (- [apple # 显示 (-,apple] 区间内的所有成员
1) "abc"
2) "add"
3) "amaze"
4) "apple"
127.0.0.1:6379> ZRANGEBYLEX testset [apple [java # 显示闭区间 [apple,java]字典区间的所有成员
1) "apple"
2) "back"
3) "java"
----ZRANGEBYSCORE key min max,通过分数返回有序集合指定区间内的成员-inf 和 +inf分别表示最小最大值----
127.0.0.1:6379> ZRANGEBYSCORE myzset 1 10 # 返回score在 [1,10]之间的的成员
1) "m1"
2) "m2"
3) "m3"
127.0.0.1:6379> ZRANGEBYSCORE myzset 1 5
1) "m1"
2) "m2"
----ZLEXCOUNT key min max,在有序集合中计算指定字典区间内成员数量----
127.0.0.1:6379> ZLEXCOUNT testset - +
(integer) 7
127.0.0.1:6379> ZLEXCOUNT testset [apple [java
(integer) 3
----ZREMZREM key member1 [member2..],移除有序集合中一个/多个成员----
127.0.0.1:6379> ZREM testset abc # 移除成员abc
(integer) 1
----ZREMRANGEBYLEX key min max,移除有序集合中给定的字典区间的所有成员----
127.0.0.1:6379> ZREMRANGEBYLEX testset [apple [java # 移除字典区间[apple,java]中的所有成员
(integer) 3
----ZREMRANGEBYRANK key start stop,移除有序集合中给定的排名区间的所有成员----
127.0.0.1:6379> ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成员
(integer) 2
----ZREMRANGEBYSCORE key min max,移除有序集合中给定的分数区间的所有成员----
127.0.0.1:6379> ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成员
(integer) 2
----ZREVRANGE key start end,返回有序集中指定区间内的成员,通过索引,分数从高到底----
# testset=> {abc,add,apple,amaze,back,java,redis} score均为0
# myzset=> {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)}
127.0.0.1:6379> ZREVRANGE myzset 0 3 # 按score递减排序,然后按索引,返回结果的 0~3
1) "m9"
2) "m7"
3) "m4"
4) "m3"
127.0.0.1:6379> ZREVRANGE myzset 2 4 # 返回排序结果的 索引的2~4
1) "m4"
2) "m3"
3) "m2"
----ZREVRANGEBYSCORRE key max min,返回有序集中指定分数区间内的成员,分数从高到低排序----
127.0.0.1:6379> ZREVRANGEBYSCORE myzset 6 2 # 按score递减顺序 返回集合中分数在[2,6]之间的成员
1) "m4"
2) "m3"
3) "m2"
----ZREVRANGEBYLEX key max min,返回有序集中指定字典区间内的成员,按字典顺序倒序----
127.0.0.1:6379> ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典区间的成员
1) "java"
2) "back"
3) "amaze"
4) "apple"
----ZREVRANK key member,返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序----
127.0.0.1:6379> ZREVRANK myzset m7 # 按score递减顺序,返回成员m7索引
(integer) 7
127.0.0.1:6379> ZREVRANK myzset m2
(integer) 4
----ZINTERSTORE destination numkeys key1 [key2 ..],计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中,numkeys:表示参与运算的集合数,将score相加作为结果的score----
# mathscore=>{(xm,90),(xh,95),(xg,87)} 小明、小红、小刚的数学成绩
# enscore=>{(xm,70),(xh,93),(xg,90)} 小明、小红、小刚的英语成绩
127.0.0.1:6379> ZINTERSTORE sumscore 2 mathscore enscore # 将mathscore enscore进行合并 结果存放到sumscore
(integer) 3 #返回sumscore中key的数量
127.0.0.1:6379> ZRANGE sumscore 0 -1 withscores # 合并后的score是之前集合中所有score的和
1) "xm"
2) "160"
3) "xg"
4) "177"
5) "xh"
6) "188"
----ZUNIONSTORE destination numkeys key1 [key2..],计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中----
127.0.0.1:6379> ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取两个集合的成员score最小值作为结果
(integer) 3
127.0.0.1:6379> ZRANGE lowestscore 0 -1 withscores
1) "xm"
2) "70"
3) "xg"
4) "87"
5) "xh"
6) "93"
应用案例:
使用经纬度定位地理坐标,并用一个有序集合Zset进行保存,GEO的底层实现原理就是Zset。
在这里我们先来增加一个经纬度的基本常识:
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
geoadd key longitud(经度) latitude(纬度) member [..] | 将具体的经纬度坐标存入一个有序集合 |
geopos key member [member..] | 获取集合中的1个/多个成员坐标 |
geodist key member1 member2 [unit] | 返回两个给定位置之间的距离。默认以米作为单位 |
georadius key longitude latitude radius m|km|mi|ft [withcoord] [withdist] [withhash] [COUNT count] | 以给定的经纬度为中心,返回集合包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素 |
georadiusbymember key member radius... | 功能与georadius相同,只是中心位置不是具体的经纬度,而是使用结合中已有的成员作为中心点 |
geohash key member1 [member2..] | 返回一个或多个位置元素的Geohash表示 |
注:指定单位的参数 unit 必须是以下单位的其中一个:
m :米。
km :千米。
mi :英里。
ft :英尺。
----getadd 添加地理位置----
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
# 127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin
# (error) ERR invalid longitude,latitude pair 39.900000,116.400000
#在这里我们先进行手动添加几个城市
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shengzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
----GEODIST ,查看两地之间的距离----
127.0.0.1:6379> GEODIST china:city beijing shanghai km # 查看上海到北京的直线距离
"1067.3788"
127.0.0.1:6379> GEODIST china:city beijing chongqing km # 查看重庆到北京的直线距离
"1464.0708"
----GEORADIUS 以给定的经纬度为中心, 找出某一半径内的元素----
----我附近的人? (获得所有附近的人的地址,定位!)就是通过半径来查询!----
----获得指定数量的人,200----
----所有数据应该都录入:china:city ,才会让结果更加准确----
----关于GEORADIUS的参数----
----通过georadius就可以完成 查找附近的人功能----
----withcoord:带上坐标----
----withdist:带上距离,单位与半径单位相同----
----COUNT n : 只显示前n个(按距离递增排序)----
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord withdist # 查询经纬度(120,30)坐标500km半径内的成员
1) 1) "hangzhou"
2) "29.4151"
3) 1) "120.20000249147415"
2) "30.199999888333501"
2) 1) "shanghai"
2) "205.3611"
3) 1) "121.40000134706497"
2) "31.400000253193539"
----GEORADIUSBYMEMBER 找出位于指定元素周围的其他元素,中心位置不是具体的经纬度,而是使用已有的成员作为中心点!----
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km
1) "beijing"
2) "xian"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"
----geohash 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!----
127.0.0.1:6379> geohash china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0
GEO 底层的实现原理其实就是 Zset!我们可以使用Zset命令来对其进行相关操作。
127.0.0.1:6379> ZRANGE china:city 0 -1 # 查看地图中全部的元素
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 移除指定元素!
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
基数就是数据集中不重复的元素的个数。举一个简单的例子:
简介:
应用场景:
127.0.0.1:6379> PFadd mykey a b c d e f g h i j # 创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey # 统计 mykey 元素的基数数量
(integer) 10
127.0.0.1:6379> PFadd mykey2 i j z x c v b n m # 创建第二组元素 mykey2
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 # 合并两组 mykey mykey2 => mykey3 并集
OK
127.0.0.1:6379> PFCOUNT mykey3 # 看并集的数量!
(integer) 15
BitMap可以用来干什么呢?
BitMap采用位存储,它是一连串的二进制数字,信息状态只有0和1,每一位所在的位置为(offset)
----setbit key offset value,为指定key的offset位设置值----
127.0.0.1:6379> setbit sign 0 1 # 设置sign的第0位为 1
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 设置sign的第2位为 1 不设置的话默认为0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> type sign
string
----getbit key offset,获取指定offset位的值----
127.0.0.1:6379> getbit sign 2 # 获取第2位的数值
(integer) 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 4 # 未设置默认是0
(integer) 0
----bitcount key [start end],统计字符串被设置为1的bit数,也可以指定统计范围按字节----
127.0.0.1:6379> BITCOUNT sign # 统计sign中为1的位数
(integer) 4
面试高频:
这里我们一定要知道Redis中的单条命令是保证原子性的,但是redis事务不能保证原子性。
Redis事务的本质:一组命令的集合!
- 一次性
- 顺序性
- 排他性
在Redis事务没有没有隔离级别的概念;所有的命令在事务中,并没有直接被执行!只有发起执行命令Exec的时候才会执行!
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰(排他)
Redis中事务的操作过程:
- 开启事务(multi)
- 命令入队(…)
- 执行事务(exec)
正常开启并执行事务:
----所有事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成----
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2 # ..
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事务执行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
2) "k2"
3) "k1"
取消事务:
127.0.0.1:6379> flushall
ok
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI # 当前未开启事务
127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行
(nil)
编译型异常(代码有问题, 命令有错!) ,事务中所有的命令都不会被执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 错误的命令(没有这个命令)
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec # 执行事务报错!
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # 所有的命令都不会被执行!
(nil)
运行时异常(1/0):如果事务队列中存在错误语法性,那么执行命令的时候,其他命令是可以正常执行的,只有错误命令会抛出异常!
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 执行的时候会失败,因为value不是数值!
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 虽然第一条命令报错了,但是
依旧正常执行成功了!
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
1、悲观锁:顾名思义就是做什么事情都很悲观,无论干什么都要上锁。
2、乐观锁:做什么事都很乐观,都不会去上锁;只有更新数据的时候去判断一下version,看一下在此期间是否有人修改过这个数据:
使用watch key
监控指定数据,相当于乐观锁加锁。
正常执行:
127.0.0.1:6379> set money 100 # 设置余额:100
OK
127.0.0.1:6379> set use 0 # 支出使用:0
OK
127.0.0.1:6379> watch money # 监视money (上锁)
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch可以当做redis的乐观锁上锁操作
(1)我们先正常启动一个客户端执行线程1
127.0.0.1:6379> watch money # money上锁
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> # 此时事务并没有执行,启动另外一个客户端插队
(2)启动另外一个客户端模拟线程插队的过程——线程2
127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money
(integer) 600
(3)回到线程1,执行事务:
127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败
(nil) # 没有结果,说明事务执行失败
127.0.0.1:6379> get money # 线程2 修改生效
"600"
127.0.0.1:6379> get use # 线程1事务执行失败,数值没有被修改
"0"
注意:
Jedis是Redis官方推荐使用的Java连接redis的客户端。所以我们在使用Java来操作redis时就要学习Jedis。
1、导入依赖
redis.clients
jedis
3.2.0
com.alibaba
fastjson
1.2.70
2、编码测试
操作命令(测试连接):
public class TestPing {
public static void main(String[] args) {
// 1、 new Jedis 对象即可
Jedis jedis = new Jedis("127.0.0.1",6379);
// jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要!
System.out.println(jedis.ping()); //PONG
}
}
事务相关举例:
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","kuangshen");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString()
// jedis.watch(result)
try {
multi.set("user1",result);
multi.set("user2",result);
int i = 1/0 ; // 代码抛出异常事务,执行失败!
multi.exec(); // 执行事务!
} catch (Exception e) {
multi.discard(); // 放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); // 关闭连接
}
}
}
1、导入依赖
org.springframework.boot
spring-boot-starter-data-redis
这里要注意一下,springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。
二者的区别:
1、容量单位:配置文件 unit单位 对大小写不敏感!
2、包含 includes:就好比我们学习 Improt 引入
3、网络(NETWORK)
bind 127.0.0.1 # 绑定的ip
protected-mode yes # 保护模式,默认表示开启
port 6379 # 端口设置
4、通用(GENERAL)
5、快照
redis 是内存数据库,如果没有持久化,那么数据断电及失!在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb. aof(可以自己进行配置,一般来说默认即可)
# 如果900s内,如果至少有一个1 key进行了修改,我们就进行持久化操作
save 900 1
# 如果300s内,如果至少10 key进行了修改,我们就进行持久化操作
save 300 10
# 如果60s内,如果至少10000 key进行了修改,我们就进行持久化操作
save 60 10000
# 我们之后学习持久化,会自己定义这个进行测试,这里先来简单了解一下
stop-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作!
rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些cpu资源!
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验!
dir ./ # rdb 文件保存的目录!
6、APPEND ONLY 模式 aof配置
appendonly no # 默认是不开启aof模式的,默认使用rdb方式持久化,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof" # 持久化的文件的名字
# appendfsync always # 每次修改都会 sync。消耗性能
appendfsync everysec # 每秒执行一次 sync,可能会丢失这1s的数据!
# appendfsync no # 不执行 sync,这个时候操作系统自己同步数据,速度最快!
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化的功能。
在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据恢复。
默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。
fork()函数用于从一个已经存在的进程内创建一个新的进程,称为“子进程”。
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。
这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置!
有时候在生产环境我们会对这个文件进行备份!
rdb保存的文件默认是dump.rdb ,都是在我们的配置文件中进行配置的,一般不需要改动!
将我们执行的所有命令都记录下来,类似于history,恢复的时候就把这个文件全部再执行一遍!
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件而不可以改写文件,redis启动之初会读取该文件重新构建数据。换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
Aof保存的是 appendonly.aof 文件。
如果要使用AOF,需要修改配置文件:
aof 默认就是文件的无限追加,这样导致的结果就是文件会越来越大。
每一次修改都同步。
每秒同步一次,最多可能会丢失一秒的数据。
相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
aof运行效率也要比 rdb 慢,所以redis默认的配置就是rdb持久化!
rdb持久化能够在指定的时间间隔内对数据进行快照存储。
aof持久化记录每次对服务器的写操作,每当服务器重启时都会执行这些命令以此来恢复数据
aof方式追加每次写操作到文件的末尾,同时在redis配置文件中还设置了对aof文件进行后台重写,使得aof文件不至于过大。
同时开启两种持久化方式
这种情况下redis在重启时会先载入aof文件来恢复原始数据(因为aof文件保存的数据比rdb文件保存的数据更完整)
建议不要只开启aof持久化,因为rdb更适合用于数据库的备份,同时可以快速重启
性能建议
建议只开启rdb文件,而且只要15分钟备份一次就可以,只保留 save 900 1
如果使用aof,带来的好处是即使在最恶劣的情况下也不会丢失超过2秒的数据,在重启时只加载自己的aof文件即可。坏处是带来了持续的IO,二是aof重写过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的,只要硬盘许可,应该尽量减少aof重写的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。
如果不使用aof,仅用rdb实现高可用性也可以,这样可以省掉一大笔 IO,也减少了重写时带来的系统波动。代价是如果Master/Slave宕机,这样就会丢失十几分钟的数据。
Redis 发布订阅是一种消息通信模式:发送者发送消息,订阅者接收消息。
实用场景:
订阅/发布消息之间的消息通信模式如图所示:
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 ;当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
操作命令:
(1) 订阅端:
127.0.0.1:6379> SUBSCRIBE kuangshenshuo # 订阅一个频道 kuangshenshuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kuangshenshuo"
3) (integer) 1
# 等待读取推送的信息
1) "message" # 消息
2) "kuangshenshuo" # 订阅的频道名称
3) "hello,kuangshen" # 消息的具体内容
1) "message"
2) "kuangshenshuo"
3) "hello,redis"
(2)发送端:
127.0.0.1:6379> PUBLISH kuangshenshuo "hello,kuangshen" # 发布者发布消息到频道!
(integer) 1
127.0.0.1:6379> PUBLISH kuangshenshuo "hello,redis" # 发布者发布消息到频道!
(integer) 1
127.0.0.1:6379>
Redis发布与订阅原理:
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,如下图中结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
总结:通过 PUBLISH向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者; 客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
主从复制就是将一台Redis服务器复制到其他的服务器,前者被称为主节点(master/leader),后者可以称为是从节点(slave/follower)。数据的复制是单向的,只能master复制到slave,master以write为主,slave以read为主。
如果我们不进行任何设置的话,Redis服务器默认都是主节点,它可以有0个或多个从节点,但是每个从节点只能有一个主节点,也就是我们常说的1对多的关系。
如果将Redis运用于工程项目中,绝对不能只使用一台Redis,原因如下:
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。对于这种场景,我们可以使如下这种架构:
主从复制,可以简单理解为读写分离! 80% 的情况下都是在进行读操作!为了减缓服务器的压力!架构中经常使用! 一主二从!只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!
只配置从节点,不用配置主节点,先来查看一下当前库的信息:
127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master # 角色 master
connected_slaves:0 # 没有从机
master_replid:b63c90e6c501143759cb0e7f450bd1eb0c70882a
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
(1)复制3个配置文件,然后分别修改如下信息:
(2)修改完成之后启动我们的3个redis服务器,可以通过进程信息查看!
默认每台Redis服务器都是主节点; 所以只采用认老大的方式配置从机就好了。
配置从机:
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host 6379 找谁当自己的老大!
OK
127.0.0.1:6380> info replicatio
#Replication
role:slave # 当前角色是从机
master_host:127.0.0.1 # 可以的看到主机的信息
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a81be8dd257636b2d3e7a9f595e69d73ff03774e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
在主机中查看:
127.0.0.1:6379> info replication
#Replication
role:master
connected_slaves:1 # 多了从机的配置
slave0:ip=127.0.0.1,port=6380,state=online,offset=42,lag=1 # 多了从机的配置
master_replid:a81be8dd257636b2d3e7a9f595e69d73ff03774e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:42
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:42
如果两个都配置完了,就有两个从机
我们这里采用命令的方式对从机进行配置,是暂时的。真实的从主配置应该在配置文件中配置,这样的话是永久的。
主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!
主机写:
从机只能读:
此时将主机断开连接,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。也就是说主机断电后从机还是能够连接到主机进行读操作,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!
如果主机断开了连接,我们可以使用 SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的这个主节点(手动)!如果这个时候老大修复了,那就重新连接(手动)!
刚我们说过命令行的配置方式是暂时的,这个时候如果从机重启了,就会变回主机!需要重新进行手动配置。
永久配置在配置文件中如设置,如下所示:
在上述情况下,如果主机出现故障,不会自动出现新的主机,有两种方式可以产生新的主机:
slaveof no one
,这样执行以后从机会独立出来成为一个主机 在上面主从复制中我们学习了如何进行手动配置,但在实际的项目部署开发中我们肯定不能这么做。手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。
哨兵模式是一种特殊的模式,在redis中哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例,查看主机是否故障,如果故障了根据投票数自动将从库转换为主库。。
一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
图解:假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器切换主机,这个过程称为客观下线。
我们目前的状态是 一主二从正常启动的状态,下面进行测试!
配置哨兵配置文件sentinel.conf
核心配置:sentinel monitor mymaster 127.0.0.1 6379 1,后面的数字1代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!
# sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1
启动哨兵:
[root@zlk bin]# redis-sentinel zlkconfig/sentinel.conf
26607:X 31 Mar 2020 21:13:10.027 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
26607:X 31 Mar 2020 21:13:10.027 # Redis version=5.0.8, bits=64,
commit=00000000, modified=0, pid=26607, just started
26607:X 31 Mar 2020 21:13:10.027 # Configuration loaded
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.8 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 26607
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
26607:X 31 Mar 2020 21:13:10.029 # WARNING: The TCP backlog setting of 511
cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value
of 128.
26607:X 31 Mar 2020 21:13:10.031 # Sentinel ID is
4c780da7e22d2aebe3bc20c333746f202ce72996
26607:X 31 Mar 2020 21:13:10.031 # +monitor master myredis 127.0.0.1 6379 quorum
1
26607:X 31 Mar 2020 21:13:10.031 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @
myredis 127.0.0.1 6379
26607:X 31 Mar 2020 21:13:10.033 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @
myredis 127.0.0.1 6379
这时候将主机 shutdown,这时候哨兵就会发送一个心跳包机制,在从机中选一个服务器(这里面有一个投票的算法),我们来看一下哨兵日志:
如果说此时主机回来了,只能归并到新的主机下当做从机,这就是哨兵模式的规定则。
完整的哨兵模式配置文件 sentinel.conf(暂做了解即可)
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等)将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# shell编程
# sentinel notification-script
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
缓存穿透就是缓存没有命中(查不到)!
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库中没有,也就是说缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败;当用户很多时,缓存都没有命中(秒杀的时候),于是都去请求了持久层数据库,这就会给持久层数据库早曾很大的压力,这时候就相当于出现了缓存穿透。
布隆过滤器是一种数据结构,对所有可能查询的参数以hash的形式进行存储,在控制层先进性校验,不符合规则就丢弃,从而避免了对底层存储系统的查询压力;
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间(提前访问一遍,在缓存中存在空对象),之后再访问这个数据将会从缓存中获取,保护了后端数据源;
这种方法会存在两个问题:
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,这类数据一般是热点数据,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间(缓存过期),持续的大并发就穿破缓存,直接请求数据库并写回缓存,就像在一个屏障上凿开了一个洞,导使数据库瞬间压力过大。
缓存雪崩是指在某一个时间段,缓存集中过期失效。最常见的就是Redis 宕机!
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
从另一方面来说,其实集中过期带来的后果并不是非常的致命。比较致命的缓存雪崩是服务器的某个节点宕机或者断网。因为自然形成的缓存雪崩,一定是由于在某个时间段内集中创建缓存,这时数据库是可以顶得住压力的,无非就是对数据库产生周期性的压力而已。而缓存服务的某个节点宕机或者断网对数据库服务器造成的压力是不可预知的,很有可能会把数据库直接压垮。
该思想的含义是:既然Redis有可能会挂掉,那我就多加几台Redis,这样其中一台挂掉了之后其他的还可以继续工作,其实就是我们前面所说的搭建Redis集群(异地多活)
在缓存失败后,通过加锁或者队列来控制读取数据库写缓存的线程数量。比如对于某个key只允许一个线程去查询数据和写缓存,其他的线程进行等待。
数据预热就是在正是部署之前,我先把有可能会访问的数据先访问一遍,这样的话部分部分可能会大量访问的数据就会加载到缓存中。在即将发生大并发访问之前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀(而不是一下子全部过期)。
Redis底层数据结构分析 :