狂神说Redis:Redis五大基本数据类型,三种特殊数据类型,事务,Jedis,SpringBoot整合Redis,Redis持久化(RDB,AOF),Redis订阅,主从复制,哨兵模式缓存穿透雪崩

学习目的

Redis等关系型数据库作缓存去缓存使用频率高的数据,减轻Mysql等关系型数据库的压力

Nosql讲解

什么是NoSQL

NoSQL翻译为Not Only SQL,译为不仅仅是SQL,意指非关系型数据库,NoSQL(Not only SQL)是一种非关系型数据库管理系统的概念。相对于传统的关系型数据库,NoSQL数据库更加灵活, 它们不依赖于固定的模式(schema-less),可以存储和处理半结构化、非结构化和复杂的数据。NoSQL数据库可以处理大量的数据,提供了高扩展性和高性能,适合用于处理大规模的分布式数据存储和处理。

NoSQL数据库通常采用键值对存储(例如Redis),文档存储(例如MongoDB),列存储(例如Apache Cassandra),图形数据库(例如Neo4j)等不同的数据模型。每种数据模型都有其自身的优点和适用场景。

NoSQL数据库在大数据、实时数据分析、云计算以及需要快速读写和高可扩展性的应用场景中得到广泛应用,例如社交媒体、物联网、日志存储和实时数据处理等领域。

web2.0的诞生,传统的关系型数据库已经很难对付web2.0时代!特别是指大规模高并发社区!会出现很多问题,NoSQL在大数据时代发展的十分迅速,尤其是Redis

很多的数据类型用户的个人信息,社交网络,地理位置,这些数据类型的存储不需要一个固定的格式,不需要太多操作就可以实现横向拓展,就比如Redis,它是使用类似于Java的Map<String, Object>来实现存储,键值对的形式存储,这只是NoSQL的解决方式之一

为什么使用NoSQL

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

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

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

NoSQL 特点

1、方便扩展(数据之间没有联系可以快速拓展)

2、大数据量高性能,Redis可以支持8w的并发量(写操作),11w访问量(读操作),NoSQL的缓存记录级,是一种细粒度的缓存,性能比较高

3、数据类型多样性(不需要事先设计数据库,随取随用,数据量过大就无法设计

4、传统的关系数据库管理系统(Relational Database Management System:RDBMS)和NoSQL的区别

关系型数据库与非关系型数据库对比
传统的RDBMS(关系型数据库)

  • 结构化
  • SQL
  • 数据和关系存在于单独的表中 row(行) column(列)
  • 数据操作,数据定义语言
  • 严格的一致性
  • 基础的事务

NoSQL(非关系型数据库)

  • 不仅仅是数据
  • 没有固定的查询语言
  • 键值对存储,列存储,文档存储,图形数据库
  • 最终一致性
  • CAP定律和BASE理论

5、大数据时代的3V + 3高

  • 大数据时代的3V

    • 海量Volume
    • 多样Variety
    • 实时Velocity
  • 大数据时代的3高

    • 高并发

    • 高可用(随时水平拆分,机器不够了,随时扩展)

    • 高性能(保证用户体验和性能)

真正在公司中用到的实践,NoSQL + 关系型数据库,这是最强组合,也是阿里巴巴的架构演进

阿里巴巴架构演讲

实际 NoSql+关系型数据库 一起使用

# 商品的基本信息
名称、价格、商家信息
MySQL / OracleIOE化(IOEIBMOracleEMC存储设备)

# 商品描述
评论,文本信息多
文档型数据库,MongoDB

# 图片
分布式文件系统 FastDFS
淘宝:TFS
GoogleGFS
HadoopHDFS
阿里云:OSS

# 商品关键字(搜索)
搜索引擎 solr elasticsearch
淘宝:ISearchISearch作者,阿里的多隆

# 商品热门波段信息
内存数据库
Redis  Tair  Memcached...

# 商品交易,外部支付接口
第三方应用

Nosql四大分类

1、KV键值对

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

2、文档型数据库(Bson,Binary Json,二进制Json)

  • MongoDB,需要掌握,它是一种基于分布式文件存储的数据库,由C++编写,主要用来处理大量的文档

  • MongoDB 是一种介于关系型数据库和非关系型数据库之间的一种中间产品,功能丰富,而且MongoDB是NoSQL中最像关系型数据库的产品

  • ConthDB

3、列存储数据库

  • HBASE
  • 分布式文件系统

4、图形关系数据库

  • 它不是存图片的!它存放的是关系,就好比一个人的社交圈,可以动态扩充
  • Neo4j,InfoGrid

4种分类的对比
在这里插入图片描述

CAP

BASE

Redis入门

Redis简介

什么是Redis?

​ Redis(Remote Directory Server),中文译为远程字典服务,免费开源,由C语言编写,支持网络,可基于内存也可持久化的日志型,KV键值对数据库,并且提供多种语言的API,是当下NoSQL中最热门的技术之一!被人们称之为结构化数据库!

Redis能干嘛?

Redis是一种高性能的开源内存数据库,它具有多种功能和用途:

  1. 缓存:Redis常用于作为缓存层,将热门数据存储在内存中,以加快读取速度。它支持丰富的数据结构和灵活的缓存策略,可以用于加速访问数据库、API调用以及动态生成的内容。

  2. 数据库:Redis可以作为一种持久化存储解决方案,支持将数据保存在硬盘上,以便在重启或故障恢复后仍可使用。它提供键值对存储、哈希表和有序集合等数据结构的操作,可用于存储和查询结构化和半结构化数据。

  3. 消息队列:Redis提供发布/订阅模式,可以用作消息代理和消息队列。它允许不同的应用程序通过发布和订阅消息进行通信,支持多对多的消息传递机制,适用于实时通信、事件驱动架构和任务队列的场景。

  4. 实时统计与计数器:Redis支持高效地进行计数和聚合操作,可用于实时统计和计数。它提供了各种数据结构和操作方法,例如位图、HyperLogLog和有序集合,可以实现快速的数据统计和计数功能。

  5. 分布式锁:Redis支持原子性操作和分布式锁,可以用于实现分布式系统中的互斥操作和资源竞争控制,确保多个节点之间的并发安全性。

  6. 地理空间索引:Redis提供了地理空间索引的支持,可以存储和查询地理位置信息。它可以进行附近位置搜索、位置围栏查询和距离计算等功能,适用于地理位置相关的应用。

总而言之,Redis是一个功能丰富、灵活且高性能的数据库,适用于缓存、数据存储、消息队列、统计计数、分布式锁等各种场景。它的特点是快速、可扩展和易于使用,被广泛应用于Web应用、分布式系统和实时数据处理等领域。

特性

Redis具有以下主要特性:

  1. 高性能:Redis将数据存储在内存中,因此具有快速的读写速度。它使用了高效的数据结构和基于异步I/O的单线程事件驱动模型,能够处理高并发的请求。

  2. 数据结构丰富:Redis支持多种数据结构,包括字符串、哈希表、列表、集合和有序集合等。这些数据结构能够满足不同的应用需求,并且支持灵活的数据操作和丰富的数据类型。

  3. 持久化:Redis支持持久化数据存储,可以将数据保存在硬盘上,以便在重启或故障恢复后仍可使用。它提供了两种持久化方式:RDB(快照方式)和AOF(追加日志方式)。

  4. 发布与订阅:Redis提供了发布/订阅模式,支持多对多的消息传递机制。应用程序可以通过发布和订阅消息进行实时通信,这在消息队列和实时数据推送方面非常有用。

  5. 分布式:Redis支持数据分片和主从复制。通过分片,可以将数据分布到多个节点上,提高了系统的扩展性和负载均衡能力。主从复制则可以实现数据的备份和故障恢复。

  6. 原子性操作:Redis支持原子性操作,保证了多个操作的原子性执行。例如,在一个事务中可以对多个键进行操作,并且确保所有操作要么全部执行成功,要么全部回滚。

  7. 高可用性:Redis支持主从复制和哨兵机制,可以实现自动故障转移和节点的自动恢复,提高了系统的可用性。

  8. Lua脚本:Redis支持Lua脚本的执行,可以在服务端执行一段自定义的Lua代码,增加了灵活性和扩展性。

总的来说,Redis是一个快速、高性能且功能丰富的内存数据库。它以其简单易用的特性而著名,并被广泛应用于缓存、实时数据处理、分布式系统和高并发应用等领域。

官网:https://redis.io/
Redis中文文档:http://www.redis.cn/documentation.html
下载地址:进入官网下载即可(Windows版本需要在GitHub上下载,并且Redis版本已停更较长时间,不建议使用)
并且,Redis官方推荐在Linux服务器上进行搭建

Redis安装(Window & Liux服务器)

Windows安装https://blog.csdn.net/qq_40220309/article/details/125185615
Liux安装https://blog.csdn.net/HJW_233/article/details/131866231
二者都有:https://blog.csdn.net/weixin_46703850/article/details/122670741?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169122082116800188592262%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=169122082116800188592262&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-4-122670741-null-null.142v92chatsearchT0_1&utm_term=%E7%8B%82%E8%83%9C%E8%AF%B4Redis&spm=1018.2226.3001.4187

windows 安装 Radis

在这里插入图片描述

文件作用
redis-benchmark.exe测试性能
redis-check-aof.exe检查AOF持久化 (Redis支持两种持久化方式,RDB / AOF)
检查AOF持久化 (Redis支持两种持久化方式,RDB / AOF)启动服务
redis-cli.exe客户端

1、运行服务

​ 双击redis-server.exe 启动Redis服务 或 打开一个 cmd 窗口 使用 cd 命令切换目录运行:

redis-server.exe redis.windows.conf

这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。
在这里插入图片描述
2、连接Redis

​ 使用 Redis客户端(redis-cli.exe)连接Redis服务

切换到 redis 目录下运行:

redis-cli.exe -h 127.0.0.1 -p 6379

redis-cli

3、连接成功

D:\tool\Redis-x64-5.0.14
λ redis-cli.exe
127.0.0.1:6379>

4、使用

127.0.0.1:6379> ping #测试连接
PONG
127.0.0.1:6379> set name zs
OK
127.0.0.1:6379> get name
"zs"
127.0.0.1:6379>

Linux安装Redis

安装Redis的第一种,官网下载安装包

1、下载安装包,redis-5.0.10.tar.gz

2、下载到Windows之后,用Xftp工具上传至Linux

3、解压安装包并将其解压

tar -zxvf redis-5.0.4.tar.gz 

在这里插入图片描述
并且解压之后可以看见Redis的配置文件redis.conf

4、同时还需要基本的环境搭建

yum install gcc-c++

# 查看版本
g++ -v

# 安装Redis所需要的环境
make

# 此命令只是为了确认当前所有环境全部安装完毕,可以选择不执行
make install 

Redis的安装,默认在/usr/local/bin下
在这里插入图片描述
5、之后,需要将Redis的配置文件复制到bin目录下,可以提前准备好一个目录,然后在复制到新创建好的目录中

[root@192 redis-5.0.4]# ls
00-RELEASENOTES  CONTRIBUTING  deps     Makefile   README.md   runtest          runtest-sentinel  src    utils
BUGS             COPYING       INSTALL  MANIFESTO  redis.conf  runtest-cluster  sentinel.conf     tests
[root@192 redis-5.0.4]# cp redis.conf /usr/local/bin/myconfig/
[root@192 redis-5.0.4]# 

6、然后修改复制之后的配置文件,修改一条信息,修改的信息就是图中划红线的位置,它的意思是指守护进程模式启动,即可以在后台运行Redis

vim 搜索 方法:/str daemonize
在这里插入图片描述
7、随后就可以开始启动Redis服务(通过指定的配置文件启动服务)

[root@192 bin]# cd /usr/local/bin
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis.conf

在这里插入图片描述
8、连接

/usr/local/bin/

[root@192 bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> get age
"23"
127.0.0.1:6379> keys *
1) "age"

9 、查看进程运行

[root@192 ~]# ps -ef|grep redis
root       2057   1967  0 16:21 pts/0    00:00:00 grep --color=auto redis
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis.conf # 启动Redis服务
#...
[root@192 bin]# ps -ef|grep redis
root       2063      1  1 16:25 ?        00:00:00 redis-server 127.0.0.1:6379
root       2069   1967  0 16:25 pts/0    00:00:00 grep --color=auto redis
[root@192 bin]# 

10、关闭服务

127.0.0.1:6379> shutdown
127.0.0.1:6379> shutdown
not connected> exit
[root@192 bin]# ps -ef|grep redis
root       2074   1967  0 16:29 pts/0    00:00:00 grep --color=auto redis

避免中文乱码

有时候会有中文乱码。

要在 redis-cli 后面加上 --raw

redis-cli --raw

就可以避免中文乱码了

[root@192 bin]# redis-cli --raw
127.0.0.1:6379> set name 张三
OK
127.0.0.1:6379> get name
张三

性能测试工具

redis-benchmark性能测试工具
在这里插入图片描述
测试命令

# 当前命令表示,性能测试,在本机,端口号6379,并发连接数100,每个连接10w个请求数量
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

测试结果

[root@192 bin]# redis-benchmark -h localhost -p 6379 -c 100 -n 100000
# 测试结果如下,以RedisINLINE命令为例
====== PING_INLINE ======
  100000 requests completed in 5.24 seconds # 十万个请求在5.24秒之内被处理
  100 parallel clients # 每次请求都有100个客户端在执行
  3 bytes payload # 一次处理3个字节的数据
  keep alive: 1 # 每次都保持一个服务器的连接,只用一台服务器处理这些请求

0.37% <= 1 milliseconds
13.17% <= 2 milliseconds
# ...
100.00% <= 104 milliseconds # 所有的请求在104秒之内完成
19076.69 requests per second # 平均每秒处理19076.69个请求

====== PING_BULK ====== # set、get、ping等每个命令都会测试
  100000 requests completed in 4.45 seconds
  100 parallel clients
  3 bytes payload
  keep alive: 1

0.31% <= 1 milliseconds
# ...

Redis基础知识

备注:在Redis中,关键字语法不区分大小写!

Redis有16个数据库支持,为啥嘞,可以查看redis.conf配置文件
在这里插入图片描述
并且初始数据库默认使用0号数据库(16个数据库对应索引0到15)

  • 可以使用select命令切换数据库:select n(0-15)
  • 查看当前库的key数量:dbsize
127.0.0.1:6379> select 12
OK
127.0.0.1:6379[12]> select 0
OK
127.0.0.1:6379> dbsize # 查看当前库的key数量
(integer) 0
  • 清空当前数据库信息:flushdb
  • 清空全部数据库信息:flushall
  • 查看所有的key:keys *
127.0.0.1:6379> keys *
1) "myset:__rand_int__"
2) "mylist"
3) "key:__rand_int__"
4) "name"
5) "counter:__rand_int__"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
# 还有一个清空的命令,叫做flushall,它的意思是清空16个数据库中的全部信息。
# 不管在那种数据库中,清空库一直都是需要慎重操作的

题外话:为什么Redis选用6379作为默认端口号?

6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。MERZ长期以来被Redis作者antirez及其朋友当作愚蠢的代名词。后来Redis作者在开发Redis时就选用了这个端口。(摘自知乎)

Redis是单线程的(从Redis6.0.1开始支持多线程)

Redis的读写速度很快,官方表示,Redis基于内存操作,CPU不是Redis的性能瓶颈,Redis的性能瓶颈是根据机器的内存和带宽

Redis是C语言编写,官方提供的数据为10万+的QPS(Queries-Per-Second,每秒内的查询次数)
Redis单线程为什么速度还是这么快?

基础语法

基础语法作用
set key value设置一个key
get key获取一个key对应value
exists key查询key是否存在
move key n(n是数字)将当前key移动到指定的几号数据库中
keys *查询当前数据库中全部的key
expire key time设置当前key的过期时间
ttl key查询当前key的存活时间
type key查看key的数据类型
flushdb清空当前数据库信息(慎用)
flushall清空16个数据库中的全部信息(慎用)
select n选择数据库

对于Redis,有两个误区:

​ 1、高性能的服务器一定是多线程的?
​ 2、多线程一定比单线程效率高?

​核心: Redis将所有的数据全部放在内存中,使用单线程去操作效率比较高,对于多线程,CPU有一种东西叫做上下文切换,这种操作耗时,对于内存系统来说,没有上下文切换,效率一定是最高的。

​ Redis使用单进程的模式来处理客户端的请求,对大部分事件的响应都是通过epoll函数的加强封装,Redis的实际处理速度依靠主进程的执行效率,epoll可以显著提高程序在大量并发连接中系统的CPU利用率

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

五大基本数据类型(String List Set Hash Zset)

String

在这里插入图片描述
类似于Redis中String这样的使用场景,value值可以是字符串,也可以是其他类型

String的存储的字符串长度最大可以达到512M

主要用途

计数器

  • 统计多单位的数量
  • 一个用户的粉丝数
  • 一个有过期时间的验证码

List(列表)

Redis中的List列表可以做很多事情,可以将其看成数据结构中的栈,也可以是队列,或者阻塞队列
在这里插入图片描述
1、插入

127.0.0.1:6379> lpush mylist a b c # 头插法
3
127.0.0.1:6379> lrange mylist 0 2 # 查看
c
b
a
127.0.0.1:6379> RPUSH list2 a b c #尾插法
3
127.0.0.1:6379> lrange list2 0 2
a
b
c
127.0.0.1:6379> llen list2 # 返回长度
2

2、弹出

127.0.0.1:6379> lpop mylist # 头出
c
127.0.0.1:6379> lrange mylist 0 2
b
a
127.0.0.1:6379> rpop list2  #尾出
c
127.0.0.1:6379> lrange list2 0 2
a
b
127.0.0.1:6379> lindex list2  1 # 定位
b
127.0.0.1:6379> lpush list4 a b c d
4
127.0.0.1:6379> LREM list4 2 b # 删除
1
127.0.0.1:6379> lrange list4 0 8
d
c
a

3、截取

127.0.0.1:6379> lpush list3 a b c d  
4                                   
127.0.0.1:6379> ltrim list3 1 2    # 截取
OK                                  
127.0.0.1:6379> lrange list3 0 8     
c                                 
b      
127.0.0.1:6379> lpush list4 a b c d
4
127.0.0.1:6379> RPOPLPUSH list3 list4 # 转移
sss
127.0.0.1:6379> lrange list4 0 8
sss
d
c
b
a

4、指定value插入

127.0.0.1:6379> lpush list5 a b c d
4
127.0.0.1:6379> lrange list5 0 -1
d
c
b
a
127.0.0.1:6379> lrange list5 0 8
d
c
b
a
127.0.0.1:6379> linsert list5 before b ssr # 从指定 value值的前面 插入
5
127.0.0.1:6379> lrange list5 0 8
d                                         
c                                         
ssr                                       
b                                         
a                                                                              
127.0.0.1:6379> linsert list5 after b ddt  # 从指定 value值的后面 插入
7                                         
127.0.0.1:6379> lrange list5 0 8          
d                                         
c                                         
ssr                                       
b                                         
ddt 
a

Set(集合)

set集合无序不重复

在这里插入图片描述

1、设置set

127.0.0.1:6379> sadd set1 a b c # 向集合添加元素
3                              
127.0.0.1:6379> smembers set1   # 查看所有
c                              
b                              
a                              
127.0.0.1:6379> sadd set1 a b c # 集合无序不重复
0                              
127.0.0.1:6379> sadd set1 d    
1                              
127.0.0.1:6379> smembers set1  
d                              
c                              
b                              
a    
127.0.0.1:6379> scard set1 # 查看key的长度
4

2、删除

127.0.0.1:6379> sismember set1 a # 查看key中指定的value是否存在
1  # 1 表示存在
127.0.0.1:6379> sismember set1 s
0  # 1 表示不存在         
127.0.0.1:6379> SMEMBERS set1  # 查看所有
d                            
c                            
b                            
a                            
127.0.0.1:6379> srem set1 d    # 指定的value 
1                             
127.0.0.1:6379> spop set1   # 随机删除        
c
127.0.0.1:6379> SMEMBERS set1 
b                             
a   
127.0.0.1:6379> sadd set2  f g    
2                                 
127.0.0.1:6379> smove set2 set1 f # 移动
1                                 
127.0.0.1:6379> SMEMBERS set1     
f                                 
b                                 
a
127.0.0.1:6379> SRANDMEMBER set1 # 随机抽取
f

2、集合

生活中的一个小现象,就比如说微信公众号,会有共同关注,还有QQ的共同好友
数学集合关系中的:交、并、补。微信公众号中的共同关注,以及QQ的共同好友,就是关系中的交!

127.0.0.1:6379> sadd set3 a b c  
3                                
127.0.0.1:6379> sadd set4 a c d  
3                                
127.0.0.1:6379> sdiff set3 set4  # 求第一个key的补集
b                                
127.0.0.1:6379> sinter set3 set4 # 求交集
c                                
a                                
127.0.0.1:6379> sunion set3 set4 # 求交集
c                                
b                                
a                                
d                                          

命令都可以在英语单词中找到一些规律
把SDIFF、SINTER还有SUNION这三个单词首字母去掉,可以得到
DIFF:different,它代表不同的,用一句Redis官网的翻译来描述:返回的集合元素是第一个key的集合与后面所有key的集合的差集
INTER:intersection,翻译过来为交叉,同样的,意指数学关系中的交集
UNION:union,翻译为联合,与数学关系中的并集也是可以沾边的

Hash(哈希)

Redis中的哈希,本质上KV相同但是KV中的V,它也是一个键值对,本质和操作字符串区别不大
在这里插入图片描述
1、创建hash

127.0.0.1:6379> hset hash1 name zs    #  设置单个  hash  
1                                              
127.0.0.1:6379> hget hash1 name      # 获取单个 hash         
zs                                             
127.0.0.1:6379> hmset hash2 name joy age 23    #  设置多个
OK                                             
127.0.0.1:6379> hmget hash2 name age      #    获取多个  
joy                                            
23                                                                

2、获取

127.0.0.1:6379> hgetall hash1  # 获取hash中全部的 键值对
name
zs
127.0.0.1:6379> hlen hash1  # 获取长度
1
127.0.0.1:6379> hkeys hash2 # 获取全部的键
name
age
127.0.0.1:6379> hvals hash2 # 获取全部的值
joy
23
127.0.0.1:6379> hset hash3 age 23 
1
127.0.0.1:6379> hincrby hash3 age 25 # 自增23
48

3、使用“:”

可以使用hash做一些临时变更的数据,可以是用户信息,或者是经常变动的信息,上面的String也提到了使用“:”进行层次分割,不过hash更适合对象存储,String适合于文本的存储

127.0.0.1:6379> HMSET user:1 name xiaohuang age 21 sex boy
OK
127.0.0.1:6379> HGETALL user:1
1) "name"
2) "xiaohuang"
3) "age"
4) "21"
5) "sex"
6) "boy"

在这里插入图片描述

Zset(有序集合)

在set的基础上增加了一个score的值,相当于zset k1 score v1,使用score来对当前key中元素进行排序

在这里插入图片描述
1、

127.0.0.1:6379> zadd set1 1 one # 添加一个元素
1
127.0.0.1:6379> zadd set1 2 tew 3 three # 添加多个元素
2
127.0.0.1:6379> zrange set1 0 -1 # 查询从开始到结束索引的zset集合
one
tew
three
127.0.0.1:6379>

2、实现元素的排序

# 根据zset中score的值来实现元素的排序
127.0.0.1:6379> ZADD salary 3500 xiaohong 6500 xiaohuang 3900 zhangsan
(integer) 3
# 当前命令,inf在Unix系统中代表的意思是无穷,所以当前命令是指,将当前zset,以从小到大的形式进行排列
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf 
1) "xiaohong"
2) "zhangsan"
3) "xiaohuang"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 在排列的同时,将score和指定的元素全部展示
1) "xiaohong"
2) "3500"
3) "zhangsan"
4) "3900"
5) "xiaohuang"
6) "6500"
127.0.0.1:6379> ZREVRANGE salary 0 -1 withscores # 将数据从大到小进行排列
1) "xiaohuang"
2) "6500"
3) "zhangsan"
4) "3900"
5) "xiaohong"
6) "3500"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 4000 withscores # 展示的同时还可以指示score的查询最大值,指定查询范围
1) "xiaohong"
2) "3500"
3) "zhangsan"
4) "3900"
127.0.0.1:6379> ZREM salary zhangsan # 删除zset中的一个元素
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1 
1) "xiaohong"
2) "xiaohuang"
127.0.0.1:6379> ZCARD salary
(integer) 2

3、

127.0.0.1:6379> ZADD myset 1 hello 2 world 3 xiaohuang 4 xiaohei 5 xiaolan
(integer) 5
# 语法:ZCOUNT key min max ,min和max包左也包右,它是一个闭区间
127.0.0.1:6379> ZCOUNT myset 2 5 # 获取指定区间的成员数量
(integer) 4
127.0.0.1:6379> 

其他的API,如果说在工作中出现了,可以查看Redis的官方文档:http://www.redis.cn/commands.html

案例:zset是Redis的数据类型,可以排序,生活中也有案例,班级成绩,员工工资

设置权重,1、普通消息;2、重要消息;添加权重进行消息判断其重要性

来一个更接地气的案例,可以打开B站,排行榜,B站会根据视频的浏览量和弹幕量进行综合评分,进行排名

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

geospatial 地理位置

微信朋友圈中的朋友的位置,或者是QQ中也有的附近的人,饿了么中外卖小哥的位置距离

这个在Redis中被定为特殊的数据类型可叫做Geo,它是Redis3.2正式推出的一个特性,可以推导出两个地方的地理位置,两地之间的距离,方圆几千米之内的人。

对于这个关于地理的数据类型,它有6个命令
在这里插入图片描述
因为这个特殊的数据类型和地理相关,需要用到地理的经纬度,可以推荐一个网站查看指定城市的经纬度:http://www.jsons.cn/lngcode/

1、geoadd 添加地理位置

语法:GEOADD key 经度 纬度 城市名称 …

​ 注意:南北极无法直接添加。用添加城市数据来说,一般都会使用Java的Jedis来操作,而这些城市数据都是被下载下来通过JavaAPI调用
​ 有效经度从-180到180度
​ 有效纬度从-85.05112878 到 85.05112878 度。超过范围会出现(error) ERR invalid longitude,latitude pair

127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing 121.47 31.23 shanghai # 添加
2
127.0.0.1:6379> GEOHASH china:city beijing   # 返回GeoHash字符串
wx4fbxxfke0                                  
127.0.0.1:6379> ZRANGE china:city 0 -1      # 使用zset命令查看geospatial
shanghai                                     
beijing 
127.0.0.1:6379> ZREM china:city beijing # 使用zset的基本命令即可删除
1
127.0.0.1:6379> ZRANGE china:city 0 -1 
shanghai

2、geodist

单位:m表示单位米、km表示千米、mi表示英里、ft表示英尺
语法:GEODIST key member1 member2 [unit]

# 查看beijing和shanghai两个位置的直线距离
127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing 121.47 31.23 shanghai 
1
127.0.0.1:6379> GEODIST china:city beijing shanghai # 单位米
1067378.7564
127.0.0.1:6379> GEODIST china:city beijing shanghai km # 单位千米
1067.3788

3、georadius

自己所在的地址为圆心,半径查找
对于社交软件来说,附近的人,就相当于,你现在所在的地址,再加上一定的半径来进行查找
GEORADIUS key 经度 纬度 半径 [单位] [WITHCOORD(搜寻到的目标的经纬度)] [WITHDIST(直线距离)] [count]

# 以111经度31纬度为中心,1000km为半径搜寻在器范围之内的城市
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km 
1) "shenzhen"
2) "guangzhou"
3) "fuzhou"
4) "shanghai"

# 追加参数,目标经纬度,直线距离(找出的城市自带直线距离和经纬度)
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km WITHCOORD WITHDIST 
# ...

# 还可以限制查询的结果条数,只显示两条
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km WITHCOORD WITHDIST count 2 
1) 1) "guangzhou"
   2) "905.0108"
   3) 1) "113.27999979257583618"
      2) "23.1199990030198208"
# ...

4、GEORADIUSBYMEMBER

找出指定元素周围的其他元素,就是以城市为中心,一定长度为半径搜索

# 找出以shanghai为中心,1000km为半径搜索
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 1000 km 
1) "xiamen"
2) "fuzhou"
3) "shanghai"

5.GEOHASH
将二维经纬度转换为一维的Geohash字符串

# 将二维经纬度转换为一维的Geohash字符串,俩个字符串长得越像,距离越接近
127.0.0.1:6379> Geohash china:city shanghai beijing 
1) "wx4fbxxfke0"
2) "wm5xzrybty0"

Hyperloglog

在讲Hyperloglog之前,什么是基数?

集合中包含的不重复元素即为基数,就比如一个A数据集,A{1,3.7,9,11},它的基数为5,可以接受误差

Hyperloglog是Redis2.8.9更新的,它是一种数据结构,主要是针对于基数统计的算法

优点,占用的内存很小,只需要使用12KB的内存即可统计2^64的数据

​ 在实际业务中,网页的UV(Unique Visitor,独立访客),一个人访问一个网站多次,只能算作是一个,用传统的方式,set集合保存用户的id,然后统计set中元素个数作为标准来判断。使用这种方式来进行数据统计的话,大量的内存用来浪费给保存用户id了,目的是为了计数,而不是为了保存用户id

Hyperloglog计数的错误率在0.81%,用来执行UV任务,可以忽略不计
在这里插入图片描述

127.0.0.1:6379> pfadd pf1 a b c d e f g h i j # 创建
1                                            
127.0.0.1:6379> pfcount pf1      # 查看元素的长度            
10                                           
127.0.0.1:6379> pfadd pf2 1 2 3 4            
1                                            
127.0.0.1:6379> PFMERGE pf1 pf2    # 合并数组          
OK                                           
127.0.0.1:6379> pfcount pf1                  
14                                           

如果在项目中允许容错,可以使用Hyperloglog

如果不行,就可以直接使用set或者Java的HashMap来实现

Bitmaps

Bitmaps是一种位存储的数据类型,在Redis2.2.0被推出,

生活中可以实现的场景,统计活跃用户,在线状态,用户签到,这些都是表示两种状态之间的,可以使用Bitmaps

Bitmaps,译为位图,也是一种数据结构,操作二进制位进行记录,只有0和1两种状态。Bitmaps通过最小的单位bit来进行存储,表示元素对应的状态
在这里插入图片描述
1.统计一周的打卡次数
周一:1 周二:0 周三:0 周四:1.。。。。

127.0.0.1:6379> setbit bit1 0 1
1
127.0.0.1:6379> setbit bit1 1 0
0
127.0.0.1:6379> setbit bit1 2 0
0
127.0.0.1:6379> setbit bit1 3 1
1
127.0.0.1:6379> getbit bit1 3
1

127.0.0.1:6379> bitcount bit1
0
# 在week中返回第一个出现1的value值
127.0.0.1:6379> BITPOS bit1 1 0 -1 
0

2、BITOP逻辑运算

一共有4种逻辑运算,AND、OR、NOT、XOR,分别代表 并、或、非、异或

127.0.0.1:6379> SETBIT bit-1 0 1
(integer) 0
127.0.0.1:6379> BITOP AND and-bit bit1 bit2  # 对bit1和bit2进行并操作 得到 and-bit
# ...
127.0.0.1:6379> BITOP OR or-bit bit1 bit2 # 或 操作

# 对bit1进行 非 操作,注意:非操作只针对一个key
127.0.0.1:6379> BITOP NOT not-bit bit1
(integer) 1

# 对bit-1和bit-2进行 异或 操作
127.0.0.1:6379> BITOP XOR xor-bit bit-1 bit-2

备注:BITOP执行命令较慢,因为其时间复杂度为O(n)。

在进行计数时,如果数据量过大,建议直接将其指派到master-slave中的slave节点进行处理,避免阻塞

事务

Redis单条命令保持原子性,但是Redis事务不保证原子性
原子性: 要么都成功,要么都失败
Redis事务没有隔离级别的概念

Redis事务本质:一组命令的集合(如我要先set 再 get,set,get这组命令就是事务)

Redis事务本质,可以将Redis的事务看成是一个队列,将一组命令进行“入队”,然后一起执行

一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行

Redis事务的三个特性:一致性,顺序性,排他性
在这里插入图片描述
1、正常流程

127.0.0.1:6379> multi  # 开启事务        
OK   
# ===  事务入队中 ===
127.0.0.1:6379> set name zs     
QUEUED         # 表示命令入队,等待客户端执行事务                 
127.0.0.1:6379> append name joy 
QUEUED        # 命令入队                  
127.0.0.1:6379> get name        
QUEUED    # 命令入队
# ===   事务入队结束 ===
127.0.0.1:6379> exec    # 执行事务  
#  === 执行结果  ===      
OK  
5                               
zsjoy                           

2、放弃事务

127.0.0.1:6379> multi # 开启事务 
OK
127.0.0.1:6379> set age 23
QUEUED
127.0.0.1:6379> append age 15
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> discard # 放弃事务
OK
# 一旦放弃事务,之前入队的全部命令都不会执行
(nil)
127.0.0.1:6379> get age # 无结果
(nil)

3、编译型异常

编译型异常(又叫入队错误)的特点:事务中有错误的命令,会导致默认放弃事务,所有的命令都不会执行

127.0.0.1:6379> multi # 开启事务 
OK
127.0.0.1:6379> set name zs
QUEUED
127.0.0.1:6379> append name2 # 错误的命令
ERR wrong number of arguments for 'append' command

127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> EXEC # 执行事务,出现编译型异常
EXECABORT Transaction discarded because of previous errors.

4、运行时异常

​ 运行时异常(又叫执行错误):在事务执行的过程中语法没有出现任何问题,但是它对不同类型的key执行了错误的操作,
​ Redis只会将返回的报错信息包含在执行事务的结果中,并不会影响Redis事务的一致性

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name zs
QUEUED
127.0.0.1:6379> incr name # 语法正确,但是对一个String类型的k1执行了错误的操作
QUEUED
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> exec # 执行事务,出现运行时异常
OK # 执行ok
ERR value is not an integer or out of range  # 命令报错,但是不影响事务整体的运行

zs # 依旧执行

Redis监视测试

本节内容参考博客redis实现乐观锁

监控! Watch

悲观锁:

  • 很悲观,无论执行什么操作都会出现问题,所以会对所有的操作加锁

乐观锁

  • 很乐观,任何情况下都不会出问题,所以不会加锁!但是在数据更新时需要判断在此之前是否有人修改过这个数据
  • 可以添加一个字段叫version用来查询
  • 在进行数据更新时对version进行比较

1、首先先模拟正常状态

语法:watch key … : 对指定key进行监控,监控这个key在事务执行之前是否被修改

# 模拟客户转账
127.0.0.1:6379> watch money     # 监控money
OK                              
127.0.0.1:6379> multi          # 如果没有被修改,那么这个事务是可以正常执行成功的 
OK                              
127.0.0.1:6379> decrby money 200  # 转账200
QUEUED                          
127.0.0.1:6379> incrby out 20   # + 20
QUEUED                          
127.0.0.1:6379> exec            
-200                            
20                              
127.0.0.1:6379>                 

2、如果被监控的key在事务之外被修改了

127.0.0.1:6379> WATCH money
OK
127.0.0.1:6379> MULTI 
OK
127.0.0.1:6379> DECRBY money 30
QUEUED
127.0.0.1:6379> INCRBY out 30
QUEUED

这个时候开始模拟另外一个客户端恶意修改被监控的key

# === 表示另一个客户端 ===
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> INCRBY money 200 # 修改被监控的数据
(integer) 280
127.0.0.1:6379> get money
"280"

回到之前客户端

# 再次执行事务,会直接返回nil,代表执行失败
127.0.0.1:6379> EXEC 
(nil)
127.0.0.1:6379> get money # 再次查看,当前监控的key已经被修改
"280"

测试多线程修改值,使用watch可以当做Redis的乐观锁操作!
实际上关于WATCH,还有一个命令,UNWATCH,意思是解除所有监控,但是官网的原话是,一旦你执行了DISCARD或者EXEC,就没必要在执行UNWATCH

Jedis

Jedis是Redis官方推荐的Java连接Redis的连接开发工具!使用Java操作Redis的中间件

测试连接

1.1创建项目
创建Moven项目,导入依赖

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

1.2编码测试

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

src/test/TestPing

import redis.clients.jedis.Jedis;

public class TestPing {
    public static void main(String[] args) {
        // new一个Jedis对象
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        // Jedis中的API就是之前学习的命令
        System.out.println(jedis.ping());
        
        //关闭连接
        jedis.close();
    }
}
  • 测试成功

返回PONG
1.3解决远程连接问题
不过这边有一个小问题,如果你的Redis是远程连接的话,会出现连接超时或者是拒绝访问的问题,在这边需要做两件事情,当然,防火墙的关闭也是必不可少的

打开redis.conf配置文件
在这里插入图片描述
1.4API测试
随后进行API测试

import redis.clients.jedis.Jedis;

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

        System.out.println("=================== String =========================");
        System.out.println(jedis.set("name", "zs"));
        System.out.println(jedis.get("name"));
        System.out.println(jedis.append("name", "+value"));
        System.out.println(jedis.get("name"));
        System.out.println(jedis.strlen("name"));

        System.out.println("=================== List =========================");
        System.out.println(jedis.lpush("listKey", "l1", "l2", "l3"));
        System.out.println(jedis.lrange("listKey", 0, -1)); // [l3, l2, l1]
        System.out.println(jedis.llen("listKey"));

        System.out.println("=================== Hash =========================");
        System.out.println(jedis.hset("hashKey", "k1", "v1"));
        System.out.println(jedis.hset("hashKey", "k2", "v2"));
        System.out.println(jedis.hset("hashKey", "k3", "v3"));
        System.out.println(jedis.hmget("hashKey", "k1", "k2", "k3")); // [v1, v2, v3]
        System.out.println(jedis.hgetAll("hashKey")); // {k3=v3, k2=v2, k1=v1}

        System.out.println("=================== Set =========================");
        System.out.println(jedis.sadd("setKey", "s1", "s2", "s3", "s4"));
        System.out.println(jedis.smembers("setKey")); // [s2, s1, s4, s3]
        System.out.println(jedis.scard("setKey"));

        System.out.println("=================== Zset =========================");
        System.out.println(jedis.zadd("ZKey", 90, "z1"));
        System.out.println(jedis.zadd("ZKey", 80, "z2"));
        System.out.println(jedis.zadd("ZKey", 85, "z3"));
        System.out.println(jedis.zrange("ZKey", 0, -1)); // [z2, z3, z1]
    }
}
  • 结果
=================== String =========================
OK
zs
8
zs+value
8
=================== List =========================
3
[l3, l2, l1]
3
=================== Hash =========================
1
1
1
[v1, v2, v3]
{k3=v3, k1=v1, k2=v2}
=================== Set =========================
4
[s4, s1, s3, s2]
4
=================== Zset =========================
1
1
1
[z2, z3, z1]

进程已结束,退出代码为 0

在这里插入图片描述
1.5测试事务

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

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name", "张三");
        jsonObject.put("age", "21");
        jsonObject.put("sex", "boy");
        Transaction multi = jedis.multi(); //  开启事务
        String user = jsonObject.toJSONString();
        try {
            multi.set("user1", user);
            multi.set("user2", user);
            multi.exec();
        } catch (Exception e) {
            multi.discard(); // 出现问题,放弃事务
            e.printStackTrace();
        } finally {
            System.out.println(jedis.mget("user1", "user2"));
            jedis.close(); // 关闭连接
        }
    }
}
  • 结果
[{"sex":"boy","name":"张三","age":"21"}, {"sex":"boy","name":"张三","age":"21"}]

常用API

1、通用API

import java.util.Set;
import redis.clients.jedis.Jedis;

public class TestKey {
    public TestKey() {
    }

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

2、String(字符串)

import java.util.concurrent.TimeUnit;
import redis.clients.jedis.Jedis;

public class TestString {
    public TestString() {
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("===========增加数据===========");
        System.out.println(jedis.set("key1", "value1"));
        System.out.println(jedis.set("key2", "value2"));
        System.out.println(jedis.set("key3", "value3"));
        
        System.out.println("删除键key2:" + jedis.del("key2"));
        System.out.println("获取键key2:" + jedis.get("key2"));
        System.out.println("修改key1:" + jedis.set("key1", "value1Changed"));
        System.out.println("获取key1的值:" + jedis.get("key1"));
        System.out.println("在key3后面加入值:" + jedis.append("key3", "End"));
        System.out.println("key3的值:" + jedis.get("key3"));
        System.out.println("增加多个键值对:" + jedis.mset(new String[]{"key01", "value01", "key02", "value02", "key03", "value03"}));
        System.out.println("获取多个键值对:" + jedis.mget(new String[]{"key01", "key02", "key03"}));
        System.out.println("获取多个键值对:" + jedis.mget(new String[]{"key01", "key02", "key03", "key04"}));
        System.out.println("删除多个键值对:" + jedis.del(new String[]{"key01", "key02"}));
        System.out.println("获取多个键值对:" + jedis.mget(new String[]{"key01", "key02", "key03"}));
        jedis.flushDB();
        System.out.println("===========新增键值对防止覆盖原先值==============");
        System.out.println(jedis.setnx("key1", "value1"));
        System.out.println(jedis.setnx("key2", "value2"));
        System.out.println(jedis.setnx("key2", "value2-new"));
        System.out.println(jedis.get("key1"));
        System.out.println(jedis.get("key2"));
        System.out.println("===========新增键值对并设置有效时间=============");
        System.out.println(jedis.setex("key3", 2, "value3"));
        System.out.println(jedis.get("key3"));

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException var3) {
            var3.printStackTrace();
        }

        System.out.println(jedis.get("key3"));
        System.out.println("===========获取原值,更新为新值==========");
        System.out.println(jedis.getSet("key2", "key2GetSet"));
        System.out.println(jedis.get("key2"));
        System.out.println("获得key2的值的字串:" + jedis.getrange("key2", 2L, 4L));
    }
}

3、List(列表)

import redis.clients.jedis.Jedis;

public class TestList {
    public TestList() {
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("===========添加一个list===========");
        jedis.lpush("collections", new String[]{"ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap"});
        jedis.lpush("collections", new String[]{"HashSet"});
        jedis.lpush("collections", new String[]{"TreeSet"});
        jedis.lpush("collections", new String[]{"TreeMap"});
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("collections区间0-3的元素:" + jedis.lrange("collections", 0L, 3L));
        System.out.println("===============================");
        System.out.println("删除指定元素个数:" + jedis.lrem("collections", 2L, "HashMap"));
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("删除下表0-3区间之外的元素:" + jedis.ltrim("collections", 0L, 3L));
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("collections列表出栈(左端):" + jedis.lpop("collections"));
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("collections添加元素,从列表右端,与lpush相对应:" + jedis.rpush("collections", new String[]{"EnumMap"}));
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("collections列表出栈(右端):" + jedis.rpop("collections"));
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("修改collections指定下标1的内容:" + jedis.lset("collections", 1L, "LinkedArrayList"));
        System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
        System.out.println("===============================");
        System.out.println("collections的长度:" + jedis.llen("collections"));
        System.out.println("获取collections下标为2的元素:" + jedis.lindex("collections", 2L));
        System.out.println("===============================");
        jedis.lpush("sortedList", new String[]{"3", "6", "2", "0", "7", "4"});
        System.out.println("sortedList排序前:" + jedis.lrange("sortedList", 0L, -1L));
        System.out.println(jedis.sort("sortedList"));
        System.out.println("sortedList排序后:" + jedis.lrange("sortedList", 0L, -1L));
    }
}

4、set(集合)

import redis.clients.jedis.Jedis;

public class TestSet {
    public TestSet() {
    }

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

5、Hash(哈希)

import java.util.HashMap;
import java.util.Map;
import redis.clients.jedis.Jedis;

public class TestHash {
    public TestHash() {
    }

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

SpringBoot整合Redis

备注:从SpringBoot2.x之后,原先使用的Jedis被lettuce替代

  • Jedis:采用直连,模拟多个线程操作会出现安全问题。为避免此问题,需要使用Jedis Pool连接池!类似于BIO模式

  • lettuce:采用netty网络框架,对象可以在多个线程中被共享,完美避免线程安全问题,减少线程数据,类似于NIO模式

整合Redis

1、创建SpringBoot项目
在这里插入图片描述
2.配置并导入依赖
在这里插入图片描述

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

3.常用方式

三种常用方式

  • redisTemplate.ops*** 操作不同的数据类型,api同指令相同
//操作String
redisTemplate.opsForValue().set("myKey", "myValue");
//操作Hash
redisTemplate.opsForHash().hasKey("name","张三");
// ...
  • redisTemplate.接常用命令
redisTemplate.exec();
//redisTemplate.keys()
  • 获取连接,再通过连接执行命令
RedisConnection conn =  redisTemplate.getConnectionFactory().getConnection();
//通过 conn 清库
conn.flushAll();

4.测试

package com.example.springboot;

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

@SpringBootTest
class ApplicationTests {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("myKey","myValue");
        System.out.println(redisTemplate.opsForValue().get("myKey"));
    }

}
  • 结果
myValue

redis中传送对象

开发中一般用json传数据
1、新建 实体类

@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private String name;
    private int age;
}

2、测试

@SpringBootTest
class ApplicationTests {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Test
    void test(){
        try {
            User user = new User("张三",25);
            //object -> json 开发中一般用json传数据(序列化)
            String json = new ObjectMapper().writeValueAsString(user);
            redisTemplate.opsForValue().set("user",json);
            System.out.println(redisTemplate.opsForValue().get("user"));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}
  • 控制台结果
 {"name":"张三","age":25}
  • 客户端结果会出现乱码
    在这里插入图片描述

  • Redis的对象都需要序列化serialization或者 String json = new ObjectMapper().writeValueAsString(user);,不序列化会报错
    在这里插入图片描述
    在这里插入图片描述

  • RedisTemplate默认的序列化是JDK序列化,这时候可能要自定义配置类使用JSON来序列化

  • 创建对象时需要序列化、implement Serializable

自定义的配置类(配置自己的JSON序列化方式可以直接拿来用)

@Configuration
@SuppressWarnings("all")
//镇压所有警告
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
//      默认的连接配置
        template.setConnectionFactory(redisConnectionFactory);

//        序列化配置
//        new 一个Jackson序列化对象,用于后面的设置
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
//        用于转义
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//       创建string的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

//       string的key和hash的key都采用string的序列化
//        value都采用Jackson的序列化

        //key采用string序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key采用string序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value采用Jackson序列化方式
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        //hash的value采用Jackson序列化方式
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);


        return template;
    }
}

测试:

@SpringBootTest
class RedisSpringbootApplicationTests {

    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
//        HashMap
        //redisTemplate.opsForValue(); //
        User user = new User("Ivring",12);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
        redisTemplate.opsForValue().set("Ivring","欧文");
    }
}
  • 通过自定义配置类这样的序列化之后key就不会乱码了,但是在企业开发中一般不直接以原生的编写,将常用的操作封装为RedisUtils,自己写一些工具类来使用

  • 在工具类中应该加入一些容错操作,能抛出异常

  • 在公司能看到一些封装的RedisUtils

Redis工具类

package com.zxy.demo.redis;

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工具类
 * @author ZENG.XIAO.YAN
 * @date   2018年6月7日
 */
@Component
public final class RedisUtil {
	
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;

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

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

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

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

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

	/**
	 * 普通缓存放入
	 * @param key 键
	 * @param value 值
	 * @return true成功 false失败
	 */
	public boolean set(String key, Object value) {
		try {
			redisTemplate.opsForValue().set(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

	}

	/**
	 * 普通缓存放入并设置时间
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
	 * @return true成功 false 失败
	 */
	public boolean set(String key, Object value, long time) {
		try {
			if (time > 0) {
				redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
			} else {
				set(key, value);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	/**
	 * 移除值为value的
	 * @param key 键
	 * @param values 值 可以是多个
	 * @return 移除的个数
	 */
	public long setRemove(String key, Object... values) {
		try {
			Long count = redisTemplate.opsForSet().remove(key, values);
			return count;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}
	// ===============================list=================================

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

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

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

	/**
	 * 将list放入缓存
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	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;
		}
	}

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

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

	/**
	 * 根据索引修改list中的某条数据
	 * @param key 键
	 * @param index 索引
	 * @param value 值
	 * @return
	 */
	public boolean lUpdateIndex(String key, long index, Object value) {
		try {
			redisTemplate.opsForList().set(key, index, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 移除N个值为value
	 * @param key 键
	 * @param count 移除多少个
	 * @param value 值
	 * @return 移除的个数
	 */
	public long lRemove(String key, long count, Object value) {
		try {
			Long remove = redisTemplate.opsForList().remove(key, count, value);
			return remove;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}
}

Redis.conf 配置文件

Redis在启动的时候是通过配置文件进行启动的
对 Redis.conf 配置文件 进行分析

1、Units

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

# Redis configuration file example

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# [翻译]单位不区分大小写,所以1GB 1Gb 1gB都是一样的。

2、includes

包含,可以在Redis启动的时候再加载一些除了Redis.conf之外的其他的配置文件,和Spring的import,jsp的include类似


################################## INCLUDES ###################################

# ... 
# [作用]
# include .\path\to\local.conf
# include c:\path\to\other.conf

3、NETWORK

网络,表示Redis启动时开放的端口默认与本机绑定

################################## NETWORK #####################################

# By default, ...
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ ...
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# [作用]网络,表示Redis启动时开放的端口默认与本机绑定
bind 127.0.0.1

# Protected mode is a layer of security protection, in order to avoid that
# ...
# [作用]是否开启保护模式,Redis默认开启,如果没有设置bind的IP地址和Redis密码,那么服务就会默认只能在本机运行
protected-mode yes

# [翻译]接受指定端口上的连接,默认为6379 (IANA #815344)。
# [翻译]如果端口0被指定,Redis将不会监听TCP套接字。
# [作用]Redis指定监听端口,默认为6379
port 6379

# ...

# [作用]表示服务器闲置多长时间(秒)后被关闭,如果这个这个数值为0,表示这个功能不起作用
timeout 0

4、GENERAL

################################# GENERAL #####################################

# By default ...
#
# 创建一个pid文件是最好的努力:如果Redis不能创建它
# 没有坏的事情发生,服务器将正常启动和运行,会启动一个进程,并且会根据文件名来判断是否是同一个进程。
# 不支持 /var/run/redis.pid

# [作用]指定服务器详细级别。
# 这可以是:
# debug(很多信息,对开发/测试很有用)
# verbose(有很多很少有用的信息,但不像调试级别那样混乱)
# notice(略微冗长,可能是您在生产中想要的内容)
# warning(只记录非常重要/关键的消息)
loglevel notice

# Specify the log file name. Also 'stdout' can be used to force
# Redis to log on the standard output.
# [作用]打印的日志文件名称,如果为空,表示标准输出,在配置守护进程的模式下会将输出信息保存到/dev/null
logfile ""

# ...
# 数据库支持数量,16个
databases 16

# [作用]是否以守护进程的方式运行,即后台运行,一般默认为no,需要手动改为yes
always-show-logo yes
  • loglevel notice
    配置日志等级,日志等级的可选项如下(翻译自配置文件,有改动):

  • debug:打印的信息较多,在工作中主要用于开发和测试

  • verbose:打印的信息仅次于debug,但是格式较为工整

  • notice:Redis默认配置,在生产环境中使用

  • warning:只打印一些重要信息,比如警告和错误

5、SNAPSHOTTING(RDB)

中文翻译为快照,如果在规定的时间内,数据发生了几次更新,那么就会将数据同步备份到一个文件中

Redis的持久化有两种方式,一种是RDB,一种是AOF。SNAPSHOTTING主要针对的是Redis持久化中的RDB

Redis是一个内存数据库,如果不采用持久化对数据进行保存,那么就会出现断电即失的尴尬场面

################################ SNAPSHOTTING  ################################
# ...
# 在900秒内,至少有一个key被修改(添加),就会进行持久化操作
save 900 1
# 在300秒内,至少有10个key被修改,就会进行持久化操作
save 300 10
# 在60秒内,至少有1万个key被修改,就会进行持久化操作
save 60 10000

# 如果Redis在进行持久化的时候出现错误,是否停止写入,默认为是
top-writes-on-bgsave-error yes

#是否在进行数据备份时压缩持久化文件,默认为是,这个操作会耗费CPU资源,可以设置为no
rdbcompression yes

# 在保存持久化文件的同时,对文件内容进行数据校验
rdbchecksum yes

# 持久化文件保存的目录,默认保存在当前目录下
dir ./

6、REPLICATION

复制主机上的数据,当前配置所指定的IP和端口号即为主机

################################# REPLICATION #################################
# Redis在配置文件中将此配置注释,默认不使用,下同
# replicaof <masterip> <masterport>

# 如果配置的主机有密码,需要配置此密码以通过master的验证
# masterauth <master-password>

7、SECRULITY

​ 安全,可以在配置文件中设置Redis的登录密码
在这里插入图片描述
8、CLIENT

​ Redis允许存在的客户端的最大数量,默认有一万个

################################### CLIENTS ####################################
# Redis允许存在的客户端的最大数量,默认有一万个
# maxclients 10000
############################## MEMORY MANAGEMENT ################################
# Redis配置最大的内存容量
# maxmemory <bytes>

# 内存达到上限之后默认的处理策略
# maxmemory-policy noeviction

处理策略有以下几种

  • noeviction:默认策略,不淘汰,如果内存已满,添加数据是报错。
  • allkeys-lru:在所有键中,选取最近最少使用的数据抛弃。
  • volatile-lru:在设置了过期时间的所有键中,选取最近最少使用的数据抛弃。
  • allkeys-random: 在所有键中,随机抛弃。
  • volatile-random: 在设置了过期时间的所有键,随机抛弃。
  • volatile-ttl:在设置了过期时间的所有键,抛弃存活时间最短的数据

9、APPEND ONLY MODE(AOF配置)

​ 这是Redis持久化的另一种方式,AOF,AOF模式默认不开启,Redis默认开启的是持久化模式是RDB,在大部分情况下,RDB的模式完全够用

appendonly no

AOF持久化的文件名称

appendfilename "appendonly.aof"
# 对于 appendfsync 它有以下几个属性 
# appendfsync always 表示每次修改都会进行数据同步,速度较慢,消耗性能
# appendfsync no 不执行同步,不消耗性能
appendfsync everysec # 每秒执行一次同步,但是可能会丢失这一秒的数据

持久化中基本的配置,将在本章博客Redis持久化中讲解

Redis持久化(RDB AOF)

为什么Redis要实现持久化
Redis的数据存在内存中,断电即失,所以要实现持久化,保存数据

RDB(Redis DataBase)

RDB,全称Redis DataBase。什么是RDB,在指定的时间间隔内将数据集快照写入到磁盘中,在恢复数据的时候将这些快照文件读取到内存中,对应的配置文件中的SNAPSHOTTING,可以查看在上面提到的Redis的配置文件
在这里插入图片描述
​ Redis会单独创建出一个子进程(fork)来进行持久化,会将数据写入到一个临时文件中,待持久化操作结束之后,临时文件会将已经持久化完成的文件替换掉,在这个过程中,主进程不进行任何IO操作,这也就确保RDB极高的性能,相比于RDB和AOF,RDB的模式会比AOF更加的高效。如果在进行大数据恢复,并且对于数据的精度要求不高,那么就可以使用RDB,Redis的持久化默认的也是RDB,在一般情况下(生产环境)不需要修改这个配置

什么是fork?fork就是复制一个和当前一模一样的进程作为原进程的子进程

​RDB保存的文件就是dump.rdb文件,都是在我们的配置文件中快照中进行配置的
在这里插入图片描述
在这里插入图片描述

RDB的触发机制(达到触发机制后,会将数据存储起来,生产rdb文件放到磁盘)

1、满足配置文件的save规则的情况下,会自动触发RDB规则

2、执行FLUSHALL命令,也会触发RDB规则,但是没有意义,因为文件内容为空

3、退出Redis,也会产生dump.rdb文件(退出Redis默认执行save命令)

4、在客户端中使用save或者bgsave命令,也可以触发RDB规则但是这两种规则有所不同

  • save命令会完全占用当前进程去进行持久化操作,也就是说,save命令只管保存,不管其他,只要有进程过来,一律阻塞
  • bgsave命令会在后台运行,手动fork子进程进行操作,并且还会相应客户端的请求

如何进行数据恢复(将磁盘中的rdb文件恢复到redis内存中)

1.只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查rdb文件,恢复其中的数据
2.查看需要存放的位置

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

3.然后将dump.rdb文件放到Redis的启动目录下即可,启动就会自动恢复其中的数据

优点:

  • 适合大规模数据修复!
  • 对数据精度要求不高

缺点

  • 在持久化的时候需要一定的时间间隔,如果在一定的间隔时间内服务器意外宕机,那么就会丢失最后一次持久化的数据
  • 因为RDB持久化是需要fork出一份子进程进行IO操作的,也就是说,在原本的进程当中再复制出一个一模一样的进程作为子进程在内存中运行,内存的承载就会变为原来的两倍

AOF(Append Only File)

​ Redis的另一种持久化方式,AOF,全名为Append Only File,它用日志的形式来记录每一个写操作,将Redis执行过的命令进行记录(读操作不记录),只追加文件,不改写文件。Redis在启动时会自动加载AOF持久化的文件重新构建数据,也就是Redis重启会把AOF持久化文件中的信息从前到后执行一次以完成数据的恢复

​ AOF持久化对应的配置文件的位置是APPEND ONLY MODE

在这里插入图片描述
AOF保存的是appendonly.aof文件

  • 启动AOF
############################## APPEND ONLY MODE ###############################

# ...
# 启动AOF,需要将no 改成yes
appendonly yes

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof"

修改完配置之后,只需重新启动就可

这里有一个小细节需要注意:如果AOF和RDB模式在配置文件中都有开启的话,为了保证数据的安全性,在Redis启动时会优先使用AOF

Redis的AOF持久化保存的文件名称就叫做appendonly.aof

  • 修复AOF

如果说appendonly.aof文件的内容发生了一些错误,那么在Redis进行启动时,会出现问题,如果appendonly.aof内部发生错误,咋办?Redis中提供了一个可以修复aof文件的修复工具叫做redis-check-aof
在这里插入图片描述
怎么使用呢,下面有一条命令

redis-check-aof --fix appendonly.aof
[root@bogon bin]# redis-check-aof --fix appendonly.aof 
0x             167: Expected \r\n, got: 6769
AOF analyzed: size=383, ok_up_to=351, diff=32
This will shrink the AOF from 383 bytes, with 32 bytes, to 351 bytes
Continue? [y/N]: y
Successfully truncated AOF

AOF优缺点

优点:

它支持 同步记录 和 异步记录 ,对应的配置文件的属性分别是下面两个

appendfsync always   # 同步记录,客户端中一有写操作,即刻记录,数据的完整性好,但是性能较差
appendfsync everysec # 异步记录,每秒记录一次,但是服务器如果在这一秒之内宕机,这一秒的数据就会丢失
appendfsync no       # 不记录

缺点:

从恢复数据的角度来说,AOF所恢复的数据量一定是比RDB来得大的,从恢复数据的时间的角度来说,AOF的时间也是大于RDB的

AOF的重写机制

AOF持久化本质就是采用日志的形式对文件内容进行追加,为了防止追加之后这个文件变得越大,所以Redis推出了一种针对于AOF文件的重写机制,如果AOF文件的大小超过配置文件中所设定的阈值时,会自动触发重写机制对文件内容进行压缩,只对可以恢复数据的命令进行保留,针对于这种重写机制,也可以在客户端中对这种重写机制进行手动触发,只需要一个命令

  • 手动触发重写机制
127.0.0.1:6379> bgrewriteaof

原理

在这里插入图片描述

​当AOF文件持久追加并且大于64mb时,Redis会fork出一条新进程来对文件进行重写,和RDB一样,AOF也是先写临时文件再将其替换掉。Redis会对新进程中的数据进行遍历,每次都会遍历set和set有关的命令。重写并没有读取原来的appendonly.aof文件,而是使用命令将内存中的数据库内容进行重写得到一个新的文件

Redis会将上一次重写的AOF文件大小进行记录,如果当前文件的大小超过源文件的一倍并且大小大于64M时就会触发重写操作

总结

在这里插入图片描述

在这里插入图片描述
Master-Slave Repllcation:主从复制

Redis发布订阅

Redis的发布订阅(publish/subscribe)是一种消息通信模式,发送者(publish)发送消息,订阅者(subscribe)接收消息

Redis客户端可以订阅任意数量的频道
在这里插入图片描述
如图,有三个客户端订阅了一个Channel1
在这里插入图片描述
当Channel1的后台发送了一个数据到Channel1的频道中,这三个订阅了Channel1的客户端就会同时收到这个数据
在这里插入图片描述
命令:这些命令被广泛用于构建即时通讯应用,比如网络聊天室和实时广播,实时提醒等等
在这里插入图片描述
这些都是用来实现数据通信的命令,现实中的场景可以是网络聊天室,广播等
订阅端

# [SUBSCRIBE] 订阅一个频道叫chanword
127.0.0.1:6379> subscribe chanword
subscribe
chanword # 频道名
1 # 1表示订阅成功(1表示命令执行成功,o表示命令执行失败)
# === 一旦开始订阅,会立即占用当前进程去监听自己所订阅的那个Channel ===
message # 标识
chanword # 频道名
Hello World # 接受的数据
message
chanword
my number is 1548 # 接受的数据

发送端

# [PUBLISH] 往频道中发布一条消息
127.0.0.1:6379> publish nochan "Hello World"     
0   # 发送失败,没有nochan这个频道                                               
127.0.0.1:6379> publish chanword "Hello World"      
1   # 发送成功,有chanword这个频道                                                
127.0.0.1:6379> publish chanword "my number is 1548"
1    # 发送成功                                                                                 

原理:

Redis是C语言编写,在实现消息的发布和订阅的时候,Redis将其封装在一个后缀名为点c的文件中,pubsub.c
在这里插入图片描述
通过subscribe和publish命令实现消息的发布和订阅,当用户订阅了一个频道之后,redis-server里面维护了一个字典,字典里有很多个Channel(频道),字典的值就是一个链表,链表中保存的是订阅了这个频道的用户,使用publish命令往频道发送数据之后,redis-server使用此频道作为key,去遍历这个指定的value链表,将信息依次发送过去
在这里插入图片描述

发布订阅的实现场景

1、实时沟通消息系统

2、微信公众号(点击关注,后台发送一篇博客,订阅的用户就可以监听到)

还有一些比较复杂的场景,可以使用消息中间件来做,RabbitMQ,RocketMQ,kafka…

Redis实现主从复制

概念

​ 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点。
主从复制,读写分离,主节点以写为主、从节点以读为主,80%的情况下都是在进行读操作,减缓服务器压力,架构中经常使用!最低配是一主二从。
在这里插入图片描述

​ 默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。

作用

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

为什么使用集群

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

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:
从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较
大;
从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。这时候就可以使用主从复制。

环境配置

只配置从库,不用配置主库!
我们在讲解配置文件的时候,注意到有一个replication模块 (见Redis.conf中第8条)

  • 查看当前库的信息:info replication
127.0.0.1:6379> info replication
# Replication
role:master # 角色 master:主节点
connected_slaves:0 # 从机数量:0
master_replid:425bbc84c7141a0738b5d0d6cd6920a756e334e4
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>

既然需要启动多个服务,就需要配置多个配置文件。每个配置文件对应修改以下信息:

端口号、pid文件名、日志文件名、rdb文件名

  • 启动单机多服务集群:(一个配置文件可对应启动一个服务)
[root@192 bin]# cd myconfig/
[root@192 myconfig]# ls
redis.conf
[root@192 myconfig]# cp redis.conf redis_master79.conf
[root@192 myconfig]# cp redis.conf redis_sub81.conf 
[root@192 myconfig]# cp redis.conf redis_sub80.conf 
[root@192 myconfig]# vim redis_master79.conf
[root@192 myconfig]# vim redis_sub80.conf 
[root@192 myconfig]# vim redis_sub81.conf 
[root@192 myconfig]# ls
redis.conf  redis_master79.conf  redis_sub80.conf  redis_sub81.conf

1、配置redis配置文件

# redis.conf 【备份不使用,不修改】
# 端口号
port 6379

# 日志文件名
logfile ""

# pid文件名
pidfile /var/run/redis_6379.pid

# rdb文件名
dbfilename dump.rdb
# redis_master79.conf 【做主节点】
port 6379
logfile "6379.log"
pidfile /var/run/redis_6379.pid
dbfilename dump6379.rdb
# redis_sub80.conf 【做从节点1】
port 6380
logfile "6380.log"
pidfile /var/run/redis_6380.pid
dbfilename dump6380.rdb
# redis_sub81.conf 【做从节点2】
port 6381
logfile "6381.log"
pidfile /var/run/redis_6381.pid
dbfilename dump6381.rdb
  • 启动redis
# 启动主节点
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis_master79.conf
# 启动从节点1
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis_sub80.conf
# 启动从节点2
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis_sub81.conf 

# 查看是否启动成功
[root@192 myconfig]# ps -ef | grep redis
root       2031      1  1 09:35 ?        00:00:00 redis-server 127.0.0.1:6379
root       2037      1  0 09:35 ?        00:00:00 redis-server 127.0.0.1:6380
root       2042      1  1 09:35 ?        00:00:00 redis-server 127.0.0.1:6381
root       2047   1977  0 09:35 pts/0    00:00:00 grep --color=auto redis

一主二从配置

默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了!

认老大(认主)!一主(79)二从(80,81)

使用SLAVEOF host port就可以为从机配置主机了。

# 6380
# SLAVEOF host port
# 二个从机 都 认 127.0.0.1 6379 为老大

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
# Replication
role:slave # 角色:奴隶(从属)
master_host:127.0.0.1 # 老大的ip
master_port:6379 # 老大的端口
# 。。。
# 6381
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
# 。。。
# 6378 [主机]
# 然后主机上也能看到从机的状态:
127.0.0.1:6379> info replication
# Replication
role:master # 角色:主机
connected_slaves:2 # 从机数量
slave0:ip=127.0.0.1,port=6381,state=online,offset=322,lag=1  # 从机2信息
slave1:ip=127.0.0.1,port=6380,state=online,offset=322,lag=0  # 从机1信息
# ...

我们这里是使用命令搭建,是暂时的(重启失效),真实开发中应该在从机的配置文件中进行配置,这样的话是永久的。
在这里插入图片描述

  • 配置从机的配置文件认主
################################# REPLICATION #################################
#
#   +------------------+      +---------------+
#   |      Master      | ---> |    Replica    |
#   | (receive writes) |      |  (exact copy) |
#   +------------------+      +---------------+
#
# 配置主机信息
# replicaof <masterip> <masterport>
replicaof 127.0.0.1 6379

# 如果 Redis 有密码需要在配置文件设置主机密码 masterauth ******
# masterauth <master-password>

使用规则

1、从机自动保存主机的所以数据

# === 主机【6379=== 
127.0.0.1:6379> set name 张三
OK
127.0.0.1:6379> set age 25
OK
127.0.0.1:6379> keys *
age
name
127.0.0.1:6379> 

# === 主机【6380=== 
127.0.0.1:6380> keys *
name
age

# === 主机【6381=== 
127.0.0.1:6381> keys *
name
age

2、从机只能读,不能写,主机可读可写但是多用于写。

127.0.0.1:6380> set name 张三 # 从机【6380】写入失败
READONLY You can't write against a read only replica.

127.0.0.1:6381> set name 张三 # 从机【6381】写入失败
READONLY You can't write against a read only replica.

127.0.0.1:6379> set name 张三 # 主机【6379】写入成功
OK
127.0.0.1:6379> get name # 主机【6379】读取成功
张三

127.0.0.1:6381> get name # 从机【6380】读取成功
张三

127.0.0.1:6381> get name # 从机【6381】读取成功
张三

3、当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。

4、当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。

5、第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:

  • 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机
  • 使用哨兵模式(自动选举)

如果没有老大了,这个时候能不能选择出来一个老大呢?手动!

​ 谋朝篡位

​ 如果主机断开了连接,我们可以使用SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,也不能让失去的从节点回归!

复制原理

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

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

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

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

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

层层链路配置

上一个M链接下一个 S! 这时候也可以完成我们的主从复制!

#      6380 依旧是slave(从机)
   +------------------+      +---------------+       +------------------+
   |      6379        | ---> |     6380      | --->  |      6381        |
   | (master 主机)    |      | (master+slave)|       |  (   slave )     |
   +------------------+      +---------------+       +------------------+
  • 配置
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
OK
  • 查看
127.0.0.1:6380> info replication
role:slave # 依旧是slave(从机)
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:3150
slave_priority:100
slave_read_only:1 # 从机的数量
connected_slaves:1 # 连接从机的数量
slave0:ip=127.0.0.1,port=6381,state=online,offset=3150,lag=0 # 6381 从机的信息

Redis哨兵模式【重点】

哨兵:字面上是侦测、巡视,哨兵模式即侦测并自动把一台从服务器切换为主服务器

参考博客:Redis哨兵(Sentinel)模式 - 简书 (jianshu.com)
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工 干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。 谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

哨兵模式概述

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

单哨兵模式
在这里插入图片描述

哨兵的作用:

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

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

多哨兵模式
在这里插入图片描述

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

配置哨兵

1、配置哨兵配置文件 sentinel.conf

# sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

后面的这个数字1,代表投票机制。即主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!

2、启动哨兵!
在这里插入图片描述

[root@192 bin]# redis-sentinel myconfig/sentinel.conf
  • 成功启动哨兵模式

(此时哨兵监视着我们的主机6379,当我们断开主机后):

[root@192 bin]# redis-sentinel myconfig/sentinel.conf
2177:X 27 Jan 2022 16:23:28.599 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2177:X 27 Jan 2022 16:23:28.599 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=2177, just started
2177:X 27 Jan 2022 16:23:28.599 # Configuration loaded
2177:X 27 Jan 2022 16:23:28.600 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.4 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 2177
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

2177:X 27 Jan 2022 16:23:28.601 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2177:X 27 Jan 2022 16:23:28.601 # Sentinel ID is 1633eb8d209a87db8ade04dbd021fcbad073782f
2177:X 27 Jan 2022 16:23:28.601 # +monitor master myredis 127.0.0.1 6379 quorum 1
2177:X 27 Jan 2022 16:28:46.365 # +sdown master myredis 127.0.0.1 6379

#【Tiper】 此时哨兵监视着我们的主机6379,当我们断开主机后,哨兵认为主机断开
2177:X 27 Jan 2022 16:28:46.365 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
2177:X 27 Jan 2022 16:28:46.365 # +new-epoch 2
2177:X 27 Jan 2022 16:28:46.365 # +try-failover master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.367 # +vote-for-leader 1633eb8d209a87db8ade04dbd021fcbad073782f 2
2177:X 27 Jan 2022 16:28:46.367 # +elected-leader master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.367 # +failover-state-select-slave master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.433 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.434 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.508 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.977 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.977 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:47.061 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:47.978 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:47.978 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:48.042 # +failover-end master myredis 127.0.0.1 6379

#【Tiper】 选取随机新的主机
2177:X 27 Jan 2022 16:28:48.042 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6380
2177:X 27 Jan 2022 16:28:48.042 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6380
2177:X 27 Jan 2022 16:28:48.042 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
2177:X 27 Jan 2022 16:29:18.052 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
2177:X 27 Jan 2022 16:35:51.699 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380

# 【Tiper】 如果主机此时回来了,只能归并到新的主机下,当做从机
2177:X 27 Jan 2022 16:36:01.641 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380

如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!

哨兵模式优缺点

优点:

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

缺点:

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

哨兵模式的全部配置

完整的哨兵模式配置文件 sentinel.conf

# Example sentinel.conf
 
# 哨兵sentinel实例运行的端口 默认26379
# 多哨兵的话,要配置多个端口
port 26379
 
# 哨兵sentinel的工作目录
dir /tmp
 
# 哨兵sentinel监控的redis主节点的 ip port 
# master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
 
 
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
 
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
 
 
 
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面: 
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。  
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
 
# SCRIPTS EXECUTION
 
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
 
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
  sentinel notification-script mymaster /var/redis/notify.sh
 
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。 
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

缓存穿透与雪崩【重点】

服务的高可用问题!\

​ Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一 些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。 另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

缓存穿透(没查到)

概念
通俗来讲就是,在缓存中没查到(缓存未命中),还要数据库中进行查找,

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

解决方案

方案1:布隆过滤器

​ 对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
在这里插入图片描述
方案2:缓存空对象

​ 一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
在这里插入图片描述

这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间

即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(量太大,缓存过期)

概念
访问量太大,缓存过期

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

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

解决方案

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

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

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

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

缓存雪崩

概念

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

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

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

解决方案

  • redis高可用

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

  • 限流降级

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

  • 数据预热

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

(服务降级:比如双11,停掉退款业务,全力保证购买业务)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值