1. 📩NoSQL概述 1.1 📃简介
⚡ NoSQL
NoSQL != 非SQL
NoSQL == Not Only SQL
不仅仅是SQL!
泛指非关系型的数据库。克服大并发。
很多的数据类型,用户的个人信息、社交网络和地理位置,这些数据类型的存储不需要一个固定的格式,不需要多元的操作就可以横向扩展。
1.2 🌀特点
方便扩展(数据之间没有关系,很好扩展)
大数据量高性能(细粒度缓存,性能高)
数据类型多样(不需要设计数据库,随取随用)
RDBMS和NoSQL的区别:
RDBMS
结构化组织
SQL
数据和关系都存在单独的表中
严格的一致性
基础的事务
···
NoSQL
不仅仅是数据
没有固定的查询语言
键值对存储,列存储,文档存储,图形存储
最终一致性
CAP和BASE
三高:高性能、高可用、高可扩展
···
1.3 🚀3V+3H
大数据时代的3V
海量 Volume
多样 Variety
实时 Velocity
互联网需求的3H
高并发 High concurrency
高可拓 High scalable
高性能 High performance
1.4 📚分类
😭呜呜呜,我好菜,我啥都不会🍼
分类
举例
典型应用场景
数据模型
优点
缺点
键值对
Tokyo Cabinet/Tyrant,Redis,Voldemort,Oracle BDB
内容UAN,主要用于处理大量数据的高访问负载,也用于一些日志系统等等
Key指向value的键值对,通常用hash table来实现
查找速度快
数据无结构化,通常只被当做字符串或者二进制数据
列存储数据库
Cassandra,HBase,Riak
分布式的文件系统
以列簇式存储,将同一列数据存在一起
查找速度快,可扩展性强,更容易进行分布式扩展
功能相对局限
文档型数据库
CouchDB,MongoDB
Web应用
Key-Value对应的键值对,Value为结构化数据
数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构
查询性能不高,而且缺乏统一的查询语言
图形数据库
Neo4J,InfoGrid,Infinite Graph
社交网络、推荐系统等等,专注于构建关系图谱
图结构
利用图结构相关算法。比如最短路径寻址,N度关系查找等
很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方案
1.5 📈阿里巴技术演进
✡技术并无高低之分,就看你如何使用
2. 📩Redis入门
🌐 official website
2.1 📑简介
💡 Redis = Remote Dictionary Server
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)^(来自官方文档)^。
2.2 🌠特性
2.3 🔰拓展
Redis 🆚 Memcache
存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性
数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,而redis支持五大数据类型和三大特殊数据类型
底层模型上:它们之间底层实现方式以及与客户端之间的应用协议不一样。redis直接构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
value的大小:redis可以达到1GB,而memcache只有1MB
3. 📩Redis安装
⚠️ notice
Github上redis的windows版本已经很久不再更新,对于最新的3.2.100版本,个人使用过,redis-cli.exe使用起来偶尔会出问题,命令写出来那一行会变成黑色,兼容性不太好,由于3.0不支持GEO等操作,我还是选择使用3.2.100版本。
Redis这种高性能服务器本身与CentOS的体质就很般配,个人推荐在Linux上安装,尤其是后期搭建redis集群环境。CentOS7本身自带的yum镜像中带的gcc安装包只有4.8.5版本,不支持高版本redis的编译,所以推荐下载5.0.8版本。
以上,不管是Windows还是Linux,都推荐使用Xshell开启Redis服务器和客户端。
🔽Xshell
3.1 💻Windows10 安装
下载: redis
解压
启动
3.2 💻CentOS7 安装
安装gcc: yum install gcc-c++ tcl
注意安装 version>6 的redis需要 version>5 的gcc:
sudo yum install centos-release-scl
sudo yum install devtoolset-7-gcc*
scl enable devtoolset-7 bash
下载压缩包: wget http://download.redis.io/releases/redis-5.0.8.tar.gz
解压压缩包: tar xzf redis-5.0.8.tar.gz
跳转目录: cd redis-5.0.8
编译安装: make
再次编译: make
最后安装:
cd src/
make install
查看结果: ll /usr/local/bin/
更改配置:
新建配置文件目录: mkdir kconfig
将原生Redis配置文件复制进来: cp /home/parak/Redis/redis-5.0.8/redis.conf
修改配置文件: gedit redis.conf
测试启动: redis-server kconfig/redis.conf
查看redis进程: ps -ef | grep redis
关闭redis服务: shutdown
4. 📩Redis配置 4.1 🚩命令
👀查看所有配置项
✏命令行编辑配置
1 config set <option> <value>
4.2 📝redis.conf 配置项说明
序号
配置项
说明
1
daemonize no
Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程(Windows 不支持守护线程的配置为 no )
2
pidfile /var/run/redis.pid
当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定
3
port 6379
指定 Redis 监听端口,默认端口为 6379
4
bind 127.0.0.1
绑定的主机地址
5
timeout 300
当客户端闲置多长秒后关闭连接,如果指定为 0 ,表示关闭该功能
6
loglevel notice
指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice
7
logfile stdout
日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null
8
databases 16
设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
9
save
Redis 默认配置文件中提供了三个条件:save 900 1 、save 300 10 、save 60 10000 分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
10
rdbcompression yes
指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大
11
dbfilename dump.rdb
指定本地数据库文件名,默认值为 dump.rdb
12
dir ./
指定本地数据库存放目录
13
slaveof
设置当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步
14
masterauth
当 master 服务设置了密码保护时,slav 服务连接 master 的密码
15
requirepass foobared
设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH 命令提供密码,默认关闭
16
maxclients 128
设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息
17
maxmemory
指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区
18
appendonly no
指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no
19
appendfilename appendonly.aof
指定更新日志文件名,默认为 appendonly.aof
20
appendfsync everysec
指定更新日志条件,共有 3 个可选值:no :表示等操作系统进行数据缓存同步到磁盘(快)always :表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)everysec :表示每秒同步一次(折中,默认值)
21
vm-enabled no
指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析 Redis 的 VM 机制)
22
vm-swap-file /tmp/redis.swap
虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享
23
vm-max-memory 0
将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0
24
vm-page-size 32
Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值
25
vm-pages 134217728
设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,,在磁盘上每 8 个 pages 将消耗 1byte 的内存。
26
vm-max-threads 4
设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
27
glueoutputbuf yes
设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
28
hash-max-zipmap-entries 64 hash-max-zipmap-value 512
指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
29
activerehashing yes
指定是否激活重置哈希,默认为开启(后面在介绍 Redis 的哈希算法时具体介绍)
30
include /path/to/local.conf
指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
4.3 🔍 重点详解
UNIT: redis对大小写不敏感
INCLUEDS[模块]: 可以包含多个配置文件
MOUDLES[模块]: 启动时加载模块
NETWORK[网络]:
bind: 绑定IP
protected-mode: 保护模式
post: 端口设置
GENERAL[通用]:
daemonize: 是否以守护进程的方式运行[^1]
pidfile /var/run/redis_6379.pid: 如果以后台的方式运行,就需要指定一个pid的配置文件
loglevel: 日志级别
logfile: 日志的文件位置
database: 数据库的数量
always-show-logo: 是否开启服务的时候显示logo
SNAPSHOTTING[快照]:
save 900 1: 如果在900s内,至少有1个key进行了修改,就进行持久化操作
save 300 10: 如果在300s内,至少有10个key进行了修改,就进行持久化操作
save 60 10000: 如果在60s内,至少有10000个key进行了修改,就进行持久化操作
stop-writes-on-bgsave-error: 持久化出现错误,是否让redis继续工作
rdbcompression: 是否压缩rdb文件,需要消耗一些CPU资源
rdbchecksum: 保存rdb的文件的时候,是否进行错误校验
dir: 文件保存的目录
REPLICATION[复制]:
SECURITY[安全]:
🔔注意: 这个命令是在redis目录下执行,而非redis客户端的内部命令
5.2 📝redis性能测试工具可选参数
序号
选项
描述
默认值
1
-h
指定服务器主机名
127.0.0.1
2
-p
指定服务器端口
6379
3
-s
指定服务器 socket
4
-c
指定并发连接数
50
5
-n
指定请求数
10000
6
-d
以字节的形式指定 SET/GET 值的数据大小
3
7
-k
1=keep alive 0=reconnect
1
8
-r
SET/GET/INCR 使用随机 key, SADD 使用随机值
9
-P
通过管道传输 请求
1
10
-q
强制退出 redis。仅显示 query/sec 值
11
–csv
以 CSV 格式输出
12
-l
生成循环,永久执行测试
13
-t
仅运行以逗号分隔的测试命令列表。
14
-I
Idle 模式。仅打开 N 个 idle 连接并等待。
5.3 📊测试结果分析 1 redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
1 redis-benchmark -h 127.0.0.1 -p 6379 -c 1 -n 100000 -q
这个是对所有操作测试性能,每秒处理的请求数量。
6. 📩Redis基础 6.1 💠Redis数据库 redis有16个数据库,默认使用第0个
测试连接
关闭连接
返回消息
切换数据库
1 select <num of database>
获取当前数据库的大小
清空当前数据库
清空所有数据库
交换数据库
6.2 🌏6379的故事 redis默认端口号为6379
作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口,因为 6379 在手机按键上 MERZ 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字。MERZ长期以来被Redis作者antirez及其朋友当作愚蠢的代名词,后来作者在开发Redis就选用了这个端口。
6.3 ⚡Redis蜜汁速度 redis是单线程的。
redis基于内存操作,CPU不是redis的性能瓶颈,Redis的瓶颈很可能是机器内存或者网路带宽。
既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章的采用单线程实现。
💉理解Redis蜜汁速度需要跨过两个误区
误区1:高性能的服务器一定是多线程的?
误区2:多线程的效率一定比单线程高?
💊Redis采用单线程依然快的原因
Redis完全基于内存,读写全部在一个CPU上,绝大部分请求是纯粹的内存操作,非常迅速,数据存在于内存中,类似于HashMap,HashMap的优势就是查询和操作的时间复杂度时O(1)
数据结构简单,对数据操作也简单
采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的CPU切换,不用取考虑各种锁的问题,不存在加锁放锁操作,没有死锁问题导致的性能消耗
使用多路复用IO模型,非阻塞IO
7. 📩Redis数据类型
🌞说明
所有命令可查看中文官方文档: http://redis.cn/commands.html#
7.1 🏆五大数据类型
类型
简介
特性
场景
String(字符串)
二进制安全
可以包含任何数据,比如jpg图片或者序列化对象
—
Hash(字典)
键值对集合
适合存储对象,并且可以像数据库中的update一个属性一样值修改某一项属性值
存储、读取、修改用户属性
List(列表)
双向链表
增删快,提供了操作某一元素的api
最新消息排行;消息队列
Set(集合)
hash表实现,元素不重复
增删查快,提供了求交集、并集和差集的操作
共同好友: 利用唯一性,统计网站UV
Sorted Set(有序集合)
将set中的元素增加一个权重score,元素按照score有序排列
数据插入集合时,已经进行了天然排序
排行榜;带权重的消息队列
🎲Key
查看所有的key
创建键值对
获取key的值
移除键值对
判断key是否存在
查看key的类型
设置key的过期时间/秒
获取key的有效时间/秒
获取key的有效时间/毫秒
7.1.1 ⚽String
向key上追加字符串
获取key的长度
Integer操作
1 2 3 4 5 6 7 8 # 加1 incr <key> # 加n incrby <key> n # 减1 decr <key> # 减n decrby <key> n
subString(start, end)操作
1 2 3 4 5 6 7 8 # 截取整个字符串 getrange <key> 0 -1 # 截取部分字符串 getrange <key> start end # 例如 > set s "Khighness" > getrange s 0 -1 > getrange s 1 4
replace(start, end)操作
1 2 3 4 5 6 7 8 # 把字符串从n位开始之后的字符替换为新的字符串newStr setrange <key> n newStr # 例如 > set s "Khighness" > setrange s 0 X > get s > setrange s 5 "XXXXX" > get s
setex (set with expire) 创建键值对的同时设置过期时间
setnx (set if not exist) 如果key不存在则创建键值对,防止覆盖原有键值对 (分布式锁中经常使用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # 设置键值对,设置过期时间 setex <key> <seconds> <value> # key不存在,则创建键值对 setnx <key> <value> # 例如 127.0.0.1:6379> setex k1 10 parak OK 127.0.0.1:6379> ttl k1 (integer) 5 127.0.0.1:6379> ttl k1 (integer) 3 127.0.0.1:6379> ttl k1 (integer) 2 127.0.0.1:6379> ttl k1 (integer) 2 127.0.0.1:6379> ttl k1 (integer) 1 127.0.0.1:6379> ttl k1 (integer) -2 127.0.0.1:6379> get k1 (nil) 127.0.0.1:6379> setnx k2 parak (integer) 1 # 1代表设置成功 127.0.0.1:6379> setnx k2 flowerk (integer) 0 # 0代表设置失败 127.0.0.1:6379> get k2 "parak" 127.0.0.1:6379> setnx k2 FlowerK (integer) 0 127.0.0.1:6379> set k2 FlowerK OK # 强制设置value 127.0.0.1:6379> get k2 "FlowerK"
多个键值对操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 一次性创建多个键值对 mset <key> <value> [key value ...] # 获取多个key的值 meget <key> [key ...] # 不存在则创建多个键值对 # 原子性操作,只要其中有一个key已存在,就会全部创建失败 msetnx <key> <value> [key value ...] # 例如 127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 OK 127.0.0.1:6379> mget k1 k2 k3 1) "v1" 2) "v2" 3) "v3" 127.0.0.1:6379> msetnx k2 v2 k4 v4 k5 v5 (integer) 0 127.0.0.1:6379> keys * 1) "k2" 2) "k3" 3) "k1" # 巧妙设计key object:{id }:{field} 127.0.0.1:6379> mset user:1:name Khighness user:1:age 18 OK 127.0.0.1:6379> mget user:1:name user:1:age 1) "Khighness" 2) "18"
组合操作
1 2 # 先获取值,再设置新的值 getset k v
7.1.2 ⚾List redis里面,list可以当成栈、队列、队列。
向list的头部添加值
1 lpush <key> value [value ...]
向list的尾部添加值
1 rpush <key> value [value ...]
判断list是否存在
移除列表的第一个元素
移除列表的最后一个元素
移除指定的值
1 lrem <key> <count> <value>
更新list
1 2 # 根据index更新值 lset <key> <index> <value>
根据下标获取值
获取list的长度
获取list指定范围的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 获取整个list的值 lrange <key> 0 -1 # 获取指定范围的值 lrange <key> start end # 例如 127.0.0.1:6379> lpush list1 1 2 3 4 5 (integer) 5 127.0.0.1:6379> lrange list1 0 -1 1) "5" 2) "4" 3) "3" 4) "2" 5) "1" 127.0.0.1:6379> lrange list1 0 1 1) "5" 2) "4" 127.0.0.1:6379> rpush list2 1 2 3 4 5 (integer) 5 127.0.0.1:6379> lrange list2 0 -1 1) "1" 2) "2" 3) "3" 4) "4" 5) "5" 127.0.0.1:6379> lrange list2 0 1 1) "1" 2) "2"
截取list中指定范围的值
1 2 3 4 5 6 7 8 9 10 11 12 # 保留下标[start, end]的值 ltrim <key> start end # 例如 127.0.0.1:6379> rpush list parak1 parak2 parak3 parak4 parak5 (integer) 5 127.0.0.1:6379> ltrim list 0 2 OK 127.0.0.1:6379> lrange list 0 -1 1) "parak1" 2) "parak2" 3) "parak3"
组合操作
1 2 3 4 5 6 7 8 9 10 # 移除source 的尾部的值插入到destination的头部 rpoplpush <source> <destination> # 例如 127.0.0.1:6379> rpush list 1 2 3 4 5 (integer) 5 127.0.0.1:6379> rpoplpush list newlist "5" 127.0.0.1:6379> lrange newlist 0 -1 1) "5"
在list中插入值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 在list中的某个值之前插入 linsert <key> before <priot> <value> # 在list中的某个值之后插入 linsert <key> after <priot> <value> # 例如 127.0.0.1:6379> rpush list 1 2 3 4 5 (integer) 5 127.0.0.1:6379> linsert list before 3 6 (integer) 6 127.0.0.1:6379> lrange list 0 -1 1) "1" 2) "2" 3) "6" 4) "3" 5) "4" 6) "5" 127.0.0.1:6379> linsert list after 5 7 (integer) 7 127.0.0.1:6379> lrange list 0 -1 1) "1" 2) "2" 3) "6" 4) "3" 5) "4" 6) "5" 7) "7"
7.1.3 🏀Set set 无序不重复集合
set通过哈希表实现,所有增删查的时间复杂度是O(1)
向set中国添加值
1 sadd <key> <value> [value ...]
查看set中的所有值
查看set中是否包含值value
获取set中的元素个数
移除set中的值value
获取set中的随机值(可以做抽奖功能)
随机移除set中的元素
将一个set集合中指定的值移动到另一个set集合
1 2 3 4 5 6 7 8 9 10 11 12 13 # 将source 中的value移动到destination smove <source> <destination> <value> # 例如 127.0.0.1:6379> sadd set k1 k2 k3 k4 k5 k6 k7 (integer) 7 127.0.0.1:6379> sadd newset k1 (integer) 1 127.0.0.1:6379> smove set newset k3 (integer) 1 127.0.0.1:6379> smembers newset 1) "k3" 2) "k1"
集合运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 集合s1和s2的并集 sunion <s1> <s> # 集合s1和s2的交集 (实现共同好友、共同关注) sinter <s1> <s2> # 集合s1中独有的元素 sdiff <s1> <s2> # 例如 127.0.0.1:6379> sadd s1 k1 k2 k3 k4 k5 k6 (integer) 6 127.0.0.1:6379> sadd s2 k5 k6 k7 k8 k9 k10 (integer) 6 127.0.0.1:6379> sunion s1 s2 1) "k5" 2) "k6" 3) "k8" 4) "k2" 5) "k3" 6) "k1" 7) "k4" 8) "k7" 9) "k10" 10) "k9" 127.0.0.1:6379> sinter s1 s2 1) "k5" 2) "k6" 127.0.0.1:6379> sdiff s1 s2 1) "k2" 2) "k1" 3) "k3" 4) "k4"
7.1.4 🏈Hash 相当于key-HashMap,value为一个map集合,更适合于对象的存储,多用于存储变更数据、
设置key指定的哈希集中指定字段的值
1 hset <key> <field> <value>
key指定的哈希集中不存在指定字段时,设置字段的值
1 hsetnx <key> <field> <value>
删除key指定的哈希集中指定字段
1 hdel <key> <field> [field ...]
判断key指定哈希集中指定字段是否存在
对key指定的哈希集中指定字段的值加上增量(Integer型,可正可负,字段不存在则在操作执行前把该字段的值设置为0)
1 hincrby <key> <field> <integer>
对key指定的哈希集中指定字段的值加上增量(float型,可正可负,字段不存在则在操作执行前把该字段的值设置为0)
1 hincrbyfloat <key> <field> <float>
获取key指定的哈希集中字段数量
获取key指定的哈希集中指定字段的值的字符串长度
1 hstrlen hash <key> <value>
key指定的哈希集操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 设置key指定的哈希集中指定字段的值 hmset <key> <field> <value> [field value ...] # 获取key指定的哈希集中指定字段所关联的值 hmget <key> <field> [field ...] # 获取key指定的哈希集中所有字段的名字 hkeys # 获取key指定哈希集中所有字段的值 hvals # 获取key指定的哈希集中所有的字段和值 hgetall # 例如 127.0.0.1:6379> hmset hash field1 hello field2 world OK 127.0.0.1:6379> hmget hash field1 field2 1) "hello" 2) "world" 127.0.0.1:6379> hkeys hash 1) "field1" 2) "field2" 127.0.0.1:6379> hvals hash 1) "hello" 2) "world" 127.0.0.1:6379> hgetall hash 1) "field1" 2) "hello" 3) "field2" 4) "world"
7.1.5 🏉Sorted Set 有序集合sorted set,集合中每个元素都会关联一个double类型的分数。
向key的有序集合中添加序号为number的value
1 zadd <key> <number> <value> [number value ...]
获取key的有序集合中的所有值
获取key的有序集合中的成员数量
获取key的有序集合中指定下标区间的成员
获取key的有序集合中指定成员member的索引
对key的有序集合中指定成员member的分数加上增量
1 zincrby <key> <Integer> member
获取key的有序集合中指定成员member的分数值
获取key的有序集合中指定成员member的排名(从小到大)
获取key的有序集合中指定成员member的排名(从大到小)
获取key的有序集合中分数在指定区间[min,max]的成员数量
通过字典区间获取key的有序集合中的成员数量
通过字典区间获取key的有序集合中的成员
1 zrangebylex <key> min max [limit offset count]
获取key的有序集合中分数在指定区间[min,max]的成员
参数说明
min max
默认情况下为闭区间,即[min ,max]
也可以是使用开区间,即(min, max),写法为 (min (max
withscores
limit offset count
offset:起始位置,count:从起始位置开始的记录数量
实现分页查询
参数: 页数pagenum,页面大小pagesize
那么实际的offset = (pagenum - 1) * pagesize,count = pagesize
即查询语句为zrangebyscore salary min max withscores limit (pagenum - 1) * pagesize pagesize
1 zrangebyscore <key> min max [withscores] [limit offset count]
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 127.0.0.1:6379> zadd salary -10000 W -20000 F -30000 S (integer) 3 127.0.0.1:6379> zadd salary 10000 K 20000 A 30000 G (integer) 3 127.0.0.1:6379> zrangebyscore salary -20000 20000 1) "F" 2) "W" 3) "K" 4) "A" 127.0.0.1:6379> zrangebyscore salary -inf inf 1) "S" 2) "F" 3) "W" 4) "K" 5) "A" 6) "G" 127.0.0.1:6379> zrangebyscore salary -inf inf withscores 1) "S" 2) "-30000" 3) "F" 4) "-20000" 5) "W" 6) "-10000" 7) "K" 8) "10000" 9) "A" 10) "20000" 11) "G" 12) "30000" 127.0.0.1:6379> zrangebyscore salary -inf inf withscores limit 4 2 1) "G" 2) "30000" 3) "K" 4) "60000"
删除key的有序集合中的一个或多个成员
1 zrem <key> member [member ...]
7.2 🌌三种特殊类型 7.2.1 🔮Geospatial Geospatial,地理空间,简称GEO,主要用于存储地理位置信息,并对存储的信息进行操作。
操作方法
命令
描述
geoadd
添加地理位置的坐标
geopos
获取地理位置的坐标
geodist
计算两个位置之间的距离
georadius
根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
georadiusbymember
根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合
geohash
返回一个或者多个位置对象的geohash值
查询地理数据:城市经纬度查询
测试数据
地方
经度
纬度
黄冈市黄梅县
115.94427
30.07033
武汉市武昌区
114.31589
30.55389
北京市丰台区
116.28625
39.8585
上海市黄浦区
121.49295
31.22337
合肥市蜀山区
117.26104
31.85117
深圳市南山区
113.93029
22.53291
大连市中山区
121.64465
38.91859
广州市天河区
113.36112
23.12467
1️⃣geoadd
描述
geoadd用于存储指定的地理位置空间,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的key中。
语法
1 geoadd <key> longitude latitude member [longtitude latitude member ...]
规则
两级无法直接添加
有效经度:-180 - 180
有效纬度:-85.05112878 - 85.05112878
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 127.0.0.1:6379> geoadd china:city 115.94427 30.07033 huanggang (integer) 1 127.0.0.1:6379> geoadd china:city 114.31589 30.55389 wuhan (integer) 1 127.0.0.1:6379> geoadd china:city 116.28625 39.8585 beijing (integer) 1 127.0.0.1:6379> geoadd china:city 121.49295 31.22337 shanghai (integer) 1 127.0.0.1:6379> geoadd china:city 117.26104 31.85117 hefei (integer) 1 127.0.0.1:6379> geoadd china:city 113.93029 22.53291 shenzhen (integer) 1 127.0.0.1:6379> geoadd china:city 121.64465 38.91859 dalian (integer) 1 127.0.0.1:6379> geoadd china:city 113.36112 23.12467 guangzhou (integer) 1
实际应用中,一般会把城市地理数据写在文件中,直接通过java程序一次性导入。
2️⃣geopos
描述
geopos用于从给定的key里返回所有指定名称(member)的位置(经度和纬度),不存在的返回nil。
语法
1 geopos <key> member [member ...]
实例
1 2 3 4 5 6 7 127.0.0.1:6379> geopos china:city huanggang shenzhen shanghai 1) 1) "115.94427019357681274" 2) "30.07033115798519418" 2) 1) "113.93029063940048218" 2) "22.53290942281488896" 3) 1) "121.49295061826705933" 2) "31.22337074392616074"
3️⃣geodist
描述
geodist用于计算两个给定位置之间的距离。
语法
1 geodist <key> member1 member2 [m|km|ft|mi]
参数说明:
member1和member2为两个地理位置
m:米,默认位置
km:千米
mi:英里
ft:英尺
实例
1 2 3 4 5 6 127.0.0.1:6379> geodist china:city huanggang shenzhen "862016.4959" 127.0.0.1:6379> geodist china:city huanggang hefei km "234.5308" 127.0.0.1:6379> geodist china:city shanghai dalian mi "531.9085"
4️⃣georadius
描述
给定一个中心的地理位置(经度和纬度),给定一个最大距离,返回给定的key包含的位置元素中,与中心的距离不超过最大距离的所有位置元素。
语法
1 georadius <key> longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count] [asc|desc] [store key] [storedist key]
参数说明:
longitude:给定中心的经度
latitude:给定中心的纬度
radius:给定的最大距离
withcoord:返回+(位置元素的经度和纬度)
withdist:返回+(位置元素与中心之间的距离)
withhash:以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
count:限定返回的记录数量
asc:查找结果根据距离从小到大排序
desc:查找结果根据距离从大到小排序
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # 查看距离广州不大于1000km的城市 127.0.0.1:6379> georadius china:city 113.36112 23.12467 1000 km asc 1) "guangzhou" 2) "shenzhen" 3) "huanggang" 4) "wuhan" # 查看距离武汉不大于1000km的城市,从大到小,限制5个,并且显示距离和城市经纬度 127.0.0.1:6379> georadius china:city 114.31589 30.55389 1000 km withcoord withdist count 5 desc 1) 1) "shenzhen" 2) "892.9663" 3) 1) "113.93029063940048218" 2) "22.53290942281488896" 2) 1) "guangzhou" 2) "831.7263" 3) 1) "113.36112052202224731" 2) "23.12467049411647935" 3) 1) "shanghai" 2) "688.9652" 3) 1) "121.49295061826705933" 2) "31.22337074392616074" 4) 1) "hefei" 2) "315.1437" 3) 1) "117.26104170083999634" 2) "31.85117048067123591" 5) 1) "huanggang" 2) "165.3475" 3) 1) "115.94427019357681274" 2) "30.07033115798519418" # 查看距离北京不大于1500km的城市 127.0.0.1:6379> georadius china:city 116.28625 39.8585 1500 km withdist asc 1) 1) "beijing" 2) "0.0002" 2) 1) "dalian" 2) "472.2545" 3) 1) "hefei" 2) "894.9324" 4) 1) "wuhan" 2) "1050.2106" 5) 1) "shanghai" 2) "1069.3051" 6) 1) "huanggang" 2) "1089.1453"
5️⃣georadiusbymember
描述
georadiusbymember 和 georadius命令一样, 都可以找出位于指定范围内的元素, 但是 georadiusbymember 的中心点是只能从key中的位置元素选。
语法
1 georadiusbymember <key> member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count] [asc|desc] [store key] [storedist key]
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 查看距离黄冈不大于900km的城市 127.0.0.1:6379> georadiusbymember china:city huanggang 900 km withdist asc 1) 1) "huanggang" 2) "0.0000" 2) 1) "wuhan" 2) "165.3475" 3) 1) "hefei" 2) "234.5308" 4) 1) "shanghai" 2) "546.1566" 5) 1) "guangzhou" 2) "814.0494" 6) 1) "shenzhen" 2) "862.0165" # 查看距离深圳不大于2000km的城市 127.0.0.1:6379> georadiusbymember china:city shenzhen 2000 km withdist desc 1) 1) "dalian" 2) "1964.1097" 2) 1) "beijing" 2) "1939.8454" 3) 1) "shanghai" 2) "1222.7809" 4) 1) "hefei" 2) "1087.3585" 5) 1) "wuhan" 2) "892.9663" 6) 1) "huanggang" 2) "862.0165" 7) 1) "guangzhou" 2) "87.9580" 8) 1) "shenzhen" 2) "0.0000"
6️⃣geohash
描述
geohash用于获取一个或多个位置元素的geohash值。
实质
降维打击 :将二维的经纬度转换为一维的字符串
如果两个字符串越接近,那么距离越近。
语法
1 geohash <key> member [member ...]
实例
1 2 3 4 127.0.0.1:6379> geohash china:city huanggang beijing hefei 1) "wt67n6hh3k0" 2) "wx4dy0j0d40" 3) "wtemhq6fs20"
7️⃣Other
GEO
GEO的底层原理就是Sorted Set,因此我们可以使用Sorted Set命令来操作GEO。
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 查看地图中全部元素 127.0.0.1:6379> zrange china:city 0 -1 1) "shenzhen" 2) "guangzhou" 3) "wuhan" 4) "huanggang" 5) "hefei" 6) "shanghai" 7) "beijing" 8) "dalian" # 移除大连这个城市 127.0.0.1:6379> zrem china:city dalian (integer) 1 # 按照分数给城市排名 127.0.0.1:6379> zrangebyscore china:city -inf inf withscores 1) "shenzhen" 2) "4046431599170567" 3) "guangzhou" 4) "4046534293000673" 5) "wuhan" 6) "4051938129491420" 7) "huanggang" 8) "4052334404505800" 9) "hefei" 10) "4052764524670284" 11) "shanghai" 12) "4054757680623470" 13) "beijing" 14) "4069146323276357"
7.2.2 📄HyperLogLog HyperLogLog,Redis中基数统计的算法。
优点
占用内存固定且较小。每个HyperLogLog键占用12KB内存,可以计算2^64^个不同元素的基数。
基数
一个数据集中不重复元素的数量
应用场景
统计网站UV
传统方式:使用set保存用户id,set的元素数量可作为标准判断。
这个方式如果保存大量的用户id,就会比较麻烦,目的是计数,而非保存用户id。
使用HyperLogLog会有**0.81%**的错误率,这个在统计UV任务中是可以接受的。
操作方法
命令
描述
pfadd <key> element [element …]
添加指定元素到HyperLogLog中
pfcount <key>
返回给定HyperLogLog的基数估算值
pfmerge <destkey> <key> [key…]
将多个HyperLogLog合并为一个HyperLogLog
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 创建第一组元素 127.0.0.1:6379> pfadd hyper K H I G H N E S S (integer) 1 # 统计第一组元素基数 127.0.0.1:6379> pfcount hyper (integer) 7 # 创建第二组元素 127.0.0.1:6379> pfadd hyper2 P A R A K (integer) 1 # 统计第二组元素基数 127.0.0.1:6379> pfcount hyper2 (integer) 4 # 合并两组元素 127.0.0.1:6379> pfmerge hyper hyper hyper2 OK # 统计所有元素基数 127.0.0.1:6379> pfcount hyper (integer) 10
7.1.3 🔳Bitmaps Bitmaps,位图,操作二进制位来进行记录,只有0和1两个状态。
应用场景
统计用户活跃度,打卡,两个状态的都可以使用Bitmaps。
操作方法
命令
描述
setbit <key> offset value
设置值
getbit <key> offset
获取值
bitcount <key> start end
获取Bitmaps指定范围值为1的个数
bitop and|or|not|xor <destkey> key [key …]
Bitmaps的集合运算
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # 打卡 0-6:周一-周日 # 2020年第一周打卡 127.0.0.1:6379> setbit 2020:week:1 0 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:1 1 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:1 2 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:1 3 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:1 4 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:1 5 0 (integer) 0 127.0.0.1:6379> setbit 2020:week:1 6 0 (integer) 0 # 2020年第二周打卡 127.0.0.1:6379> setbit 2020:week:2 0 0 (integer) 0 127.0.0.1:6379> setbit 2020:week:2 1 0 (integer) 0 127.0.0.1:6379> setbit 2020:week:2 2 0 (integer) 0 127.0.0.1:6379> setbit 2020:week:2 3 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:2 4 0 (integer) 1 127.0.0.1:6379> setbit 2020:week:2 5 1 (integer) 0 127.0.0.1:6379> setbit 2020:week:2 6 1 (integer) 0 # 检查打卡 127.0.0.1:6379> getbit 2020:week:1 3 (integer) 1 127.0.0.1:6379> getbit 2020:week:2 1 (integer) 0 # 统计打卡 127.0.0.1:6379> bitcount 2020:week:1 (integer) 5 127.0.0.1:6379> bitcount 2020:week:2 (integer) 3 # 对两周打卡结果取并集 127.0.0.1:6379> bitop and andres 2020:week:1 2020:week:2 (integer) 1 127.0.0.1:6379> bitcount andres (integer) 1 # 对两周打卡结果取交集 127.0.0.1:6379> bitop or orres 2020:week:1 2020:week:2 (integer) 1 127.0.0.1:6379> bitcount orres (integer) 7
8. 📩Redis事务
💡 说明
Redis单条命令执行具有原子性,但是事务不保证原子性。
8. 1📖定义 一组命令的队列
8.2 🌠特征
8.3 ⏳三个阶段
开始事务 (multi)
命令入队 (…)
执行事务 (exec)
8.4 📝操作方法
命令
描述
discard
取消事务,放弃执行事务块内的所有命令
exec
执行事务块内的所有命令
multi
标记一个事务的开始
unwatch
取消watch命令对所有key的监视
watch
监视一个或多个key,如果在事务执行之前这个或这些key被其他命令锁改动,那么事务将被打断
8.5 🕵️实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 开启事务 127.0.0.1:6379> multi OK # 命令入队 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> mget k1 k2 QUEUED 127.0.0.1:6379> getset k3 v3 QUEUED 127.0.0.1:6379> get k3 QUEUED # 执行事务 127.0.0.1:6379> exec 1) OK 2) OK 3) 1) "v1" 2) "v2" 4) "v3" 5) "v3"
8.6 ⭕异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> getset k3 # 错误命令 (error) ERR wrong number of arguments for 'getset' command 127.0.0.1:6379> set k4 v4 QUEUED 127.0.0.1:6379> exec # 执行事务报错 (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get k1 # 所有命令都未被执行 (nil)
运行异常:错误操作的命令抛出异常,其他命令正常执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> incr k1 # 错误操作 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> exec # 执行事务仅错误操作执行失败,其他命令执行成功 1) OK 2) (error) ERR value is not an integer or out of range 3) OK 127.0.0.1:6379> mget k1 k2 1) "v1" 2) "v2"
9.7 🔭监控
悲观锁:很悲观,认为什么时候都会出问题,无论做什么都会加锁。
乐观锁:很乐观,认为什么时候都不会出问题,所以不会上锁。更新数据的时候会比较version,判断数据是否更新过。
watch的本质:select version,一旦发现监视的数据version改变,事务将被打断。
实例1-watch的监控测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 个人财务 127.0.0.1:6379> set money 100 OK # 个人支出 127.0.0.1:6379> set out 0 OK # 监控财务 127.0.0.1:6379> watch money OK # 开启事务 127.0.0.1:6379> multi OK # 消费10元 127.0.0.1:6379> decrby money 30 QUEUED # 支出增加 127.0.0.1:6379> incrby out 30 QUEUED # 执行事务 127.0.0.1:6379> exec 1) (integer) 70 2) (integer) 30
实例2-watch的多线程测试,watch可以当做redis的乐观锁操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 # 线程1 # 监控财务 127.0.0.1:6379> watch money OK 127.0.0.1:6379> multi OK # 消费10元 127.0.0.1:6379> decrby money 10 QUEUED # 支出增加 127.0.0.1:6379> incrby out 10 QUEUED # 执行之前线程2修改了财务,这个时候就会导致事务执行失败 127.0.0.1:6379> exec (nil) # 线程2 # 执行在线程1的事务exec 之前 # 查询财务 127.0.0.1:6379> get money "70" # 充值1000 127.0.0.1:6379> incrby money 1000 (integer) 1070 # 线程1 # 执行在线程1的事务exec 之后 # 1、如果发现事务执行失败,就先解锁 127.0.0.1:6379> unwatch OK # 2、获取最新的值,再次监视 127.0.0.1:6379> watch money OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> decrby money 50 QUEUED 127.0.0.1:6379> incrby out 50 QUEUED # 3、对比监视的值是否发生了变化 # 如果没有变化,那么可以执行成功,否则执行失败 127.0.0.1:6379> exec 1) (integer) 1020 2) (integer) 80
9. 📩Jedis
📢 说明
Jedis是Redis官方推荐的Java连接开发工具。
Jedis中的所有api就对应Redis中的所有命令。
9.1 ➕导入依赖
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > top.parak</groupId > <artifactId > springboot-redis</artifactId > <version > 1.0-SNAPSHOT</version > <developers > <developer > <name > KHighness</name > <email > parakovo@gmail.com</email > </developer > </developers > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.2.5.RELEASE</version > <relativePath /> </parent > <properties > <fastjson.version > 1.2.68</fastjson.version > <jackson.version > 2.11.0</jackson.version > <jedis.version > 3.3.0</jedis.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > <exclusions > <exclusion > <groupId > org.junit.vintage</groupId > <artifactId > junit-vintage-engine</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-context</artifactId > <version > 2.1.0.RELEASE</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > ${fastjson.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <scope > provided</scope > </dependency > <dependency > <groupId > redis.clients</groupId > <artifactId > jedis</artifactId > <version > ${jedis.version}</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
9.2 ⌨️编码测试 9.2.1 🅿Ping测试
Ping.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package top.parak.jedis;import lombok.extern.log4j.Log4j2;import redis.clients.jedis.Jedis;@Log4j2 public class Ping { public static void main (String[] args) { Jedis jedis = new Jedis ("127.0.0.1" , 6379 ); log.info(jedis.ping()); jedis.close(); } }
运行结果
1 17:52:31.303 [main] INFO top.parak.jedis.Ping - PONG
9.2.1 ⚪GEO-api测试
city.txt
1 2 3 4 5 6 7 8 huanggang 115.94427 30.07033 wuhan 114.31589 30.55389 beijing 116.28625 39.8585 shanghai 121.49295 31.22337 hefei 117.26104 31.85117 shenzhen 113.93029 22.53291 dalian 121.64465 38.91859 guangzhou 113.36112 23.12467
Geo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package top.parak.jedis;import lombok.extern.log4j.Log4j2;import org.springframework.util.ResourceUtils;import redis.clients.jedis.GeoCoordinate;import redis.clients.jedis.GeoUnit;import redis.clients.jedis.Jedis;import redis.clients.jedis.params.GeoRadiusParam;import java.io.FileInputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;import java.util.Arrays;import java.util.HashMap;import java.util.Map;@Log4j2 public class Geo { public static void readAndWriteIntoRedis (String path, Jedis jedis) throws IOException { FileInputStream fileInputStream = new FileInputStream (path); FileChannel channel = fileInputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(512 ); channel.read(byteBuffer); String[] res = new String (byteBuffer.array()).split("\n" ); Map<String, GeoCoordinate> map = new HashMap <>(); Arrays.stream(res).forEach(s -> { String[] ss = s.split("\\s+" ); map.put(ss[0 ], new GeoCoordinate (Double.valueOf(ss[1 ]), Double.valueOf(ss[2 ]))); }); jedis.geoadd("china:city" , map); } public static void main (String[] args) throws IOException { Jedis jedis = new Jedis ("127.0.0.1" , 6379 ); readAndWriteIntoRedis(ResourceUtils.getFile("src/main/resources/city.txt" ).getAbsolutePath(), jedis); log.info("==========地图中的所有城市==========" ); jedis.zrange("china:city" , 0 , -1 ).stream().forEach(s -> { log.info(s + " " ); }); log.info("==========查询黄冈的经纬度==========" ); log.info(jedis.geopos("china:city" , "huanggang" )); log.info("==========查询距离杭州不超过1000km的城市==========" ); GeoRadiusParam geoRadiusParam = new GeoRadiusParam (); geoRadiusParam.withCoord().withDist().sortAscending(); jedis.georadius("china:city" , 120.153576 , 30.287459 , 1000 , GeoUnit.KM, geoRadiusParam).forEach( c -> { log.info("城市名称:{}, 经纬度:{},距离:{}KM" , c.getMemberByString(), c.getCoordinate(), c.getDistance()); }); log.info("==========查询距离武汉不超过1000KM的城市==========" ); jedis.georadiusByMember("china:city" , "wuhan" , 1000 , GeoUnit.KM, geoRadiusParam).forEach( c -> { log.info("城市名称:{}, 经纬度:{},距离:{}KM" , c.getMemberByString(), c.getCoordinate(), c.getDistance()); }); } }
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 20:21:02.671 [main] INFO top.parak.jedis.Geo - ==========地图中的所有城市========== 20:21:02.718 [main] INFO top.parak.jedis.Geo - shenzhen 20:21:02.718 [main] INFO top.parak.jedis.Geo - guangzhou 20:21:02.718 [main] INFO top.parak.jedis.Geo - wuhan 20:21:02.718 [main] INFO top.parak.jedis.Geo - huanggang 20:21:02.718 [main] INFO top.parak.jedis.Geo - hefei 20:21:02.718 [main] INFO top.parak.jedis.Geo - shanghai 20:21:02.718 [main] INFO top.parak.jedis.Geo - beijing 20:21:02.718 [main] INFO top.parak.jedis.Geo - dalian 20:21:02.718 [main] INFO top.parak.jedis.Geo - ==========查询黄冈的经纬度========== 20:21:02.720 [main] INFO top.parak.jedis.Geo - [(115.94427019357681,30.070331157985194)] 20:21:02.721 [main] INFO top.parak.jedis.Geo - ==========查询距离杭州不超过1000km的城市========== 20:21:02.724 [main] INFO top.parak.jedis.Geo - 城市名称:shanghai, 经纬度:(121.49295061826706,31.22337074392616),距离:165.0KM 20:21:02.724 [main] INFO top.parak.jedis.Geo - 城市名称:hefei, 经纬度:(117.26104170084,31.851170480671236),距离:325.8468KM 20:21:02.725 [main] INFO top.parak.jedis.Geo - 城市名称:huanggang, 经纬度:(115.94427019357681,30.070331157985194),距离:405.4241KM 20:21:02.725 [main] INFO top.parak.jedis.Geo - 城市名称:wuhan, 经纬度:(114.31589037179947,30.55389005243692),距离:560.6357KM 20:21:02.725 [main] INFO top.parak.jedis.Geo - 城市名称:dalian, 经纬度:(121.64465099573135,38.91858901014995),距离:969.6213KM 20:21:02.725 [main] INFO top.parak.jedis.Geo - ==========查询距离武汉不超过1000KM的城市========== 20:21:02.725 [main] INFO top.parak.jedis.Geo - 城市名称:wuhan, 经纬度:(114.31589037179947,30.55389005243692),距离:0.0KM 20:21:02.725 [main] INFO top.parak.jedis.Geo - 城市名称:huanggang, 经纬度:(115.94427019357681,30.070331157985194),距离:165.3475KM 20:21:02.726 [main] INFO top.parak.jedis.Geo - 城市名称:hefei, 经纬度:(117.26104170084,31.851170480671236),距离:315.1437KM 20:21:02.726 [main] INFO top.parak.jedis.Geo - 城市名称:shanghai, 经纬度:(121.49295061826706,31.22337074392616),距离:688.9652KM 20:21:02.726 [main] INFO top.parak.jedis.Geo - 城市名称:guangzhou, 经纬度:(113.36112052202225,23.12467049411648),距离:831.7263KM 20:21:02.726 [main] INFO top.parak.jedis.Geo - 城市名称:shenzhen, 经纬度:(113.93029063940048,22.53290942281489),距离:892.9663KM
9.2.2 ⚫Hyper-api测试
Hyper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package top.parak.jedis;import lombok.extern.log4j.Log4j2;import redis.clients.jedis.Jedis;@Log4j2 public class Hyper { public static void main (String[] args) { Jedis jedis = new Jedis ("127.0.0.1" , 6379 ); jedis.pfadd("hyper1" , "K" , "H" , "I" , "G" , "H" , "N" , "E" , "S" , "S" ); log.info("hyper1中的元素基数:{}" , jedis.pfcount("hyper1" )); jedis.pfadd("hyper2" , "P" , "A" , "R" , "A" , "K" ); log.info("hyper2中的元素基数:{}" , jedis.pfcount("hyper2" )); jedis.pfmerge("hyper" , "hyper1" , "hyper2" ); log.info("hyper1和hyper2合并后的元素基数:{}" , jedis.pfcount("hyper" )); } }
运行结果
1 2 3 20:36:20.386 [main] INFO top.parak.jedis.Hyper - hyper1中的元素基数:7 20:36:20.390 [main] INFO top.parak.jedis.Hyper - hyper2中的元素基数:4 20:36:20.390 [main] INFO top.parak.jedis.Hyper - hyper1和hyper2合并后的元素基数:10
9.2.3 🔴Bitmaps-api测试
Bit.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package top.parak.jedis;import lombok.extern.log4j.Log4j2;import redis.clients.jedis.BitOP;import redis.clients.jedis.Jedis;@Log4j2 public class Bit { public static String getChineseExpression (int i) { switch (i) { case 0 : return "星期一" ; case 1 : return "星期二" ; case 2 : return "星期三" ; case 3 : return "星期四" ; case 4 : return "星期五" ; case 5 : return "星期六" ; case 6 : return "星期日" ; default : return "Error" ; } } public static void main (String[] args) { Jedis jedis = new Jedis ("127.0.0.1" , 6379 ); boolean [] bool1 = new boolean []{true , true , true , true , true , false , false }; boolean [] bool2 = new boolean []{false , false , false , true , true , true , false }; for (int i = 0 ; i < bool1.length; i++) { jedis.setbit("2020:week:1" , i, bool1[i]); } for (int i = 0 ; i < bool2.length; i++) { jedis.setbit("2020:week:2" , i, bool2[i]); } log.info("2020年第一周的打卡天数:{}" , jedis.bitcount("2020:week:1" )); log.info("2020年第一周具体打卡情况" ); for (int i = 0 ; i < bool1.length; i++) { log.info(getChineseExpression(i) + ": " + (jedis.getbit("2020:week:1" , i) ? "已打卡" : "未打卡" )); } log.info("2020年第二周的打卡天数:{}" , jedis.bitcount("2020:week:2" )); log.info("2020年第二周具体打卡情况" ); for (int i = 0 ; i < bool2.length; i++) { log.info(getChineseExpression(i) + ": " + (jedis.getbit("2020:week:2" , i) ? "已打卡" : "未打卡" )); } jedis.bitop(BitOP.AND, "2020:week:1and2" , "2020:week:1" , "2020:week:2" ); log.info("2020年第一周和第二周两天都打卡的天数:{}" , jedis.bitcount("2020:week:1and2" )); jedis.bitop(BitOP.OR, "2020:week:1or2" , "2020:week:1" , "2020:week:2" ); log.info("2020年第一周和第二周至少有一天打卡的天数:{}" , jedis.bitcount("2020:week:1or2" )); jedis.bitop(BitOP.XOR,"2020:week:1xor2" , "2020:week:1" , "2020:week:2" ); log.info("2020年第一周和第二周仅有一天打卡的天数:{}" , jedis.bitcount("2020:week:1xor2" )); } }
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 21:07:40.042 [main] INFO top.parak.jedis.Bit - 2020年第一周的打卡天数:5 21:07:40.046 [main] INFO top.parak.jedis.Bit - 2020年第一周具体打卡情况 21:07:40.046 [main] INFO top.parak.jedis.Bit - 星期一: 已打卡 21:07:40.046 [main] INFO top.parak.jedis.Bit - 星期二: 已打卡 21:07:40.046 [main] INFO top.parak.jedis.Bit - 星期三: 已打卡 21:07:40.046 [main] INFO top.parak.jedis.Bit - 星期四: 已打卡 21:07:40.046 [main] INFO top.parak.jedis.Bit - 星期五: 已打卡 21:07:40.046 [main] INFO top.parak.jedis.Bit - 星期六: 未打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期日: 未打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 2020年第二周的打卡天数:3 21:07:40.047 [main] INFO top.parak.jedis.Bit - 2020年第二周具体打卡情况 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期一: 未打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期二: 未打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期三: 未打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期四: 已打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期五: 已打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期六: 已打卡 21:07:40.047 [main] INFO top.parak.jedis.Bit - 星期日: 未打卡 21:07:40.048 [main] INFO top.parak.jedis.Bit - 2020年第一周和第二周两天都打卡的天数:2 21:07:40.048 [main] INFO top.parak.jedis.Bit - 2020年第一周和第二周至少有一天打卡的天数:6 21:07:40.048 [main] INFO top.parak.jedis.Bit - 2020年第一周和第二周仅有一天打卡的天数:4
9.2.4 🔵事务测试
Affair.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package top.parak.jedis;import com.alibaba.fastjson.JSONObject;import lombok.extern.log4j.Log4j2;import redis.clients.jedis.Jedis;import redis.clients.jedis.Transaction;import java.util.concurrent.TimeUnit;@Log4j2 public class Affair { public static void main (String[] args) { Jedis jedis = new Jedis ("127.0.0.1" , 6379 ); JSONObject jsonObject1 = new JSONObject (); jsonObject1.put("name" , "KHighness" ); jsonObject1.put("age" , 19 ); jsonObject1.put("constellation" , "Virgo" ); jsonObject1.put("Hobby" , "Jay" ); String json1 = jsonObject1.toJSONString(); JSONObject jsonObject2 = new JSONObject (); jsonObject2.put("name" , "BingYao" ); jsonObject2.put("age" , 16 ); jsonObject2.put("constellation" , "Taurus" ); jsonObject2.put("Hobby" , "Czk" ); String json2 = jsonObject2.toJSONString(); jedis.set("user1" , json1); jedis.set("user2" , json2); jedis.watch("user1" , "user2" ); Transaction multi = jedis.multi(); new Thread ( () -> { try { TimeUnit.SECONDS.sleep(5 ); multi.set("user1" , json1); multi.set("user2" , json2); multi.exec(); } catch (InterruptedException e) { multi.discard(); log.info(e.getMessage()); } finally { log.info("user1: [{}]" , jedis.get("user1" )); log.info("user2: [{}]" , jedis.get("user2" )); jedis.close(); } }, "Multi" ).start(); new Thread ( () -> { try { new Affair ().resetInfo1(jedis); } catch (InterruptedException e) { log.info(e.getMessage()); } }, "Other" ).start(); } public void resetInfo1 (Jedis jedis) throws InterruptedException { TimeUnit.SECONDS.sleep(1 ); JSONObject jsonObject1 = new JSONObject (); jsonObject1.put("name" , "KHighness" ); jsonObject1.put("age" , 20 ); jsonObject1.put("constellation" , "Leo" ); jsonObject1.put("Hobby" , "BingYao" ); String json1 = jsonObject1.toJSONString(); jedis.set("user1" , json1); } }
运行结果
1 2 3 4 5 6 7 8 Exception in thread "Other" redis.clients.jedis.exceptions.JedisDataException: Cannot use Jedis when in Multi. Please use Transaction or reset jedis state. at redis.clients.jedis.BinaryJedis.checkIsInMultiOrPipeline(BinaryJedis.java:1895) at redis.clients.jedis.Jedis.set(Jedis.java:152) at top.parak.jedis.Affair.resetInfo1(Affair.java:82) at top.parak.jedis.Affair.lambda$main$1(Affair.java:66) at java.lang.Thread.run(Thread.java:748) 22:05:18.651 [Multi] INFO top.parak.jedis.Affair - user1: [{"constellation":"Virgo","name":"KHighness","Hobby":"Jay","age":19}] 22:05:18.654 [Multi] INFO top.parak.jedis.Affair - user2: [{"constellation":"Taurus","name":"BingYao","Hobby":"Czk","age":16}]
10. 📩Springboot整合
⚠️notice
在SpringBoot2.X之后,原来使用的jedis被替换为了lettuce,在windows下lettuce连接池仅支持3.2.100版本的Redis
10.1 🔎源码分析
自动配置类:RedisAutoConfiguration.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration () { } @Bean @ConditionalOnMissingBean(name = {"redisTemplate"}) public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { 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; } }
10.2 🔑整合使用
导入依赖:pom.xml(见上jedis)
配置环境:application.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 spring.redis.database =0 spring.redis.host =127.0.0.1 spring.redis.port =6379 spring.redis.password =spring.redis.timeout =100 spring.redis.lettuce.pool.max-active =100 spring.redis.lettuce.pool.max-idle =10 spring.redis.lettuce.pool.min-idle =0 spring.redis.lettuce.shutdown-timeout =100ms
⌨️自定义RedisTemplate:RedisConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package top.parak.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration public class RedisConfig { @Bean @SuppressWarnings("all") @Qualifier("redisTemplate") public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate <>(); redisTemplate.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer (Object.class); redisTemplate.setKeySerializer(jackson2JsonRedisSerializer); ObjectMapper objectMapper = new ObjectMapper (); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer (); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
⌨️Redis工具类:RedisUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 package top.parak.common;import lombok.extern.log4j.Log4j2;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.Map;import java.util.Set;import java.util.concurrent.TimeUnit;@Log4j2 @Component public class RedisUtil { @Autowired private RedisTemplate<String, Object> redisTemplate; public boolean expire (String key, long time) { try { if (time > 0 ) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true ; } catch (Exception e) { log.error(e.getMessage()); return false ; } } public long getExpire (String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } public boolean hasKey (String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { log.error(e.getMessage()); return false ; } } 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)); } } } public Object get (String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } public boolean set (String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true ; } catch (Exception e) { log.error(e.getMessage()); return 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) { log.error(e.getMessage()); return false ; } } public long incr (String key, long delta) { if (delta < 0 ) { throw new RuntimeException ("增量必须大于0" ); } return redisTemplate.opsForValue().increment(key, delta); } public long decr (String key, long delta) { if (delta < 0 ) { throw new RuntimeException ("减量必须大于0" ); } return redisTemplate.opsForValue().decrement(key, delta); } public Object hget (String key, String item) { return redisTemplate.opsForHash().get(key, item); } public Map<Object, Object> hmget (String key) { return redisTemplate.opsForHash().entries(key); } public boolean hmset (String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true ; } catch (Exception e) { log.error(e.getMessage()); return 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) { log.error(e.getMessage()); return false ; } } public boolean hset (String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true ; } catch (Exception e) { log.error(e.getMessage()); return 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) { log.error(e.getMessage()); return false ; } } public void hdel (String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } public boolean hHasKey (String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } public double hincr (String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } public double hdecr (String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } public Set<Object> sGet (String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { log.error(e.getMessage()); return null ; } } public boolean sHasKey (String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { log.error(e.getMessage()); return false ; } } public long sSet (String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { log.error(e.getMessage()); return 0 ; } } 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) { log.info(e.getMessage()); return 0 ; } } public long sGetSetSize (String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { log.info(e.getMessage()); return 0 ; } } public long lGetListSize (String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { log.error(e.getMessage()); return 0 ; } } public Object lGetIndex (String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { log.error(e.getMessage()); return null ; } } public boolean lSet (String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true ; } catch (Exception e) { log.error(e.getMessage()); return false ; } } 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) { log.error(e.getMessage()); return false ; } } public boolean lUpdateIndex (String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true ; } catch (Exception e) { log.error(e.getMessage()); return false ; } } public long lRemove (String key, long count ,Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { log.error(e.getMessage()); return 0 ; } } }
10.3 💨api测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package top.parak;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.data.redis.core.StringRedisTemplate;import top.parak.common.RedisUtil;import top.parak.entity.User;import lombok.extern.log4j.Log4j2;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.core.RedisTemplate;import java.util.HashMap;import java.util.Map;@SpringBootTest @Log4j2 class SpringbootRedisApplicationTest { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisTemplate redisTemplate; @Autowired private RedisUtil redisUtil; @Test void main () { redisTemplate.opsForValue().set("name" , "K殿下" ); log.info(redisTemplate.opsForValue().get("name" )); } @Test void test1 () { stringRedisTemplate.opsForValue().set("Knum" , "3" ); log.info(stringRedisTemplate.opsForValue().increment("Knum" , 3 )); } @Test void test2 () throws JsonProcessingException { User user = new User ("KHighness" , 19 ); String jsonUser = new ObjectMapper ().writeValueAsString(user); redisTemplate.opsForValue().set("user" , user); log.info(redisTemplate.opsForValue().get("user" )); } @Test void test3 () { HashMap hashMap = new HashMap <String, String>(); hashMap.put("name1" , "KHighness" ); hashMap.put("name2" , "ParaK" ); hashMap.put("name3" , "FlowerK" ); redisUtil.hmset("K" , hashMap); for (Map.Entry k : redisUtil.hmget("K" ).entrySet()) { log.info(k.toString()); } } }
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 __ __ __ _ __ / / ,< / __ \/ / __ `/ __ \/ __ \/ _ \/ ___/ ___/ / /| |/ / / / / /_/ / / / / / / / __(__ |__ ) /_/ |_/_/ /_/_/\__, /_/ /_/_/ /_/\___/____/____/ /____/ Copyright © 2020 KHighness. All Rights Reserved 2020 -10 -12 13 :36 :35.073 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : No active profile set, falling back to default profiles: default 2020 -10 -12 13 :36 :35.417 INFO 18840 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!2020 -10 -12 13 :36 :35.420 INFO 18840 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.2020 -10 -12 13 :36 :35.445 INFO 18840 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 10ms. Found 0 Redis repository interfaces.2020 -10 -12 13 :36 :35.534 INFO 18840 --- [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=8d774ca9-71ca-37a9-91e1 -35cd9af79e442020 -10 -12 13 :36 :35.699 INFO 18840 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$67ea5b63] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2020 -10 -12 13 :36 :36.483 INFO 18840 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020 -10 -12 13 :36 :36.818 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : Started SpringbootRedisApplicationTest in 2.287 seconds (JVM running for 3.29 )2020 -10 -12 13 :36 :37.191 INFO 18840 --- [ main] io.lettuce.core.EpollProvider : Starting without optional epoll library2020 -10 -12 13 :36 :37.192 INFO 18840 --- [ main] io.lettuce.core.KqueueProvider : Starting without optional kqueue library2020 -10 -12 13 :36 :37.740 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : K殿下2020 -10 -12 13 :36 :37.762 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : 6 2020 -10 -12 13 :36 :37.802 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : User(name=KHighness, age=19 )2020 -10 -12 13 :36 :37.836 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : name3=FlowerK2020 -10 -12 13 :36 :37.836 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : name2=ParaK2020 -10 -12 13 :36 :37.836 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : name1=KHighness
11. 📩Redis持久化
📌tip
Redis是内存数据库,如果不将内存中的数据库状态保存在磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能。
11.1 📁RDB
🔔RDB = Redis Database
将内存中的数据以快照”RDB”的形式将数据持久化到磁盘的一个二进制文件dump.rdb,定时保存。
🔨配置
1 2 3 save 900 1 # 15分钟备份一次 save 300 10 # 如果在300s内,至少有10个key进行了修改,就进行持久化操作 save 60 10000 # 如果在60s内,至少有10000个key进行了修改,就进行持久化操作
可以在24小时内,每小时备份一次,并且在每个月的每一天也备份一个RDB文件。
这样的话,即使遇上问题,也可以随时将数据集恢复到不同的版本。
🔍工作机制
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何I/O操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失 。我们默认的就是RDB,一般情况下不需要修改这个配置。
💣触发机制
save的规则满足的情况下,会自动触发rdb规则
执行flushall命令,也会触发rdb规则
退出redis,也会产生rdb文件
💟恢复rdb
1 2 3 127.0.0.1:6379> config get dir 1) "dir" 2) "/usr/local/bin" # 如果在这个目录下存在dump.rdb文件,启动就会自动恢复其中的数据
🌠优点缺点
优点:
适合大规模的数据恢复(适合文件备份)
对数据的完整性要求不高
缺点:
Redis服务器宕机时会丢失数据
fork进程会占用一定的内容空间
11.2 📁AOF
🔔AOF = Append Only Mode
把所有的对Redis的服务器进行修改的命令都存到一个文件(默认为appendonly.aof)里,命令的集合。
🔔配置
1 2 3 4 appendonly yes # 开启AOF appendfsync yes # 默认开启同步 appendfsync always # 每次数据修改发生时候都会写入AOF文件 appendfsync everysec # 每秒钟同步一次,这个死AOF的缺省策略
📝AOF重写
AOF文件的大小随着时间的流逝一定越来越大,影响包括但不限于:对于Redis服务器计算机的存储压力;AOF还原数据库状态的时间增加
为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF 文件不会包含任何浪费空间的冗余命令,通常会较旧AOF文件小很多
Redis会在最近一次重写后记住AOF文件的大小,将次基本大小与当前大小进行比较,如果当前大小大于指定的百分比,则触发重写。
指定零百分比可以禁用重写功能。
1 2 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 触发重写的AOF文件的最小大小
💢产生问题
每次重启Redis的时候,会优先使用AOF文件还原数据。
如果AOF文件以外产生错位,或者人工意外改写,可以通过redis-check-aof --fix appendonly.aof
修复文件
1 2 3 4 5 6 [root@master bin]# redis-check-aof --fix appendonly.aof 'x 3f: Expected prefix '*', got: ' AOF analyzed: size=114, ok_up_to=63, diff=51 This will shrink the AOF from 114 bytes, with 51 bytes, to 63 bytes Continue? [y/N]: y Successfully truncated AOF
🌠优点缺点
优点:
AOF会让redis变得非常耐久,AOF的默认策略是每秒同步一次,在这种配置下,就算Redis服务器宕机,也最多丢失一秒钟的数据
缺点:
对于相同的数据集来说,AOF的文件体积要大于RDB的文件体积,数据恢复的速度更慢
根据所使用的sync策略,AOF的速度可能慢于RDB
12. 📩Redis发布订阅 12.1 💬说明 Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者(pub) 发送方消息,订阅者(sub)接收消息。
Redis客户端可以订阅任意数量的频道。
12.2 📷模型
🗼订阅模型
🗽消息模型
📜发布订阅命令
命令
描述
psubscribe pattern [pattern …]
订阅一个或多个符合给定模式的频道
pubsub subcommand [argument [argument …]]
查看订阅与发布系统状态
publish channel message
将消息发送到指定的频道
punsubscribe channel [channel …]
退订所有给定模式的频道
subscribe channel [channel …]
订阅给定的一个或多个频道的信息
unsubscribe [channel [channel …]]
退订给定的频道
🎏演示
开启三个redis-cli
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 第一个客户端,订阅频道:Khighness 127.0.0.1:6379> subscribe Khighness Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "Khighness" 3) (integer) 1 1) "message" 2) "Khighness" # 第二个客户端,在频道Khighness发布消息 127.0.0.1:6379> publish Khighness "Client1: Hello, Khighness" (integer) 1 # 第三个客户端,在频道Khighness发布消息 127.0.0.1:6379> publish Khighness "Client3: Hello, Khighness" (integer) 1 # 订阅频道的第一个客户端就能收到消息 1) "message" 2) "Khighness" 3) "Client2: Hello, Khighness" 1) "message" 2) "Khighness" 3) "Client3: Hello, Khighness"
🕵️原理
Redis是C语言编写的,通过分析Redis源代码里面的pubsub.c文件,了解发布和订阅机制的底层实现。
通过subscribe命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个频道channel,而字典的值则是一个个链表,链表中保存了所有订阅这个频道的客户端client。subscribe命令的关键,就是将client添加到给定channel的订阅链中。
通过publish命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
在Redis中,可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到响应的消息。这一功能最明显的用法就是用作实时消息系统,普通的即时聊天和群聊功能。
13. 📩Redis主从复制 13.1 📖概念 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主结点(master),后者称为从结点(slave);数据的复制是单向的,只能由主节点到从结点。master以写为主,salve以读为主。
默认情况下,每台Redis服务器都是主结点;且一个主结点可以有多个从结点(或没有从结点),但一个从结点只能有一个主节点。
13.2 🔧作用
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从结点分担读负载,可以大大提高Redis服务器的并发量
高可用基石:主从复制是哨兵和集群可实施的基础,因此说主从复制是Redis高可用的基础
13.3 🔍复制原理 slave启动成功连接到master后会发送一个sync同步命令。
master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制 :slave服务在接收到数据库文件后,将其存盘并加载到内存中。
增量复制 :master继续将新的所有收集到的修改命令依次传给slave,完成同步。
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。
14. 📩Redis集群搭建 14.1 🔱方法 ==搭建临时伪集群,命令操作即可==
主要操作:操作从机,认老大。
查看redis服务器信息:info replication
在从机上认老大master:slaveof <master-ip> <master-port>
==搭建永久集群,修改配置文件==
主要操作:修改redis-conf文件
1 2 replicaof <masterip> <masterport> # 配置master的ip和端口号 masterauth <master-passwordd> # 如果master有密码则配置密码
==master关机解决——谋权篡位==
通过slaveof no one
让slave自己变成master
14.2 🔪操作
复制三份redis.conf文件,修改信息
port
logfile
pidfile
dbfilename
分别在三个配置文件下启动redis服务
1 2 3 4 5 [root@master bin]# ps -ef | grep redis root 18914 1 0 00:54 ? 00:01:38 redis-server 127.0.0.1:6379 root 31612 1 0 11:51 ? 00:00:00 redis-server 127.0.0.1:6380 root 31623 1 0 11:51 ? 00:00:00 redis-server 127.0.0.1:6381 root 31634 31340 0 11:51 pts/2 00:00:00 grep --color=auto redis
开启三个终端开启三个redis客户端分别连接三个redis服务器
1 2 3 4 5 6 # Terminal1 [parak@master bin]$ redis-cli -p 6379 # Terminal2 [parak@master bin]$ redis-cli -p 6380 # Terminal3 [parak@master bin]$ redis-cli -p 6381
将6379端口的服务的配置成master,另外两个配置成slave
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 # 6380 127.0.0.1:6380> slaveof 127.0.0.1 6379 OK 127.0.0.1:6380> info replication # Replication role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up master_last_io_seconds_ago:3 master_sync_in_progress:0 slave_repl_offset:14 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:9c3e61afce386f90c00db9ee4e9a2e7b4b265297 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:14 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:14 # 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 master_link_status:up master_last_io_seconds_ago:8 master_sync_in_progress:0 slave_repl_offset:42 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:9c3e61afce386f90c00db9ee4e9a2e7b4b265297 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:42 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:43 repl_backlog_histlen:0 # 6379 127.0.0.1:6379> info replication # Replication role:master connected_slaves:2 slave0:ip=127.0.0.1,port=6380,state=online,offset=2087,lag=0 slave1:ip=127.0.0.1,port=6381,state=online,offset=2087,lag=0 master_replid:9c3e61afce386f90c00db9ee4e9a2e7b4b265297 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:2087 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:2087
在master上写入值,在slave上读取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 6379 127.0.0.1:6379> hmset student:1 name Khighness gender male age 19 OK 127.0.0.1:6379> hmset student:2 name bingyao gender female age 16 OK # 6380 127.0.0.1:6380> hmget student:1 name gender age 1) "Khighness" 2) "male" 3) "19" # 6381 127.0.0.1:6381> hmget student:2 name gender age 1) "bingyao" 2) "female" 3) "16" # slave只能读取,不能写入 127.0.0.1:6380> set K2 V2 (error) READONLY You can't write against a read only replica
15. 📩Redis哨兵模式 驾校手动挡=>上路自动挡
15.1 📙概述 主从切换技术的方法:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。
简单来说,哨兵模式就是谋权篡位的自动版,能够后台监控主机是否故障,如果发生故障则根据投票数自动将库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。
原理:哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
graph TD;
A((哨兵))
B(主Redis服务器)
C(从Redis服务器1)
D(从Redis服务器2)
S[以独立的进程监控3台服务器Redis是否正常运行]
S --> A
A --> C
A --> B
A --> D
B --> C
B --> D
这里的哨兵有两个作用:
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器
当哨兵监测到master宕机,会自动将slave切换为master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机
一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控,各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主管的认为主服务器不可用,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
15.2 🎏测试 准备三个redis服务,6379-master、6380-slave、6381-slave
修改配置文件sentinel.conf
1 sentinel monitor mymaster 127.0.0.1 6379 1
启动哨兵
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 34055:X 20 Oct 2020 14:39:15.404 * Increased maximum number of open files to 10032 (it was originally set to 1024). _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 5.0.8 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in sentinel mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 26379 | `-._ `._ / _.-' | PID: 34055 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' 34055:X 20 Oct 2020 14:39:15.407 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 34055:X 20 Oct 2020 14:39:15.408 # Sentinel ID is dbfb304470e8ed2bb81b4be42f847e21ff5d9519 34055:X 20 Oct 2020 14:39:15.408 # +monitor master mymaster 127.0.0.1 6379 quorum 1 34055:X 20 Oct 2020 14:40:15.646 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:40:25.731 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
关闭master服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 # 6379: master -> shutdown 127.0.0.1:6379> SHUTDOWN not connected> exit # sentinel # 监控到master宕机 # 选出6380为master 34055:X 20 Oct 2020 14:43:47.509 # +sdown master mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:47.509 # +odown master mymaster 127.0.0.1 6379 #quorum 1/1 34055:X 20 Oct 2020 14:43:47.509 # +new-epoch 1 34055:X 20 Oct 2020 14:43:47.509 # +try-failover master mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:47.510 # +vote-for-leader dbfb304470e8ed2bb81b4be42f847e21ff5d9519 1 34055:X 20 Oct 2020 14:43:47.510 # +elected-leader master mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:47.510 # +failover-state-select-slave master mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:47.594 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:47.594 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:47.678 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:48.296 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:48.296 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:48.371 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:49.310 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:49.310 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:49.362 # +failover-end master mymaster 127.0.0.1 6379 34055:X 20 Oct 2020 14:43:49.362 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380 34055:X 20 Oct 2020 14:43:49.362 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380 34055:X 20 Oct 2020 14:43:49.362 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 34055:X 20 Oct 2020 14:44:19.365 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 # 6380: 新王登基 127.0.0.1:6380> info replication # Replication role:master connected_slaves:1 slave0:ip=127.0.0.1,port=6381,state=online,offset=42337,lag=1 master_replid:12a540accf0c9347d3e45fad83ccddea86f3b3c3 master_replid2:b80e4fbbe06a83ac070f38e89267bd81b26ec5ca master_repl_offset:42351 second_repl_offset:20188 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:8342 repl_backlog_histlen:34010 # 6381: 参拜新王 127.0.0.1:6381> info replication # Replication role:slave master_host:127.0.0.1 master_port:6380 master_link_status:up master_last_io_seconds_ago:0 master_sync_in_progress:0 slave_repl_offset:63969 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:12a540accf0c9347d3e45fad83ccddea86f3b3c3 master_replid2:b80e4fbbe06a83ac070f38e89267bd81b26ec5ca master_repl_offset:63969 second_repl_offset:20188 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:8053 repl_backlog_histlen:55917 # 重启6379的服务 # 重启之后俯首称臣 [root@master bin]# redis-server kconfig/redis6379.conf [root@master bin]# redis-cli -p 6379 127.0.0.1:6379> ping PONG 127.0.0.1:6379> info relplication 127.0.0.1:6379> info replication # Replication role:slave master_host:127.0.0.1 master_port:6380 master_link_status:up master_last_io_seconds_ago:2 master_sync_in_progress:0 slave_repl_offset:50522 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:12a540accf0c9347d3e45fad83ccddea86f3b3c3 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:50522 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:48596 repl_backlog_histlen:1927
15.3 🌠优点
哨兵集群,基于主从复制模式,继承了主从的所有优点
主从可以切换,故障可以转移,增强系统的可用性
哨兵模式是主从模式的升级版,手动到自动,更加健壮
15.4 📰配置详解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # sentinel.conf # 哨兵sentinel实例运行的端口,默认26379 port 26379 # 守护进程 daemonize no # 进程文件 pidfile "/var/run/redis-sentinel.pid" # 进程文件 logfile "" # 工作目录 dir "/tmp" # 哨兵sentinel监控的master的IP和Port # quorum配置多少个哨兵统一认为master失联。那么这时客观上认为主结点失联 sentinel monitor <master-name> <master-ip> <redis-ip> <quorum> # 在redis实例中开启了授权密码 # 设置哨兵senti的连接密码 sentinel auth-pass <master-name> <password> # 配置指定在发生failover主从切换时最多可以有多少个slave同时对新的master同步 # numreplicas越小,完成failover的事件就越长 # numreplicas越大,就意味着越多的slave因为replication(复制)而不可用 # numreplicas设置为1,保证每次只有slave处于不能处理命令请求的状态 sentinel parallel-syncs <master-name> <numreplicas> # 配置指定多milliseconds毫秒之后,master没有响应sentinel # 此时,哨兵主观上认为master下线,默认30秒 sentinel down-after-milliseconds <master-name> <milliseconds> # 配置故障转移的超时时间,默认2分钟 # 可以用于以下方面 # 1. 同一sentinel对同一个master两次failo ver的间隔时间 # 2. 当一个slave从一个错误的master那里同步数据开始计算时间,直至slave被纠正为向正确的master那里同步数据 # 3. 当想要取消一个正在进行的failover需要的时间 # 4. 当进行failover时,配置所有slaves指向新的master所需的最大时间 sentinel failover-timeout <master-name> <milliseconds> # 通知脚本 sentinel notification-script <master-name> <script-path> # 客户端重新配置主节点参数脚本 sentinel client-reconfig-script <master-name> <script-path>
16. 📩Redis穿透、击穿和雪崩 16.1 🔥缓存穿透
💭问题说明
查询的key对应的数据不在redis缓存中,即缓存没有命中,于是向持久层数据库查询,数据库也没有,当请求量过大的时候,可能压垮数据库。
即大面积的缓存失效,大并发请求打崩DB。
💖解决方法
1️⃣参数校验
在接口层增加校验,不合法的参数直接return,比如id<0直接拦截。
2️⃣布隆过滤器
利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查DB刷新KV再return。
3️⃣缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护数据库。
16.2 💧缓存击穿
💭问题说明
查询的一个key非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发直接落到了数据库上,就在这个Key的点上击穿了缓存。
即单个key的缓存失效,大并发请求击穿redis直落DB。
💙解决方法
设置热点数据永不过期,或者加上互斥锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static String getData (String key) throws InterruptedException { String result = getDataByKV(key); if (StringUtils.isBlank(result)) { try { if (reenLock.tryLock()) { result = getDataByDB(key); if (StringUtils.isNotBlank(result)) { setDataToKV(key, result); } } else { Thread.sleep(100L ); result = getData(key); } } finally { reenLock.unlock(); } } return result; }
16.3 🌊缓存雪崩
💭问题说明
当redis服务器重启或则大量缓存集中在某一个时间段失效,瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的
💚解决方法
1️⃣redis高可用
搭建集群,异地多活
2️⃣限流降级
在缓存失效后,通过加锁或者队列哎控制读数据库写缓存的线程数量
3️⃣数据预热
在正式部署之前,先把可能的数据预先访问一遍,让可能的数据加载到缓存中。
在即将发生大并发访问写入key的时候,设置不同的缓存时间,让缓存失效的时间点尽量均匀。