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) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)^(来自官方文档)^。

2.2 🌠特性

  • 性能优秀,数据在内存中,读写速度非常快,支持10w并发QPS
  • 单进程单线程,是线程安全的,采用IO多路复用机制
  • 丰富的数据类型,支持String、Hash、List、Set、Sorted Set
  • 支持数据持久化,可以将内存中数据保存在磁盘中,重启时加载
  • 主从复制,哨兵模式,高可用
  • 可以用作分布式锁
  • 可以进行地图信息分析
  • 可以作为消息中间件使用,支持发布订阅
  • 可以作为计数器使用,记录网页或者小程序等的浏览量
  • ······

2.3 🔰拓展

Redis 🆚 Memcache

  1. 存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性
  2. 数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,而redis支持五大数据类型和三大特殊数据类型
  3. 底层模型上:它们之间底层实现方式以及与客户端之间的应用协议不一样。redis直接构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
  4. 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

1
daemonize yes

测试启动: redis-server kconfig/redis.conf


查看redis进程: ps -ef | grep redis


关闭redis服务: shutdown


4. 📩Redis配置

4.1 🚩命令

👀查看所有配置项

1
config get *

✏命令行编辑配置

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 1save 300 10save 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 🔍 重点详解

  1. UNIT: redis对大小写不敏感

  2. INCLUEDS[模块]: 可以包含多个配置文件

  3. MOUDLES[模块]: 启动时加载模块

  4. NETWORK[网络]:

    • bind: 绑定IP
    • protected-mode: 保护模式
    • post: 端口设置
  5. GENERAL[通用]:

    • daemonize: 是否以守护进程的方式运行[^1]
    • pidfile /var/run/redis_6379.pid: 如果以后台的方式运行,就需要指定一个pid的配置文件
    • loglevel: 日志级别
    • logfile: 日志的文件位置
    • database: 数据库的数量
    • always-show-logo: 是否开启服务的时候显示logo
  6. 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: 文件保存的目录
  7. REPLICATION[复制]:

    • 见主从复制
  8. SECURITY[安全]:

    • requirepass: 设置密码

    • # 设置密码
      > config set requirepass <password>
      # 登录输入
      > auth <password>
      # 获取密码
      > config get requirepass
      # 取消设置
      > config set requirepass ''
      
      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

      9. CLIENTS[客户端]:

      - maxclients: 设置可连接redis的最大客户端数量
      - maxmemory: 配置redis的最大内存容量
      - maxmemory-policy: 内存到达上限的处理策略
      - volatile-lru:只对设置了过期时间的key进行LRU(默认值)
      - allkeys-lru : 删除lru算法的key
      - volatile-random:随机删除即将过期key
      - allkeys-random:随机删除
      - volatile-ttl : 删除即将过期的
      - noeviction : 永不过期,返回错误

      10. APPEND ONLY MODE[AOF]:

      - appendonly: 默认不开启AOF模式
      - appendfilename: AOF持久化的文件名称
      - appendfsync always: 每次修改都会同步,消耗性能
      - appendfsync everysec: 每秒执行一次同步,可能会丢失这1s的数据
      - appendfsync no: 不执行同步,操作系统自己同步数据,速度最快



      [^1]: 守护进程,后台运行的特殊进程,伴随着主进程的结束而消亡。



      ## 5. 📩Redis测试



      ### 5.1🔬测试方法

      > redis的性能测试命令

      ```shell
      redis-benchmark [option] [option value]

🔔注意: 这个命令是在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
ping

关闭连接

1
quit

返回消息

1
echo <str>

切换数据库

1
select <num of database>

获取当前数据库的大小

1
dbsize

清空当前数据库

1
flushdb

清空所有数据库

1
flushall

交换数据库

1
swap <n1> <n2>

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采用单线程依然快的原因

  1. Redis完全基于内存,读写全部在一个CPU上,绝大部分请求是纯粹的内存操作,非常迅速,数据存在于内存中,类似于HashMap,HashMap的优势就是查询和操作的时间复杂度时O(1)
  2. 数据结构简单,对数据操作也简单
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的CPU切换,不用取考虑各种锁的问题,不存在加锁放锁操作,没有死锁问题导致的性能消耗
  4. 使用多路复用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

1
keys *

创建键值对

1
set <key> <value>

获取key的值

1
get <key>

移除键值对

1
move <key> <value>

判断key是否存在

1
exists <key>

查看key的类型

1
type <key>

设置key的过期时间/秒

1
expire <key> <seconds> 

获取key的有效时间/秒

1
ttl <key>

获取key的有效时间/毫秒

1
pttl <key>

7.1.1 ⚽String

向key上追加字符串

1
append <key> <value>

获取key的长度

1
strlen <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 # "Khighness"
> getrange s 1 4 # "high"

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 # "Xhighness"
> setrange s 5 "XXXXX"
> get s # "XhighXXXXX"

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
exists <key>

移除列表的第一个元素

1
lpop <key> 

移除列表的最后一个元素

1
rpop <key> 

移除指定的值

1
lrem <key> <count> <value>

更新list

1
2
# 根据index更新值
lset <key> <index> <value>

根据下标获取值

1
lindex <key> <index>

获取list的长度

1
llen <key>

获取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中的所有值

1
smembers <key>

查看set中是否包含值value

1
sismember <key> <value>

获取set中的元素个数

1
scard <key>

移除set中的值value

1
srem <key> <value>

获取set中的随机值(可以做抽奖功能)

1
srandmember <key>

随机移除set中的元素

1
spop <key>

将一个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指定哈希集中指定字段是否存在

1
hexists <key> <field>

对key指定的哈希集中指定字段的值加上增量(Integer型,可正可负,字段不存在则在操作执行前把该字段的值设置为0)

1
hincrby <key> <field> <integer>

对key指定的哈希集中指定字段的值加上增量(float型,可正可负,字段不存在则在操作执行前把该字段的值设置为0)

1
hincrbyfloat <key> <field> <float>

获取key指定的哈希集中字段数量

1
hlen <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类型的分数。

  • redis通过分数对集合中的成员进行排序。

  • 有序集合中成员是唯一的,分数可以重复。

  • 集合是通过哈希表实现的,所以增删查的事件复杂度都是O(1)。

  • 集合中最大的成员数量为2^32^-1(4294967295), 每个集合可存储40多亿个成员。

向key的有序集合中添加序号为number的value

1
zadd <key> <number> <value> [number value ...]

获取key的有序集合中的所有值

1
zrange <key> 0 -1

获取key的有序集合中的成员数量

1
zcard <key> 

获取key的有序集合中指定下标区间的成员

1
zrange <key> start end

获取key的有序集合中指定成员member的索引

1
zrank <key> member

对key的有序集合中指定成员member的分数加上增量

1
zincrby <key> <Integer> member

获取key的有序集合中指定成员member的分数值

1
zscore <key> member

获取key的有序集合中指定成员member的排名(从小到大)

1
zrank <key> member

获取key的有序集合中指定成员member的排名(从大到小)

1
zrevrank <key> member

获取key的有序集合中分数在指定区间[min,max]的成员数量

1
zcount <key> min max

通过字典区间获取key的有序集合中的成员数量

1
zlexcount <key> min max

通过字典区间获取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>

<!-- Springboot-Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Test -->
<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>

<!-- Springboot-Aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- Springboot-Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- SpringCloud-Context -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

<!-- Fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- Jedis -->
<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;

/**
* @author: KHighness
* @date: 2020/10/11 17:47
* @apiNote: 测试链接
*/

@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;


/**
* @author: KHighness
* @date: 2020/10/11 17:59
* @apiNote: 测试Geospatial
*/

@Log4j2
public class Geo {

/**
* 读取文件将地理数据写进redis
* @param path
* @throws IOException
*/
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 -> {
// 使用正则\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;

/**
* @author: KHighness
* @date: 2020/10/11 20:26
* @apiNote: 测试hyper
*/

@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;

/**
* @author: KHighness
* @date: 2020/10/11 20:38
* @apiNote: 测试Bitmaps
*/

@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;

/**
* @author: KHighness
* @date: 2020/10/11 21:33
* @apiNote: 测试事务
*/

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

// 创建json数据
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

  • jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,需要使用jedis pool连接池,更像BIO模式

  • lettuce:底层整合Netty,实例可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数据,更像NIO模式

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"})
// ==> 这个注解说明,不存在我们自定义名为redisTemplate的Bean的情况下,这个Bean才生效
// ==> 因此我们可以使用自定义的RedisTemplate,SpringBoot会优先使用自定义RedisTemplate
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 默认的RedisTemplate没有过多的配置,Redis对象都需要序列化和反序列化
// 两个泛型都是 Object, Obeject 的类型,我们以后使用需要强制转换成 String, Object
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
// 由于String是Redis中最常使用的类型,所以单独一个StringRedisTemplate
// 所以操作String类型,直接使用StringRedisTemplate即可
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
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接超时时间(毫秒)
spring.redis.timeout=100

# Spring 2.X以后,使用lettuce连接池
# 连接池最大阻塞等待时间(使用负值表示没有限制)
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;

/**
* @author: KHighness
* @date: 2020/10/7 20:23
* @apiNote: 自定义RedisTemplate
*/

@Configuration
public class RedisConfig {

/**
* <p>自定义redisTemplate</p>
* @param redisConnectionFactory
* @return
*/
@Bean
@SuppressWarnings("all")
@Qualifier("redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
/* 创建redisTemplate */
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
/* 关联redisConnectionFactory */
redisTemplate.setConnectionFactory(redisConnectionFactory);
/* Jackson2JsonRedisSerializer:Json序列化器 */
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:String序列化器 */
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
/* 设置key的序列化方式:String */
redisTemplate.setKeySerializer(stringRedisSerializer);
/* 设置value的序列化方式:Json */
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
/* 设置hash的key的序列化方式:Json */
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
/* 设置hash的value的序列化方式:Json */
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;

/**
* @author: KHighness
* @date: 2020/10/7 21:33
* @apiNote: Redis操作工具类
*/

@Log4j2
@Component
public class RedisUtil {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/*==================================================================
// common //
==================================================================*/

/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return true 成功,false 失败
*/
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;
}
}

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

/*==================================================================
// String //
==================================================================*/

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

/**
* 删除缓存
* @param key 可以传一个或多个
*/
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));
}
}
}

/**
* 普通缓存获取
* @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) {
log.error(e.getMessage());
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) {
log.error(e.getMessage());
return false;
}
}

/**
* 递增
* @param key 键
* @param delta 增量
* @return 递增后的值
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("增量必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}

/**
* 递减
* @param key 键
* @param delta 减量
* @return 递减后的值
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("减量必须大于0");
}
return redisTemplate.opsForValue().decrement(key, delta);
}

/*==================================================================
// map //
==================================================================*/

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

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

/**
* HashSet
* @param key 键
* @param map 对应的多个键值
* @return true 成功,false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error(e.getMessage());
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) {
log.error(e.getMessage());
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) {
log.error(e.getMessage());
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) {
log.error(e.getMessage());
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 增量
* @return 递增后的值
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}

/**
* hash递减
* @param key 键
* @param item 值
* @param by 减量
* @return 递减后的值
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}

/*==================================================================
// set //
==================================================================*/

/**
* 根据key获取Set中的所有值
* @param key 键
* @return set中的所有值
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error(e.getMessage());
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) {
log.error(e.getMessage());
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) {
log.error(e.getMessage());
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) {
log.info(e.getMessage());
return 0;
}
}

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

/*==================================================================
// list //
==================================================================*/

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

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

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

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

/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return true 成功,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;
}
}

/**
* 移除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) {
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-35cd9af79e44
2020-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 library
2020-10-12 13:36:37.192 INFO 18840 --- [ main] io.lettuce.core.KqueueProvider : Starting without optional kqueue library
2020-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=FlowerK
2020-10-12 13:36:37.836 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : name2=ParaK
2020-10-12 13:36:37.836 INFO 18840 --- [ main] t.parak.SpringbootRedisApplicationTest : name1=KHighness

11. 📩Redis持久化

📌tip

Redis是内存数据库,如果不将内存中的数据库状态保存在磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能。

  • Redis默认是按照快照RDB的持久化方式

  • Redis重启的时候会优先使用AOF文件还原数据库状态

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

  • 将rdb文件放在redis启动目录,redis服务器启动的时候就会自动检查dump.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 🔧作用

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从结点分担读负载,可以大大提高Redis服务器的并发量
  4. 高可用基石:主从复制是哨兵和集群可实施的基础,因此说主从复制是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 🔪操作

  1. 复制三份redis.conf文件,修改信息
    • port
    • logfile
    • pidfile
    • dbfilename
  2. 分别在三个配置文件下启动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
  1. 开启三个终端开启三个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
  1. 将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
  1. 在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 {
//从Redis查询数据
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的时候,设置不同的缓存时间,让缓存失效的时间点尽量均匀。