【狂神说】Redis

学习资料:
【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili

(72条消息) Redis(基于狂神说Java-Redis)_你给我把被子盖好了,别再踢了的博客-CSDN博客_狂神说redis

Redis 安装 | 菜鸟教程 (runoob.com)

学习目的

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

1、NoSQL简介

1.1 什么是NoSQL

NoSQL翻译为Not Only SQL,译为不仅仅是SQL,意指非关系型数据库

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

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

1.2 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 / Oracle 去IOE化(IOE:IBM、Oracle、EMC存储设备)

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

# 图片
分布式文件系统 FastDFS
淘宝:TFS
Google:GFS
Hadoop:HDFS
阿里云:OSS

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

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

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

1.3 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种分类的对比

在这里插入图片描述

2、Redis入门

本次使用的Redis的版本采用的是5.0.10

2.1 Redis简介

什么是Redis?

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

Redis能干嘛?

  1. 内存存储,持久化,因为内存断电即失,并且Redis支持两种持久化方式,RDB / AOF
  2. 效率高,可用于高速缓存
  3. 消息发布订阅(消息队列)
  4. 地图信息分析
  5. 计数器(eg:微博浏览量)

​ …

特性

  1. 数据类型多样

  2. 持久化

  3. Redis集群

  4. 事务

官网:https://redis.io/

Redis中文文档:http://www.redis.cn/documentation.html

下载地址:进入官网下载即可(Windows版本需要在GitHub上下载,并且Redis版本已停更较长时间,不建议使用)

并且,Redis官方推荐在Linux服务器上进行搭建

2.2 windows 安装 Radis

D:\tool\Redis-x64-5.0.14

【狂神说】Redis_第1张图片

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

1、运行服务

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

redis-server.exe redis.windows.conf

这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。

【狂神说】Redis_第2张图片

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>

2.3 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、同时还需要基本的环境搭建

# 保证Redis的正常运行,gcc的安装也是必要的
yum install gcc-c++

# 查看版本
g++ -v

# 安装Redis所需要的环境
make

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

Redis的安装,默认在/usr/local/bin

在这里插入图片描述

5、之后,需要将Redis的配置文件复制到bin目录下,可以提前准备好一个目录,然后在复制到新创建好的目录中

[root@192 opt]# cd  /opt/redis-5.0.4/
[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

【狂神说】Redis_第3张图片

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

2.4 避免中文乱码

有时候会有中文乱码。

要在 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
张三

2.5 性能测试工具

redis-benchmark性能测试工具

【狂神说】Redis_第4张图片

  • 测试命令
# 当前命令表示,性能测试,在本机,端口号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
# 测试结果如下,以Redis的INLINE命令为例
====== 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
# ...

2.6 Redis基础知识

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

Redis有16个数据库支持,为啥嘞,可以查看redis.conf配置文件
【狂神说】Redis_第5张图片

并且初始数据库默认使用0号数据库(16个数据库对应索引0到15)

  • 可以使用select命令切换数据库:select n(0-15)
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

  • 清空数据库信息
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单线程为什么速度还是这么快?

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

3、基础语法

基础语法(命令小写) 作用
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 选择数据库
基础语法(命令大写):         
SET key value	          设置一个key
GET key                 获取一个key对应value
EXISTS key              查询key是否存在          
MOVE key n(数字)	     将当前key移动到指定的几号数据库中
KEYS *                  查询当前数据库中全部的key
EXPIRE key time         设置当前key的过期时间
TTL key                 查询当前key的存活时间
TYPE key                查看key的数据类型

1、set key value

127.0.0.1:6379> set name 张三 # 设置key-value
OK
127.0.0.1:6379> get name # 查询key指定的value
张三

2、exists key

127.0.0.1:6379> exists name # 查看当前key是否存在
1
127.0.0.1:6379> exists name1
0

3、move

127.0.0.1:6379> move name 1
1
127.0.0.1:6379> KEYS *
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
name

4、expire key time

127.0.0.1:6379[1]> expire name 5 # 设置当前key的过期时间,单位是秒
1
127.0.0.1:6379[1]> ttl name  # 查看指定key的存活时间,
-2 # 返回-2表示当前key已经过期,如果为-1,表示永不过期
127.0.0.1:6379> KEYS * # name 过期被清空
(empty list or set)

5、type key

127.0.0.1:6379[1]> set name 张三
OK
127.0.0.1:6379[1]> type name
string

4、Redis五大数据类型

以下命令记忆小写(命令提示大写),一是好记,二是java中与redis相关的方法与小写命令类似

4.1 String(字符串)

语法 作用
append key Value 对指定key实现字符串拼接,如果key不存在,等同于set
strlen key 查看指定key的长度
incr key 对指定key进行自增,类似于Java中的i++
decr key 自减,类似于Java的i–
incrby key n 对指定key按照指定的步长值进行自增
decrby key n 按照指定的步长值自减
setrange key index value 从指定key的索引开始,插入指定的value值。
getrange key index value 将指定key按照索引的开始和结束范围进行截取,成为一个新的key
setex key time value 设置一个有存活时间的key
setex key value 如果这个key不存在,即创建
mset key value … 设置多个key value
mget key value 获取多个key指定的value
getset key value 先获取指定的key,然后再设置指定的value
# 语法:
APPEND key appendValue      # 对指定key实现字符串拼接,如果key不存在,等同于set
STRLEN key                  # 查看指定key的长度
INCR key                    # 对指定key进行自增,类似于Java中的i++
DECR key                    # 自减,类似于Java的i--
INCRBY key n                # 对指定key按照指定的步长值进行自增
DECRBY key n				# 按照指定的步长值自减
SETRANGE key index value          #  从指定key的索引开始,插入指定的value值。如果key不存	在且索引>1,那么当前的索引之前的数据,会用\x00代替并占用一个索引位置,相当于ASCII码中的null

GETRANGE key startIndex endInde  #将指定key按照索引的开始和结束范围进行截取,成为一个新的key
SETEX key time value	   # 设置一个有存活时间的key
SETNX key value            # 如果这个key不存在,即创建
MSET key value ...	       #设置多个key value
MGET key ...			   #获取多个key指定的value
GETSET key value          # 先获取指定的key,然后再设置指定的value

1、简单使用

127.0.0.1:6379[1]> set name ab
OK
127.0.0.1:6379[1]> append ab cd
2
127.0.0.1:6379[1]> get name
ab
127.0.0.1:6379[1]> strlen name
2

2、实现自增自减效果

  • 注意:value的自增和自减只适用于Integer类型
127.0.0.1:6379[1]> incr age
26
127.0.0.1:6379[1]> decr age
25
127.0.0.1:6379[1]> incrby age 10
35
127.0.0.1:6379[1]> decrby age 24
11

3、实现字符串截取效果

127.0.0.1:6379> set k1 hello,xiaohuang
OK
127.0.0.1:6379> get k1
"hello,xiaohuang"
127.0.0.1:6379> GETRANGE k1 0 3 # 实现字符串截取,有起始索引和结束索引,相当于Java中的subString()
"hell"
# 如果结束索引为-1,则表示当前截取的字符串为全部
127.0.0.1:6379> GETRANGE k1 0 -1
"hello,xiaohuang"

4、实现字符串的替换效果

127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 2 hello # 实现字符串的替换效果,命令中的数字“2”表示从索引2的位置开始将其替换为指定字符串
(integer) 7
127.0.0.1:6379> get key2
"abhello"

5、设置过期时间

# setex(set with expire) # 设置过期时间
# setnx(set with not exist) # 如果key不存在,创建(分布式锁中常用)
127.0.0.1:6379[1]> setex name 10 张三
OK
127.0.0.1:6379[1]> keys *
name
127.0.0.1:6379[1]> get name

127.0.0.1:6379[1]> keys *
127.0.0.1:6379> setnx lan redis # 如果key不存在,即创建
(integer) 1 
127.0.0.1:6379> setnx lan mongodb
(integer) 0 # 0表示没有设置成功,也可理解为“有0行受到影响”
127.0.0.1:6379> get lan
"redis"

6、一次性设置(获取)多个键值对

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> KEYS * 
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
# 也可以在这边的语法前面加上一个m,代表设置多个
127.0.0.1:6379> msetnx k1 vv1 k4 v4 
(integer) 0
# 但是这边同时设置多个值,如果有一个key已经存在,那么这一条设置语句便不会执行成功,
# 因为Redis单条语句是原子操作,要么同时成功,要么同时失败
127.0.0.1:6379> keys * 
1) "k2"
2) "k1"
3) "k3"
# 在Redis中,还推荐了一个比较有意思的东西
# 这是Redis中关于key的命名,可以用“:”来代表层次结构,可以对指定的key进行分类存储
127.0.0.1:6379> mset user:1:name xiaohuang user:1:age 21
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "xiaohuang"
2) "21"
127.0.0.1:6379> getset sqlan redis # 先获取当前key指定的value,如果不存在,会返回nil(null),然后设置新值
(nil)
127.0.0.1:6379> get sqlan
"redis"
127.0.0.1:6379> getset sqlan hbase
"redis"
127.0.0.1:6379> get sqlan
"hbase"

类似于Redis中String这样的使用场景,value值可以是字符串,也可以是其他类型

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

主要用途

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

4.2 List(列表)

Redis中的List列表可以做很多事情,可以将其看成数据结构中的栈,也可以是队列,或者阻塞队列

命令 作用 关键字
LPUSH key value1 value2 … 设置一个key,从头部插入数据(头插法) lpush
RPUSH key value1 value2 … 设置一个key,从尾部插入数据(尾插法) rpush
LRANGE key startIndex endIndex 返回列表中从开始索引到结束索引位置的value值 lrange
LPOP key 从key头部弹出一个value lpop
RPOP key 从尾部弹出一个value rpop
LINDEX index 返回key中指定索引的value值 lindex
LREM key n value 删除key中的指定的value值,n代表删除几个 lrem
LLEN key 返回key的长度 llen
LTRIM key startIndex endIndex 截取key,截取的范围从开始索引到结束索引 ltrim
LSET key index value 从当前key的索引开始插入指定的value值 lset
RPOPLPUSH key1 key2 从key1的尾部弹出一个元素,将此元素从key2的头部插入 (转移)
LINSERT key BEFORE 从指定key中已存在的value的前面插入一个指定的value
AFTER oldValue newValue 从指定key后面插入一个指定的value

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

4.3 set(集合)

set集合无序不重复

命令 作用 关键词
SADD key value1 value2 … 设置一个key sadd
SMEMBERS key 查看当前key smembers
SISMEMBER key value 查看key中指定的value是否存在 sismember
SCARD key 查看key的长度 scard
SREM key value 删除key中的指定value srem
SPOP key 随机删除key中的一个value spop
SRANDMEMBER key [n] 随机查看当前key中的一个或者多个value srandmember
SMOVE key1 key2 key1Value 将key1中的value移动到key2中 smove
SDIFF key1 key2 两个key相交,求第一个key的补集 sdiff
SINTER key1 key2 两个key相交,求交集 sinter
SUNION key1 key2 两个key相交,求并集 sunion

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,翻译为联合,与数学关系中的并集也是可以沾边的

4.4 Hash(哈希)

Redis中的哈希,本质上KV相同但是KV中的V,它也是一个键值对,本质和操作字符串区别不大

命令 作用 关键字
HSET key field value 设置单个hash hset
HGET key field 获取单个 hget
HMSET key field1 v1 field2 v2 设置多个 hmset
HMGET key field 获取多个 hmget
HGETALL key 获取hash中全部的field-value hgetall
HLEN key 获取hash长度 hlen
HEXISTS key field 查询hash中指定的field是否存在 hexists
HKEYS key 只获取hash中的field hkeys
HVALS key 只获取hash中value hvals
HINCRBY key field n 对hash中指定的field设置自增自减 hincrby

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"

4.5 Zset(有序集合)

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

命令 作用 关键字
ZADD key score1 value1 score2 value2 … zset中添加一个或多个元素 zadd
ZRANGE key startIndex endIndex 查询从开始到结束索引的zset集合 zrange
ZRANGEBYSCORE key min max [WITHSCORES] 对hash中按照指定数值进行升序排列
ZREVRANGE key startIndex endIndex 对指定开始和结束索引进行降序排列
ZREM key field 删除hash中指定的field zrem
ZCARD key 查询hash长度 zcard
ZCOUNT key [min max] 查询hash数量,还可以增加最大值和最小值的范围 zcount

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站会根据视频的浏览量和弹幕量进行综合评分,进行排名

5、三种特殊数据类型

5.1 geospatial 地理位置

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

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

对于这个关于地理的数据类型,它有6个命令

关键词 命令 作用
geoadd GEOADD key 经度 纬度 城市名称 添加地理位置
geodist GEODIST key member1 member2 [unit] 指定两个位置的距离
geohash GEOHASH key member 返回一个或多个元素的GeoHash表示,该命令返回11个字符组成的GeoHash字符串
geopos GEOPOS key member1 member2 … 获取一个或多个地理信息
georadius GEORADIUS key 经度 纬度 半径 [单位] [WITHCOORD(搜寻到的目标的经纬度)] [WITHDIST(直线距离)] [count] 自己所在的地址为圆心,半径查找
georadiusbymember GEORADIUSBYMEMBER key member 长度 [unit]单位 找出指定元素周围的其他元素,就是以城市为中心,一定长度为半径搜索

因为这个特殊的数据类型和地理相关,需要用到地理的经纬度,可以推荐一个网站查看指定城市的经纬度: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.2 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任务,可以忽略不计

关键词 命令 作用
pfadd PFADD key value1 value2… 创建一组数据集,如果数据集中有相同的元素就会有去重效果
pfcount PFCOUNT key 查看元素的长度
pfmerge PFMERGE key3 key1 key2 将两组元素合并成一个新数组,并带有去重效果,相当于数学中的并集
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来实现

5.3 Bitmaps

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

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

Bitmaps,译为位图,也是一种数据结构,操作二进制位进行记录,只有0和1两种状态。Bitmaps通过最小的单位bit来进行存储,表示元素对应的状态

关键词 命令 作用
setbit SETBIT key offset value 设置一个key,在指定的offset位置上设置一个value,这个value只能是0或者1
getbit GETBIT key offset 获取指定key上的offset位的value值
bitcount BITCOUNT key [start] [end] 在指定key中计算被设置为 1 的比特位的数量。
bitop BITOP operation destKey key1 key2 … 对一个或者多个key进行二进制的逻辑运算
bitpos BiTPOS key bit [start] [end] 指定key中返回value中第一个出现0或1的offset

1、

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

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

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节点进行处理,避免阻塞

6、事务

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

原子性: 要么都成功,要么都失败

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

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

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

Redis事务的三个特性:一致性,顺序性,排他性

关键字 命令 作用
multi MULTI 开启事务
exec EXEC 执行事务
discard DISCARD 关闭事务(放弃事务)

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 # 依旧执行

7、Redis监视测试

监控

悲观锁

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

乐观锁

  • 很乐观,任何情况下都不会出问题,所以不会加锁!但是在数据更新时需要判断在此之前是否有人修改过这个数据
  • 可以添加一个字段叫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,还有一个命令,UNWATCH,意思是解除所有监控,但是官网的原话是,一旦你执行了DISCARD或者EXEC,就没必要在执行UNWATCH

8、Jedis

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

8.1 测试连接

8.1.1 创建项目

创建Moven项目,导入依赖


<dependency>
  <groupId>redis.clientsgroupId>
  <artifactId>jedisartifactId>
  <version>3.2.0version>
dependency>

<dependency>
  <groupId>com.alibabagroupId>
  <artifactId>fastjsonartifactId>
  <version>1.2.70version>
dependency>

8.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

8.1.3 解决远程连接问题

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

打开redis.conf配置文件

【狂神说】Redis_第6张图片

8.1.4 API测试

随后进行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

【狂神说】Redis_第7张图片

8.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"}]

8.2 常用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"}));
    }
}

9、SpringBoot整合Redis

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

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

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

9.1 整合Redis

1、创建SpringBoot项目

【狂神说】Redis_第8张图片

2、分析源码

首先先查看RedisAutoConfiguration中的源码

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
// @Import注解导入了两个配置类,有Lettuce和Jedis,
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    
  @Bean
  @ConditionalOnMissingBean(name = "redisTemplate") //如果redisTemplate存在,则方法不生效
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
        throws UnknownHostException {
     // 默认的RedisTemplate直接使用此类内部默认设置操作数据,但是Redis对象需要序列化
     // 泛型都是Object,后面使用的话,大都是RedisTemplate
     RedisTemplate<Object, Object> template = new RedisTemplate<>();
     template.setConnectionFactory(redisConnectionFactory);
     return template;
  }

  @Bean
  @ConditionalOnMissingBean
  public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
        throws UnknownHostException {
     StringRedisTemplate template = new StringRedisTemplate();
     template.setConnectionFactory(redisConnectionFactory);
     return template;
  }
}

上面的@Import注解导入了两个配置类,有Lettuce和Jedis,可以点开这两个类查看

【狂神说】Redis_第9张图片

【狂神说】Redis_第10张图片

对比一下可以发现,Jedis配置类中有两个类是默认不存在的,不存在就无法使用

3、配置

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

4、常用方式

三种常用方式

  • 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();

5、测试

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

9.2 json

开发中一般用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}

3、这样也行

User user = new User("张三", 25);
redisTemplate.opsForValue().set("user", String.valueOf(user));
System.out.println(redisTemplate.opsForValue().get("user"));
  • 结果
User(name=张三, age=25)

4、 推荐实体类序列化

public class User implements Serializable {
    private String name;
    private int age;
}

9.3 序列化

@SpringBootTest
class ApplicationTests {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("myKey", "张三");
        System.out.println(redisTemplate.opsForValue().get("myKey"));
    }
}
127.0.0.1:6379> get myKey
寮犱笁

客户端中文会取得乱码,这与RedisTemplate默认序列化有关

先展示RedisTemplate的部分源码

// 这些是RedisTemplate的序列化配置
private @Nullable RedisSerializer keySerializer = null;
private @Nullable RedisSerializer valueSerializer = null;
private @Nullable RedisSerializer hashKeySerializer = null;
private @Nullable RedisSerializer hashValueSerializer = null;

@Override
public void afterPropertiesSet() {
  super.afterPropertiesSet();
  boolean defaultUsed = false;
  if (defaultSerializer == null) {
    // 这边默认使用JDK的序列化方式,我们可以自定义一个配置类,采用其他的序列化方式
    defaultSerializer = new JdkSerializationRedisSerializer(
      classLoader != null ? classLoader : this.getClass().getClassLoader());
   }
	}
}

自定义RedisConfig

package com.example.springboot.config;

//...

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        //  RedisTemplate对象,使用更方便
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);

        //Json的序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String的序列化配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }
}

9.4 RedisUtil工具类

RedisUtil工具类
在SpringBoot中,如要操作Redis,就需要一直调用RedisTemplate.opsxxx的方法,一般在工作中不会去这样使用,公司里都会内部将这些操作数据类型的API进行一个封装,就像在学JDBC还有Mybatis等框架的时候,都会有一个XxxUtil的Java工具类,使用起来比较简单

这边推荐一个GitHub:https://github.com/iyayu/RedisUtil.git

9.4.1 自定义工具类

package com.example.springboot.utils;

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

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

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

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


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


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


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

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

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

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


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

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


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


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


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

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

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

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


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


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

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


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


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


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


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


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

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


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


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


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


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


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

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

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

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


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


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


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


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

    }


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

    }


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


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

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


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

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

    }

}

9.4.2 使用工具类

@Autowired
private RedisUtil redisUtil;

@Test
void test() {
    redisUtil.set("name","张三");
    System.out.println(redisUtil.get("name"));

}
  • 结果
张三

10、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

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

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  

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

7、SECRULITY

​ 安全,可以在配置文件中设置Redis的登录密码

8、CLIENT

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

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

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

处理策略有以下几种

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

9、APPEND ONLY MODE

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

appendonly no

AOF持久化的文件名称

appendfilename "appendonly.aof"

每秒执行一次同步,但是可能会丢失这一秒的数据

# 对于 appendfsync 它有以下几个属性 
# appendfsync always 表示每次修改都会进行数据同步,速度较慢,消耗性能
# appendfsync no 不执行同步,不消耗性能
appendfsync everysec # 数据不同步,每秒记录一次

11、Redis持久化

为什么Redis要实现持久化

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

11.1 RDB

​ RDB,全称Redis DataBase。什么是RDB,在指定的时间间隔内将数据集快照写入到磁盘中,在恢复数据的时候将这些快照文件读取到内存中,对应的配置文件中的SNAPSHOTTING,可以查看在上面提到的Redis的配置文件

​ Redis会单独创建出一个子进程(fork)来进行持久化,会将数据写入到一个临时文件中,待持久化操作结束之后,临时文件会将已经持久化完成的文件替换掉,在这个过程中,主进程不进行任何IO操作,这也就确保RDB极高的性能,相比于RDB和AOF,RDB的模式会比AOF更加的高效。如果在进行大数据恢复,并且对于数据的精度要求不高,那么就可以使用RDB,Redis的持久化默认的也是RDB,在一般情况下(生产环境)不需要修改这个配置

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

​ RDB保存的文件就是dump.rdb文件

# 配置文件
# The filename where to dump the DB
dbfilename dump.rdb

在这里插入图片描述

RDB

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

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

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

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

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

如何进行数据恢复

1、首先使用一个Redis命令查看持久化文件保存的位置

# config get dir
# linux
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/bin"

2、然后将dump.rdb文件放到Redis的启动目录下即可

优点:

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

缺点

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

11.2 AOF

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

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

  • 启动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

如果说appendonly.aof文件的内容发生了一些错误,那么在Redis进行启动时,会出现问题

[root@bogon bin]# redis-server myconfig/redis.conf 
1574:C 16 Nov 2020 03:47:18.350 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1574:C 16 Nov 2020 03:47:18.350 # Redis version=5.0.10, bits=64, commit=00000000, modified=0, pid=1574, just started
1574:C 16 Nov 2020 03:47:18.350 # Configuration loaded
[root@bogon bin]# redis-cli -p 6379
# 下面的这一个返回结果,表示连接被拒绝
Could not connect to Redis at 127.0.0.1:6379: Connection refused 
not connected> exit

如果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

Redis优缺点

优点:

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

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

缺点:

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

AOF的重写机制

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

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

原理

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

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

可以在配置文件中查看重写的信息

auto-aof

# 自动重写只追加文件。
# Redis能够自动重写日志文件隐式调用
# BGREWRITEAOF当AOF日志大小以指定的百分比增长时。
# 这是它的工作原理:Redis会记住AOF文件的大小
# 在启动时使用AOF)。
#
#基本大小与当前大小相比较。如果当前大小为大于指定百分比时,重写被触发。也
#你需要指定AOF文件被重写的最小大小
#对于避免重写AOF文件非常有用,即使百分比增加了
#已经达到,但仍然很小。
#
#指定一个百分比为零,以禁用自动AOF
#重写特性。

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

【狂神说】Redis_第11张图片

总结

  • RDB可以在指定的时间间隔内对数据集进行持久化快照存储
  • AOF持久化记录每次客户端发送给Redis服务器的写操作,服务器中重启时会重新执行命令恢复原始数据,AOF持久化的每一次记录都会追加在文件的末尾,并且Redis有重写机制的存在使得AOF的文件被控制在合理的大小
  • 如果Redis只做缓存,如果说只希望数据在服务器启动的时候存在,可以不使用任何的持久化方式
  • 刚刚上面讲到一个小细节,如果两种持久化同时开启,Redis服务器会默认先找AOF持久化,因为AOF的保存数据集要比RDB要完整,这也就是Redis考虑安全的原因

12、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…

13、Redis主从复制

13.1 概念

​ 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。

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

作用

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

为什么使用集群

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

13.2 环境配置

我们在讲解配置文件的时候,注意到有一个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

13.3 一主二从配置

默认情况下,每台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  
replicaof 127.0.0.1 6379

# 如果 Redis 有密码需要在配置文件设置主机密码 masterauth ******
# masterauth 

13.4 使用规则

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 从机的信息

14、哨兵模式【重点】

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

参考博客:Redis哨兵(Sentinel)模式 - 简书 (jianshu.com)

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

14.1 哨兵模式概述

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

单哨兵模式

【狂神说】Redis_第12张图片

哨兵的作用:

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

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

多哨兵模式

【狂神说】Redis_第13张图片

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

14.2 配置哨兵

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

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

哨兵模式优缺点

优点:

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

缺点:

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

哨兵模式的全部配置

完整的哨兵模式配置文件 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    
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在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无法正常启动成功。
#通知脚本
# 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

15、缓存穿透与雪崩【重点】

服务的高可用问题!

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

15.1 缓存穿透(没查到)

概念

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

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

解决方案

方案1:布隆过滤器

​ 对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。

在这里插入图片描述

方案2:缓存空对象

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

在这里插入图片描述

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

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

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

概念

​ 访问量太大,缓存过期

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

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

解决方案

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

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

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

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

15.3 缓存雪崩

概念

大量的key设置了相同的过期时间,导致大量缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

在这里插入图片描述

解决方案

  • redis高可用

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

  • 限流降级

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

  • 数据预热

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

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

你可能感兴趣的:(程序开发,web开发,学习笔记,redis,nosql,spring,boot)