Skip to content

Redis 学习指南

导航目录

一、NoSQL 数据库概述

1.1 什么是 NoSQL 数据库

NoSQL(Not Only SQL),意即"不仅仅是 SQL",泛指非关系型的数据库。NoSQL 不依赖业务逻辑方式存储,而以简单的 key-value 模式存储,大大增加了数据库的扩展能力。

  • 不遵循 SQL 标准
  • 不支持 ACID
  • 远超于 SQL 的性能

1.2 NoSQL 适用场景

  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的需求

1.3 NoSQL 不适用场景

  • 需要事务支持(报错能回滚)
  • 基于 SQL 的结构化查询存储,处理复杂的关系查询

1.4 常见 NoSQL 数据库

1.4.1 Memcached

  • 很早出现的 NoSQL 数据库
  • 数据都在内存中,一般不持久化
  • 支持简单的 key-value 模式,支持类型单一
  • 一般作为缓存数据库辅助持久化的数据库

1.4.2 Redis

  • 几乎覆盖了 Memcached 的绝大部分功能
  • 数据都在内存中,支持持久化,主要用作备份恢复
  • 除了支持简单的 key-value 模式,还支持多种数据结构的存储,如 String, List、Set、Hash、ZSet 等
  • 一般作为缓存数据库辅助持久化的数据库

1.4.3 MongoDB

  • 高性能、开源、模式自由(schema free)的文档型数据库
  • 数据都在内存中,如果内存不足,把不常用的数据保存到硬盘
  • 虽然是 key-value 模式,但是对 value(尤其是 json)提供了丰富的查询功能
  • 支持二进制数据及大型对象
  • 可以根据数据的特点替代 RDBMS,成为独立的数据库,或配合 RDBMS 存储特定的数据
json
// 查询年龄大于25且城市为北京的用户
db.users.find({ age: { $gt: 25 }, city: "Beijing" })

1.5 DB-Engines 数据库排名

查看连接http://db-engines.com/en/ranking

二、Redis 简介和安装

2.1 Redis 简介和适用场景

  • Redis 是当前比较热门的 NOSQL 系统之一,是一个开源的使用 ANSI C 语言编写的 key-value 存储系统
  • Redis 数据都是缓存在计算机内存中,但是 Redis 会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件,实现数据的持久化
  • Redis 读写速度快,读取速度是 110000 次/s,写的速度是 81000 次/s
  • Redis 的所有操作都是原子性的
  • Redis 支持多种数据结构:string(字符串),list(列表),hash(哈希),set(集合),zset(有序集合)
  • Redis 支持集群部署
  • 支持过期时间,支持事务,消息订阅

2.1.1 配合关系型数据库做高速缓存

  • 高频次,热门访问的数据,降低数据库 IO

2.1.2 多样的数据结构存储持久化数据

2.2 Redis 的安装和基本操作

2.2.1 Redis 的官网和下载

2.2.2 Redis 安装

  1. 下载 redis 及版本选择

    • 7.0.10 for Linux(redis-7.0.10.tar.gz)或安装新版本
    • 不用考虑在 windows 环境下对 Redis 的支持
  2. 下载安装最新版本的 gcc 编译器

bash
# 安装C语言环境
yum -y install gcc

# 测试安装是否成功
gcc --version
  1. 上传 redis-7.0.10.tar.gz 放/opt 目录

  2. 解压命令:

bash
tar -zxvf redis-7.0.10.tar.gz
  1. 解压完成后进入目录:
bash
cd redis-7.0.10
  1. 在 redis-7.0.10 目录下再次执行 make 命令(只是编译好)
bash
# 如果没有准备好C语言编译环境,make会报错
# 解决方案:运行make distclean
make distclean

# 安装好gcc后再次make
make
  1. 跳过 make test,继续执行 make install
bash
make install

2.2.3 Redis 的启动和停止

2.2.3.1 查看安装目录
bash
cd /usr/local/bin
  • redis-benchmark: 性能测试工具
  • redis-check-aof:修复有问题的 AOF 文件
  • redis-check-dump:修复有问题的 dump.rdb 文件
  • redis-sentinel:Redis 集群使用
  • redis-server:Redis 服务器启动命令
  • redis-cli:客户端,操作入口
2.2.3.2 前台启动方式
bash
redis-server 配置文件[后台启动即可]
  • 不推荐原因: 窗口不能关闭,关闭则服务停止
2.2.3.3 后台启动方式
  1. 在/root 目录下创建 myredis 目录,用于存储启动使用的配置文件
bash
cd /root
mkdir myredis
  1. 拷贝一份 redis.conf 到 myredis 目录
bash
cp /opt/redis-7.0.10/redis.conf /root/myredis
  1. 修改配置文件中的内容
bash
# 修改redis.conf文件将里面的daemonize no改成yes,让服务在后台启动
# 修改配置文件中的bind,注释该配置,取消绑定仅主机登录
# 修改protected-mode为no,取消保护模式
  1. 启动 redis 时,使用我们自己修改之后的配置文件
bash
redis-server /root/myredis/redis.conf
  1. 查看服务启动状态
bash
ps -ef | grep redis
2.2.3.4 通过客户端连接 redis
  • 通过客户端指令连接 redis
bash
redis-cli
  • 如果想退出客户端可以按 Ctrl+c,退出客户端不会关闭 redis 服务

  • 通过客户端连接制定端口下的 redis (默认 6379)

bash
redis-cli -p 6379
  • 连接后,测试与 redis 的连通性
bash
ping
2.2.3.5 停止 redis 服务
  • 单实例非客户端连接模式下关闭服务
bash
redis-cli shutdown
  • 在客户端连接模式下,直接使用 shutdown 关闭当前连接的 redis 服务
bash
shutdown
  • 多实例关闭指定端口的 redis 服务
bash
redis-cli -p 6379 shutdown
2.2.3.6 Redis 小知识及操作
  1. 端口号 6379 由来

    • Alessia Merz
  2. 数据库操作

bash
# 默认16个数据库,类似数组下标从0开始,初始默认使用0号库
# 使用命令 select <dbid>来切换数据库。如: select 8
# 统一密码管理,所有库同样密码。
# dbsize查看当前数据库的key的数量
# flushdb清空当前库
# flushall通杀全部库
  1. Redis 单线程+IO 多路复用

Redis 之所以快速,是由于以下几个关键因素:

  • 内存存储:Redis 将数据存储在内存中,这使得数据的读取和写入非常快速
  • 单线程模型:Redis 使用单线程模型,即每个 Redis 实例都是由单个主线程处理所有请求
  • 非阻塞 I/O(Non-blocking I/O)和事件驱动:Redis 使用了一种称为多路 I/O 复用的技术

三、Redis 常用数据类型和命令

3.1 学习目标

  1. 什么是 Redis 的五大数据类型

    • redis 的存储是 key-value 形式的,这里的五大类型指的是 value 的五种数据类型
  2. 相关命令

    • 如何对键进行一些操作
    • String 类型的 value 值如何进行操作
    • List 类型的 value 如何进行操作
    • Set 类型的 value 如何进行操作
    • Hash 类型的 value 如何进行操作
    • Zset 类型的 value 如何进行操作
  3. redis 常见数据类型操作命令的帮助文档

3.2 key 操作的相关命令

语法功能
keys *查看当前库所有 key (匹配:keys *1)
exists key判断某个 key 是否存在
type key查看你的 key(对应的 value 的类型)是什么类型
del key删除指定的 key 数据
unlink key非阻塞删除,仅将 keys 从 keyspace 元数据中删除,真正的删除会在后续异步操作
expire key 1010 秒钟:为给定的 key 设置过期时间
ttl key查看还有多少秒过期,-1 表示永不过期,-2 表示已过期
select命令切换数据库
dbsize查看当前数据库的 key 的数量
flushdb清空当前库
flushall清空全部库

3.3 字符串类型(String)

3.3.1 简介

  • String 是 Redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value
  • String 类型是二进制安全的,意味着 Redis 的 string 可以包含任何数据,比如 jpg 图片或者序列化的对象
  • String 类型是 Redis 最基本的数据类型,一个 Redis 中字符串 value 最多可以是 512M

3.3.2 常用命令

语法解释
set <key><value>添加键值对
NX:当数据库中 key 不存在时,可以将 key-value 添加数据库
XX:当数据库中 key 存在时,可以将 key-value 添加数据库,与 NX 参数互斥
EX:key 的超时秒数
PX:key 的超时毫秒数,与 EX 互斥
get <key>查询对应键值
append <key><value>将给定的<value> 追加到原值的末尾
strlen <key>获得值的长度
setnx <key><value>只有在 key 不存在时 设置 key 的值
incr <key>将 key 中储存的数字值增 1,只能对数字值操作,如果为空,新增值为 1
decr <key>将 key 中储存的数字值减 1,只能对数字值操作,如果为空,新增值为-1
incrby / decrby <key><步长>将 key 中储存的数字值增减。自定义步长
mset <key1><value1><key2><value2> .....同时设置一个或多个 key-value 对 (不推荐)
mget <key1><key2><key3> .....同时获取一个或多个 value
msetnx <key1><value1><key2><value2> .....同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。有一个失败则都失败(原子性)
getrange <key><起始位置><结束位置>获得值的范围,类似 java 中的 substring,前包,后包
setrange <key><起始位置><value>用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从 0开始)
setex <key> <过期时间> <value>设置键值的同时,设置过期时间,单位秒
getset <key><value>以新换旧,设置了新值同时获得旧值

3.3.3 数据结构

String 的数据结构为简单动态字符串(Simple Dynamic String,缩写 SDS)。是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

c
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;        // 已使用的字节长度
    uint8_t alloc;      // 分配的总字节长度(不包括头部和终止符)
    unsigned char flags; // 标志位,记录类型信息
    char buf[];         // 实际存储字符串的字节数组
};

// 存储长度较短的字符串(通常 <= 44 字节)
set name "john_doe"  // 字符串长度为 8 字节

// 存储长度超过阈值的字符串(通常 > 44 字节)
// 预分配策略:当字符串长度小于 1MB 时,按 2 倍大小预分配;超过 1MB 时,每次多分配 1MB
set long_str "x".repeat(100)  // 字符串长度 100 字节

3.4 Redis 列表(List)

3.4.1 简介

单键多值,一个键下的 value 是一个 List。Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

3.4.2 常用命令

语法功能
lpush/rpush <key><value1><value2><value3> ....从左边/右边插入一个或多个值
lpop/rpop <key>从左边/右边吐出一个值。值在键在,值光键亡
rpoplpush <key1><key2>从<key1>列表右边吐出一个值,插到<key2>列表左边
lrange <key><start><stop>按照索引下标获得元素(从左到右)
0 左边第一个,-1 右边第一个,(0 -1 表示获取所有)
lindex <key><index>按照索引下标获得元素(从左到右)
llen <key>获得列表长度
linsert <key> before <value><newvalue>在<value>的前面插入<newvalue>插入值
linsert <key> after <value><newvalue>在<value>的后面插入<newvalue>插入值
lrem <key><n><value>从左边删除 n 个 value(从左到右)
lset<key><index><value>将列表 key 下标为 index 的值替换成 value

3.4.3 数据结构

Redis 的 List 是一个有序的字符串元素集合,它的底层实现有两种数据结构:压缩列表(ziplist)快速列表(quicklist)。Redis 会根据列表的元素数量和元素大小自动选择合适的底层结构。

  1. 压缩列表(ziplist):元素少且短时使用(默认元素数少于 512 且每个元素大小小于 64 字节)
  2. 快速列表(quicklist):元素多或大时使用,是双向链表 + 压缩列表的组合

3.5 Redis 集合(Set)

3.5.1 简介

Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

Redis 的 Set 是 string 类型的无序集合。它底层其实是一个 value 为 null 的 hash 表,所以添加,删除,查找的复杂度都是 O(1)

3.5.2 常用命令

语法功能
sadd <key><value1><value2> .....将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers <key>取出该集合的所有值
sismember <key><value>判断集合<key>是否为含有该<value>值,有 1,没有 0
scard<key>返回该集合的元素个数
srem <key><value1><value2> ....删除集合中的某个元素
spop <key>随机从该集合中吐出一个值
spop <key><N>随机从该集合中吐出 N 个值
srandmember <key><n>随机从该集合中取出 n 个值。不会从集合中删除
smove <source><destination><value>把集合中一个值从一个集合移动到另一个集合
sinter <key1><key2>返回两个集合的交集元素
sunion <key1><key2>返回两个集合的并集元素
sdiff <key1><key2>返回两个集合的差集元素(key1 中的,不包含 key2 中的)

3.5.3 数据结构

Set 数据结构是 dict 字典,字典是用哈希表实现的。Java 中 HashSet 的内部实现使用的是 HashMap,只不过所有的 value 都指向同一个对象。Redis 的 set 结构也是一样,它内部也使用 hash 结构,所有 value 都指向同一个内部值。

3.6 Redis 哈希(Hash)

3.6.1 简介

Redis hash 是一个键值对集合。Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。类似 Java 里面的 Map<String,Object>用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息。

  • 通过 key(用户 ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

3.6.2 常用命令

语法功能
hset <key><field><value><field><value>给<key>集合中的 <field>键赋值<value>
hget <key1><field>从<key1>集合<field>取出 value
hmset <key1><field1><value1><field2><value2>...批量设置 hash 的值
hexists<key1><field>查看哈希表 key 中,给定域 field 是否存在
hkeys <key>列出该 hash 集合的所有 field
hvals <key>列出该 hash 集合的所有 value
hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx <key><field><value>将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在

3.6.3 数据结构

Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable。

3.7 Redis 有序集合 Zset

3.7.1 简介

Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。

3.7.2 常用命令

语法功能
zadd <key><score1><value1><score2><value2>…将一个或多个 member 元素及其 score 值加入到有序集 key 当中
zrange<key><start><stop> [WITHSCORES]升序返回有序集 key 中,下标在<start><stop>之间的元素,0 代表第一个元素索引,-1 代表最后一个元素索引.带 WITHSCORES,可以让分数一起和值返回到结果集
zrevrange <key><start><stop> [WITHSCORES]降序返回有序集 key 中,下标在<start><stop>之间的元素,0 代表第一个元素索引,-1 代表最后一个元素索引.带 WITHSCORES,可以让分数一起和值返回到结果集
zrangebyscore <key> <min> <max> [withscores] [limit offset count]返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列
zrevrangebyscore <key> <max> <min> [withscores] [limit offset count]同上,改为从大到小排列
zincrby <key><increment><value>为元素的 score 加上增量
zrem <key><value>删除该集合下,指定值的元素
zcount <key><min><max>统计该集合,分数区间内的元素个数
zrank <key><value> | zrevrank返回该值在集合中的排名,从 0 开始

3.7.3 数据结构

Redis 的 ZSET(有序集合)是一种键值对存储结构,每个元素关联一个浮点型分数(score),并通过分数实现自动排序。其底层实现结合了 压缩列表(ziplist)跳跃表(skiplist) 两种数据结构,以平衡内存效率和操作性能。

3.8 Redis 综合练习

  1. 用户信息存储
bash
# 方案1: 多key处理 string
set user:1:id 1
set user:1:name alice
set user:1:email xxx

# 方案2: hash处理
hset user:1 id 1 name alice emial xxx

# 方案3: json string
hset user:1 "{id:1,name:alice,email:xxx}"
  1. 商品信息存储

  2. 聊天记录存储

bash
# 记录消息
lpush chat:1 在嘛 在么? 咋不说话啊? 吃饭了么? 是不是男朋友在旁边不方便啊! 我不在意! 我明天给你买早餐  没事我也给你男朋友一起买  好吗  说话 球球 球球啦 gun

# 消息切割
ltrim chat:1 0 9  -> 切前10条!

# 获取聊天
lrange chat:1 0 -1
  1. 好友列表存储
bash
# 添加好友
sadd user:1 2 3 4
sadd user:2 3 4 5

# 判断好友
sismember user:1 5 -> 0

# 删除好友
srem user:1 2

# 共同好友
sinter user:1 user:2   ->  3 4

# 可能是1好友
sdiff user:2 user:1

# 好友数量
scard user:1
scard user:2
  1. 竞赛排名
bash
# 添加学生
zadd ranks 90 1  80 2 100 caijiejie

# 查看高分
zrevrange ranks 0 -1

# 查看低分
zrange ranks 0 -1

# 查看及格(高)
zrevrangebyscore ranks 100 60

# 查看不及格(低)
zrangebyscore ranks 0 59

# 查看前三名
zrevrangebyscore ranks 100 0 limit 0 3

# 查看柴姐姐第几
zrevrank ranks caijiejie  -> 0

# 查看及格数量
zcount ranks 60 100
  1. 用户在线时长统计
bash
# 方案1: 长key
# 方案2: hash处理

四、Jedis 客户端程序

4.1 学习目标

  1. 了解 Jedis
  2. 能够独立搭建 Jedis 的环境
  3. 熟练操作 key 操作相关 API
  4. 熟练操作 String 操作相关 API
  5. 熟练操作 List 操作相关 API
  6. 熟练操作 Set 操作相关 API
  7. 熟练操作 Hash 操作相关 API
  8. 熟练操作 Zset 操作相关 API

4.2 Jedis 简介

Redis 不仅是使用命令来操作,现在基本上主流的语言都有客户端支持,比如 java、C、C#、C++、php、Node.js、Go 等。在官方网站里列一些 Java 的客户端,有 Jedis、Redisson、Jredis、JDBC-Redis、等其中官方推荐使用 Jedis 和 Redisson。在企业中用的最多的就是 Jedis。Jedis 提供了完整 Redis 命令,而 Redisson 有更多分布式的容器实现。

4.3 环境准备

  1. 创建 maven 普通项目,导入如下依赖
xml
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
  1. 虚拟机和 Redis 设置

    • 禁用 Linux 的防火墙:Linux(CentOS7)里执行命令
    • systemctl stop/disable firewalld.service
    • redis.conf 中注释掉 bind 127.0.0.1,然后 protected-mode 的值设置为 no
  2. 测试 JAVA 程序和 Redis 之间的通信

java
package com.atguigu.jedis;
import redis.clients.jedis.Jedis;
public class Demo01 {
    @Test
    public void TestPing() {
        // 创建Jedis连接,指定Redis服务器地址和端口
        Jedis jedis = new Jedis("192.168.6.101",6379);
        // 测试连接是否成功
        String pong = jedis.ping();
        System.out.println("连接成功:"+pong);
        // 关闭连接
        jedis.close();
    }
}

4.4 key 相关 API

java
@Test
public void testKeyAPI(){
    // 设置键值对
    jedis.set("k1", "v1");
    // 添加键值对并设置过期时间(100秒)
    jedis.setex("k2",100, "v2");
    jedis.set("k3", "v3");
    // 获取所有的键
    Set<String> keys = jedis.keys("*");
    System.out.println(keys.size());
    for (String key : keys) {
        System.out.println(key);
    }
    // 判断某个键是否存在
    System.out.println(jedis.exists("k1"));
    // 查看键剩余过期时间
    System.out.println(jedis.ttl("k2"));
    // 根据键获取值
    System.out.println(jedis.get("k1"));
}

4.5 String 相关 API

java
// 添加String
System.out.println(jedis.set("k1", "v1"));
// 一次添加多个
System.out.println(jedis.mset("ka","aaa","kb","bbb"));
// 获取
System.out.println(jedis.get("k1"));
// 一次获取多个
System.out.println(jedis.mget("k1","ka","kb"));
// 追加
System.out.println(jedis.append("k1", "vvvvv"));
// 获取长度
System.out.println(jedis.strlen("k1"));
// 不存在时进行设置
System.out.println(jedis.setnx("k1","xxxxx"));
System.out.println(jedis.setnx("k2","10"));
// 增长/减少
System.out.println(jedis.incr("k2"));
System.out.println(jedis.decr("k2"));
System.out.println(jedis.incrBy("k2", 10));
System.out.println(jedis.decrBy("k2", 10));

4.6 List 相关 API

java
@Test
public void testList(){
    // 从左边放入List
    Long lpush = jedis.lpush("klist", "a", "b", "c", "d", "d");
    System.out.println(lpush);
    // 获取List所有元素
    List<String> kList = jedis.lrange("klist", 0, -1);
    kList.forEach(System.out::println);
    // 从左边取出一个元素
    String klist = jedis.lpop("klist");
}

4.7 Set 相关 API

java
@Test
public void testSet(){
    // 添加一个set集合
    jedis.sadd("skey","a","b","c","d","e");
    // 获取制定的set集合
    Set<String> skey = jedis.smembers("skey");
    skey.forEach(System.out::println);
    //判断是否包含
    System.out.println(jedis.sismember("skey","a"));
    //删除元素
    jedis.srem("skey","a","b");
    //弹出一个元素
    System.out.println(jedis.spop("skey"));
    //弹出N个元素
    System.out.println(jedis.spop("skey",2));
    //从一个set向另一个set移动元素
    jedis.smove("skey","bkey","X");
}

4.8 Hash 相关 API

java
// 添加值
jedis.hset("player1","pname","宇智波赵四儿");
jedis.hset("player1","page","14");
jedis.hset("player1","gender","boy");
// 获取值
System.out.println(jedis.hget("player1","pname"));

// 批量添加值
Map<String,String> player2=new HashMap<String,String>();
player2.put("pname","旋涡刘能");
player2.put("page","13");
player2.put("gender","boy");
jedis.hmset("player2",player2);

// 查看filed是否存在
System.out.println(jedis.hexists("player1", "pname"));
// 查看集合中所有的field
Set<String> player1fields = jedis.hkeys("player1");
player1fields.forEach(System.out::println);
// 查看集合中所有的value
List<String> player1vals = jedis.hvals("player1");
player1vals.forEach(System.out::println);
// 给制定属性+1
jedis.hincrBy("player1","page",5);
// 如不存在,添加某个属性
jedis.hsetnx("player1","height","156");
System.out.println(jedis.hget("player1","page"));
System.out.println(jedis.hget("player1","height"));

4.9 ZSet 相关 API

java
// 准备数据
Map<String ,Double> map=new HashMap<>();
map.put("李四",11d);
map.put("王五",8d);
map.put("赵六",20d);
map.put("刘七",3d);
// 添加元素
jedis.zadd("zkey",10,"张三");
jedis.zadd("zkey",map);
// 升序返回有序
Set<String> zkeys = jedis.zrange("zkey", 0, -1);
zkeys.forEach(System.out::println);
// 降序返回元素
Set<String> zkeys2 = jedis.zrevrange("zkey", 0, -1);
zkeys2.forEach(System.out::println);

System.out.println("===========");
Set<String> zkeys3 = jedis.zrangeByScore("zkey", 10, 20);
zkeys3.forEach(System.out::println);
System.out.println("===========");
Set<String> zkeys4 = jedis.zrevrangeByScore("zkey", 20, 10);
zkeys4.forEach(System.out::println);
// 增加分数
jedis.zincrby("zkey",5,"张三");
jedis.zincrby("zkey",-5,"赵六");
// 删除元素
jedis.zrem("zkey","张三");
System.out.println(jedis.zcount("zkey",10,20));
System.out.println(jedis.zrank("zkey","李四"));

五、SpringBoot 整合 Redis

5.1 SpringDataRedis 介绍

5.1.1 SpringData-Redis 介绍

SpringData 模块是 SpringBoot 中对各种数据操作的单元,集成对各种数据库的简化操作方式,其中对 Redis 数据库操作的模块叫做 spring-data-redis!

  • 提供了不同 Redis 客户端的整合(Jedis 和 Lettuce)
  • 提供了简化操作 api 对象,RedisTemplate
  • 支持 Redis 高级场景应用(集群,哨兵等配置)
  • 支持数据序列化和反序列化存储(核心是 String)
  • 更方便集成到 SpringBoot 环境等等

5.1.2 SpringDataRedis 方法分组介绍

SpringDataRedis 提供的直接操作 api 对象为 RedisTemplate,我们先了解下,他针对数据操作的方法有哪些!

方法名操作数据类型
redisTemplate.opsForValue()操作 String 数据类型
redisTemplate.opsForHash()操作 Hash 数据类型
redisTemplate.opsForList()操作 List 数据类型
redisTemplate.opsForSet()操作 Set 数据类型
redisTemplate.opsForZSet()操作 ZSet 数据类型

5.2 创建工程

5.3 添加依赖

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.5</version>
</parent>

<dependencies>
    <!-- 基本启动 starter - autoconfigure - 142配置类  web-->
    <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>
    </dependency>

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

    <!-- 连接池-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>

</dependencies>

5.4 创建配置文件

application.properties

properties
# redis单机连接的基本信息
spring.data.redis.host=120.46.137.83
spring.data.redis.port=6385

# 配置客户端类型(springboot2以后,默认切换到lettuce)
spring.data.redis.client-type=lettuce

# redis连接池配置
# 含义:这个属性指定是否启用 Lettuce 连接池。
spring.data.redis.lettuce.pool.enabled=true
# 含义:这个属性定义了连接池中允许的最大活动连接数。
spring.data.redis.lettuce.pool.max-active=8
# 含义:这个属性定义了连接池中允许的最大空闲连接数。
spring.data.redis.lettuce.pool.max-idle=5
# 含义:这个属性定义了在获取连接时最长的等待时间(以毫秒为单位)。
spring.data.redis.lettuce.pool.max-wait=100

#切换jedis
# spring.data.redis.client-type=jedis
# spring.data.redis.jedis.pool.enabled=true
# spring.data.redis.jedis.pool.max-active=8

5.5 创建启动类

java
package com.atguigu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

5.6 测试 Template 代码

java
@SpringBootTest(classes = Application.class)
public class SpringBootRedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testString(){
        // 设置字符串值
        redisTemplate.opsForValue().set("name","赵伟风");
        // 获取字符串值
        String result = (String) redisTemplate.opsForValue().get("name");

        System.out.println("result = " + result);
    }
}

5.7 序列化定制

5.7.1 RedisTemplate 序列化需求介绍

  1. 问题演示和解释

    • 我们发现,代码存储 key="name"到了 redis 变了样,这是因为 redis 有自带的序列化器转化的时的问题
  2. 常见序列化器

序列化器名作用备注
JdkSerializationRedisSerializer将数据转化字节流进行存储默认
GenericJackson2JsonRedisSerializerjackson 序列化器,数据进行 json 方式序列化导入依赖 jackson
StringRedisSerializer字符串形式存储,一般用于 key注意 utf-8 格式

5.7.2 RedisTemplate 序列化具体配置

java
@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置连接工厂
        template.setConnectionFactory(connectionFactory);
        // 创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer =
            							new GenericJackson2JsonRedisSerializer();
        // 设置Key的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置Value的序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 返回修改的模板对象
        return template;
    }
}

5.8 RedisTemplate 其他方法

java
@SpringBootTest(classes = Main.class)
public class SpringRedisTest {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Test
    public void testRedis2(){

        //字符串操作
        redisTemplate.opsForValue().set("name","zwf");
        Object name = redisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);

        List list = new ArrayList<>();
        list.add("lucy");
        list.add("mary");
        redisTemplate.opsForValue().set("abc",list);
        System.out.println(redisTemplate.opsForValue().get("abc"));

        System.out.println("----------------------------------------------");

        //集合操作
        redisTemplate.opsForList().rightPushAll("names","1","2","3");
        List names = redisTemplate.opsForList().range("names", 0, -1);
        System.out.println("names = " + names);

        System.out.println("----------------------------------------------");

        // 存储哈希表
        String hashKey = "myHash";
        String field1 = "name";
        String value1 = "John";
        String field2 = "age";
        String value2 = "25";

        redisTemplate.opsForHash().put(hashKey, field1, value1);
        redisTemplate.opsForHash().put(hashKey, field2, value2);

        // 获取哈希表
        Object retrievedValue1 = redisTemplate.opsForHash().get(hashKey, field1);
        System.out.println("retrievedValue1 = " + retrievedValue1);
        Object retrievedValue2 = redisTemplate.opsForHash().get(hashKey, field2);
        System.out.println("retrievedValue2 = " + retrievedValue2);

        System.out.println("----------------------------------------------");

        // 存储集合
        String setKey = "mySet";
        value1 = "Apple";
        value2 = "Banana";
        String value3 = "Orange";
        redisTemplate.opsForSet().add(setKey, value1);
        redisTemplate.opsForSet().add(setKey, value2);
        redisTemplate.opsForSet().add(setKey, value3);
        // 获取集合
        Set<Object> retrievedSet = redisTemplate.opsForSet().members(setKey);
        System.out.println("Retrieved set: " + retrievedSet);

        System.out.println("----------------------------------------------");

        // 添加元素到 Sorted Set
        redisTemplate.opsForZSet().add("myZSet", "value1", 1.0);
        redisTemplate.opsForZSet().add("myZSet", "value2", 2.0);
        redisTemplate.opsForZSet().add("myZSet", "value3", 3.0);

        // 获取 Sorted Set 的元素数量
        Long size = redisTemplate.opsForZSet().size("myZSet");
        System.out.println("Sorted Set size: " + size);

        // 获取指定元素的分数
        Double score = redisTemplate.opsForZSet().score("myZSet", "value2");
        System.out.println("Value2 score: " + score);

        // 获取指定范围的元素(按分数排序)
        Set<String> range = redisTemplate.opsForZSet().range("myZSet", 0, -1);
        System.out.println("Sorted Set range: " + range);

        // 移除指定元素
        Long removedCount = redisTemplate.opsForZSet().remove("myZSet", "value1");
        System.out.println("Removed count: " + removedCount);
    }

}

六、Redis 配置文件解读

6.1 学习目标

  1. 了解网络相关的配置
  2. 了解 GENERAL 通用配置
  3. 了解 SECURITY 安全配置
  4. 了解 LIMIT 限制

6.2 网络配置相关

6.2.1 bind 绑定连接 IP

bash
# 默认情况bind=127.0.0.1只能接受本机的访问请求
# 不写的情况下,无限制接受任何ip地址的访问
# 生产环境肯定要写你应用服务器的地址
# 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应

6.2.2 端口号:6379

6.2.3 tcp-backlog 连接队列

bash
# 设置tcp的backlog,backlog其实是一个连接队列
# backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列
# 在高并发环境下你需要一个高backlog值来避免慢客户端连接问题

# 注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128)
# 所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果

6.2.4 timeout 连接超时

bash
# 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭

6.2.5 tcp-keepalive 连接心跳检测

bash
# 对访问客户端的一种心跳检测,每个n秒检测一次
# 单位为秒,如果设置为0 则不会进行Keepalive检测,建议设置成60

6.3 GENERAL 通用配置

6.3.1 UNITS 单位

bash
# 配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit,大小写不敏感

6.3.2 INCLUDES 包含

bash
# 在当前配置文件中引入其他配置文件中的内容,一般都是引入一些公共配置

6.3.3 daemonize 后台进程

bash
# 是否为后台进程,设置为yes(后台) no(前台) ,守护进程,后台启动

6.3.4 pidfile 进程 ID 文件

bash
# 存放pid文件的位置,每个实例会产生一个不同的pid文件

6.3.5 databases 16

bash
# 设定库的数量 默认16,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id

6.4 SECURITY 安全配置

6.4.1 设置密码

bash
# 访问密码的查看、设置和取消
# 在命令中设置密码,只是临时的。重启redis服务器,密码就还原了
# 永久设置,需要再配置文件中进行设置
# requirepass 123456
# auth 密码

6.5 LIMIT 限制

6.5.1 maxclients 客户端最大连接数

  • 设置 redis 同时可以与多少个客户端进行连接
  • 默认情况下为 10000 个客户端
  • 如果达到了此限制,redis 则会拒绝新的连接请求,并且向这些连接请求方发出"max number of clients reached"以作回应

6.5.2 maxmemory 最大占用内存

  • 建议必须设置,否则,将内存占满,造成服务器宕机
  • 设置 redis 可以使用的内存量。一旦到达内存使用上限,redis 将会试图移除内部数据,移除规则可以通过 maxmemory-policy 来指定
  • 如果 redis 无法根据移除规则来移除内存中的数据,或者设置了"不允许移除",那么 redis 则会针对那些需要申请内存的指令返回错误信息,比如 SET、LPUSH 等
  • 但是对于无内存申请的指令,仍然会正常响应,比如 GET 等

6.5.3 maxmemory-policy 置换策略

maxmemory-policy是 Redis 的配置选项之一,用于指定内存达到最大限制时的置换策略。当 Redis 的内存占用超过了maxmemory的设置值时,Redis 会根据所配置的maxmemory-policy策略来选择哪些键进行置换或删除以释放内存空间。

  1. noeviction(默认值):当内存达到最大限制时,Redis 将拒绝新的写操作,直到有足够的内存空间可用
  2. allkeys-lru:当内存达到最大限制时,Redis 会优先选择最近最少使用(Least Recently Used)的键进行删除
  3. allkeys-lfu:当内存达到最大限制时,Redis 会优先选择最不经常使用(Least Frequently Used)的键进行删除
  4. allkeys-random:当内存达到最大限制时,Redis 会随机选择要删除的键
  5. volatile-lru:当内存达到最大限制时,Redis 会优先选择最近最少使用的带有过期时间的键进行删除
  6. volatile-lfu:当内存达到最大限制时,Redis 会优先选择最不经常使用的带有过期时间的键进行删除
  7. volatile-random:当内存达到最大限制时,Redis 会随机选择要删除的带有过期时间的键

6.5.4 maxmemory-samples

  • 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis 默认会检查这么多个 key 并选择其中 LRU 的那个
  • 一般设置 3 到 7 的数字,数值越小样本越不准确,但性能消耗越小
  • 设置为 10,那么 Redis 将会增加额外的 CPU 开销以保证接近真正的 LRU 性能

七、Redis 事务-锁机制及案例

7.1 学习目标

  1. 熟悉 Redis 事务的定义和特点
  2. 熟练 Redis 事务控制的相关命令
  3. 熟练使用 Redis 的锁对数据进行监视
  4. 熟练编写 Redis 调用 LUA 脚本代码

7.2 Redis 事务和锁机制

7.2.1 Redis 事务的定义

Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis 事务的主要作用就是串联多个命令防止别的命令插队。

7.2.2 Redis 事务控制命令

命令功能
multi开始组队
exec执行队列中的命令
discard取消组队
bash
# 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行
# 直到输入Exec后,Redis会将之前的命令队列中的命令依次执行
# 组队的过程中可以通过discard取消组队

7.2.3 Redis 事务错误处理

  1. 情况 1:组队成功,提交成功

  2. 情况 2:组队报错,提交失败

    • 提交失败组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
  3. 情况 3:组队成功,提交时有成功有失败

    • 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,其他的命令都会执行,不会回滚

7.2.4 Redis 事务和锁案例

场景说明

  • 有很多人有你的账户,同时去参加双十一抢购
  • 一个请求想给金额减 8000
  • 一个请求想给金额减 5000
  • 一个请求想给金额减 1000

悲观锁

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁

乐观锁

  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制

监视和取消监视 key

  • 在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
  • 取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了

7.2.5 Redis 事务的三个特性

  • 单独的隔离操作

    • 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
  • 没有隔离级别的概念

    • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性

    • 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

7.2.6 使用 RedisTemplate 进行事务代码演示

java
@Test
public void performTransaction() {
    redisTemplate.setEnableTransactionSupport(true);

    Object execute = redisTemplate.execute(new SessionCallback<Object>() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            //可以开启锁
            operations.watch("key1");
            operations.multi(); // 开启事务
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            try {
                // 在事务中执行多个命令
                operations.opsForValue().set("key1", "value111");
                operations.opsForValue().set("key2", "value222");
                operations.exec(); // 提交事务
            } catch (Exception e) {
                e.printStackTrace();
                operations.discard(); // 取消事务,释放锁
            }
            return "xxx";
        }
    });

    System.out.println("execute = " + execute);
}

7.3 Redis Lua 脚本

7.3.1 什么是 LUA

Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被 C/C++代码调用,也可以反过来调用 C/C++的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,所以 Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

LUA 脚本的优势

  • 将复杂的或者多步的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数。提升性能
  • LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作

7.3.2 创建 SpringBoot 工程

7.3.3 引入相关依赖

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.5</version>
</parent>

<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>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 连接池-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

7.3.4 创建配置文件

properties
spring.data.redis.host=192.168.6.131
spring.data.redis.port=6379

7.3.5 创建 LUA 脚本

创建文件夹 lua,创建脚本文件 test.lua

LUA 脚本

lua
local current = redis.call('GET', KEYS[1])
if current == ARGV[1]
  then redis.call('SET', KEYS[1], ARGV[2])
  return true
end
return false

-- 账号key  密码value
-- 修改密码: 提供账号【KEYS[1]】 原密码【ARGV[1]】 和 新密码【ARGV[2]】

7.3.6 创建配置类

java
package com.atguigu;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class AppRedisConfiguration  {

    //简单序列化
    @Bean
    public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        // 设置键序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置简单类型值的序列化方式
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        // 设置默认序列化方式
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    //加载lua脚本,设置返回值类型
    @Bean
    public RedisScript<Boolean> script() {
        Resource scriptSource = new ClassPathResource("lua/test.lua");
        return RedisScript.of(scriptSource, Boolean.class);
    }

}

7.3.7 创建测试类

java
package com.atguigu;

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 org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.List;

@SpringBootTest
public class TestLua {

    @Autowired
    private RedisScript<Boolean> script;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Test
    public void test() {
        boolean flag = checkAndSet("hello","helloworld");
        System.out.println(flag ? "修改成功" : "修改失败");

        // 手工添加一个值,再试试
        redisTemplate.opsForValue().set("key", "hello");
        boolean flag1 = checkAndSet("world","hello");
        System.out.println(flag1 ? "修改成功" : "修改失败");
    }

    private boolean checkAndSet(String value1,String value2) {
        List<String> keyList = Collections.singletonList("key");
        return redisTemplate.execute(script, keyList, value1,value2);
    }
}

RedisTemplate.execute 需要传入三个值

  1. 第一个参数 RedisScript script:Lua 脚本
  2. 第二个参数 List keys:集合
    • 如果是单个参数,使用这个可以转换为单元素集合
      • Collections.singletonList(参数);
    • 多参数
      • List<String> keys = Arrays.asList(key1, key2, key3);
  3. 第三个参数 args:ARGV,也就是其他类型参数

八、Redis 的持久化

8.1 学习目标

  1. 熟悉 Redis 持久化的概念
  2. 熟悉 RDB 持久化方式特点以及相关操作
  3. 熟悉 AOF 持久化方式特点以及相关操作

8.2 持久化总体介绍

Redis 是一个内存型数据库,内存的特性是掉电或者程序退出则不保存数据,但是经过实测我们发现,Redis 重启服务后,之前存储的数据仍然在,那么这就是通过持久化的方式实现的.

Redis 提供了 2 个不同形式的持久化方式:

  • RDB(Redis DataBase):定时数据快照,默认方式

    • RDB 持久化是一种周期性将 Redis 数据集快照保存到磁盘的机制
    • 它会创建一个二进制文件(以dump.rdb为扩展名),其中包含了当前数据库中的所有键值对的快照
  • AOF(Append Of File):指令日志文件,手动开启

    • AOF 持久化通过将 Redis 的写操作追加到一个日志文件(Append-Only File)中来记录数据库状态的持久化方式
    • AOF 文件以文本方式保存 Redis 数据库的操作命令,它可以通过重新执行这些命令来还原数据集

8.3 RDB 持久化

8.3.1 RDB 简介

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存里。

8.3.2 RDB 持久化流程

Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。

Fork 子进程

  • Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了"写时复制技术"
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

8.3.3 RDB 相关配置与操作

RDB 文件名配置

  • 在 redis.conf 中配置文件名称,默认为 dump.rdb

RDB 文件位置配置

  • rdb 文件的保存路径,也可以修改。默认为 Redis 启动时命令行所在的目录下
  • 可以通过修改该配置,将 RDB 文件存到系统的制定目录下 dir "/root/myredis/"

RDB 自动执行快照策略

  • save 命令临时这只快照执行策略
  • 格式:save 秒钟 写操作次数
  • RDB 是整个内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条件
  • 默认是 1 分钟至少 1 万个 key 发生变化,或 5 分钟至少 100 个 key 发生变化,或 1 个小时至少 1 个 key 发生变化

手动执行快照命令

  • save:使用主进行进行持久化指令,save 时只管保存,其它不管,全部阻塞。手动保存。不建议
  • bgsave:Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求
    • 可以通过 lastsave 命令获取最后一次成功执行快照的时间

RDB 备份异常策略

  • stop-writes-on-bgsave-error 配置
    • 当 Redis 无法写入磁盘的话,直接关掉 Redis 的写操作。推荐 yes

RDB 文件压缩配置

  • rdbcompression 配置
    • 对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用 LZF 算法进行压缩。如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。推荐 yes

RDB 文件检查完整性配置

  • rdbchecksum 配置
    • 在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能.推荐 yes

RDB 手动备份操作

  • 查询 rdb 文件的目录
  • 将 *.rdb 的文件拷贝到别的地方

RDB 的恢复

  • 关闭 Redis
  • 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  • 启动 Redis, 备份数据会直接加载

RDB 禁用操作

  • 修改配置文件永久禁用
  • 通过指令临时禁用
bash
# 动态停止RDB:redis-cli config set save ""  save后给空值,表示禁用保存策略(不建议)

8.3.4 RDB 的优势和劣势

优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

劣势

  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑
  • 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
  • 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改

8.4 AOF 持久化

8.4.1 AOF 简介

Append Only File 以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

8.4.2 AOF 持计划流程

  1. 客户端的请求写命令会被 append 追加到 AOF 缓冲区内
  2. AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的 AOF 文件中
  3. AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量
  4. Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的

8.4.3 AOF 相关配置与操作

AOF 文件名配置

  • 可以在 redis.conf 中配置文件名称,默认为 appendonly.aof

AOF 文件位置路径

  • Redis6 中,AOF 文件的保存路径,同 RDB 的路径一致
  • Redis7 有变化:
    • base:基本文件
    • incr:增量文件
    • manifest:清单文件

AOF 开启-修复-恢复操作

bash
# AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载

正常恢复数据

  • 修改默认的 appendonly no,改为 yes,开启 AOF 方式
  • 将有数据的 aof 文件复制一份保存到对应目录(查看目录:config get dir)
  • 恢复:重启 redis 然后重新加载

异常修复数据

  • 修改默认的 appendonly no,改为 yes
  • 如遇到 AOF 文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof.1.incr.aof 进行恢复
  • 备份被写坏的 AOF 文件
  • 恢复:重启 redis,然后重新加载

AOF 同步频率设置

  • appendfsync always

    • 始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性比较好
  • appendfsync everysec

    • 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失
  • appendfsync no

    • redis 不主动进行同步,把同步时机交给操作系统

AOF 重写

  • AOF 重写是指重新生成一份 AOF 文件,保留可以恢复数据的最小指令集
  • 可以通过 bgrewriteaof 命令手动触发重写
  • 也可以通过配置文件中的 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 参数自动触发

AOF 重写的优势

  • 减少 AOF 文件大小
  • 减少 IO 操作
  • 提高数据恢复速度

8.4.4 AOF 的优势和劣势

优势

  • 更高的数据完整性和持久性
  • 可以通过配置不同的同步策略来平衡性能和数据安全
  • AOF 文件是可读的,可以手动修改或修复

劣势

  • AOF 文件通常比 RDB 文件大
  • AOF 恢复速度比 RDB 慢
  • 写操作的性能消耗比 RDB 大

8.5 总结

  • RDB:适合对数据完整性要求不高,追求高性能和快速恢复的场景
  • AOF:适合对数据完整性要求高,能够容忍一定性能开销的场景
  • 混合使用:可以同时开启 RDB 和 AOF,以获得更好的数据安全保障

九、Redis 主从复制

9.1 学习目标

  1. 了解 Redis 主从复制的概念和作用
  2. 掌握 Redis 主从复制的配置方法
  3. 理解 Redis 主从复制的工作原理
  4. 了解 Redis 哨兵模式

9.2 主从复制的概念

Redis 主从复制是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master),后者称为从节点(slave)。数据的复制是单向的,只能由主节点到从节点。

9.3 主从复制的作用

  • 数据冗余:实现数据的热备份,是持久化之外的一种数据冗余方式
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载
  • 高可用基石:主从复制还是哨兵模式和集群模式的基础

9.4 主从复制的配置

配置从节点

  1. 在从节点的 redis.conf 文件中添加:
bash
# 配置主节点的IP和端口
slaveof <masterip> <masterport>

# 配置从节点只读
slave-read-only yes
  1. 启动从节点

  2. 查看主从关系:

bash
# 在主节点执行
info replication

9.5 主从复制的工作原理

  1. 连接建立阶段:从节点向主节点发送 SYNC 命令,请求同步数据
  2. 数据同步阶段:主节点执行 bgsave 生成 RDB 文件,发送给从节点,从节点加载 RDB 文件
  3. 命令传播阶段:主节点将后续的写命令发送给从节点,从节点执行这些命令

9.6 Redis 哨兵模式

哨兵模式的概念

  • 哨兵(sentinel)是一个分布式系统,用于监控 Redis 集群中的主节点是否正常运行
  • 当主节点出现故障时,哨兵会自动将一个从节点升级为新的主节点
  • 哨兵模式是 Redis 高可用的解决方案

哨兵模式的配置

  1. 创建 sentinel.conf 文件:
bash
# 哨兵监听的主节点
sentinel monitor mymaster <masterip> <masterport> <quorum>

# 哨兵的端口
tcp-port 26379

# 哨兵的工作目录
dir /tmp

# 哨兵的日志文件
logfile "sentinel.log"
  1. 启动哨兵:
bash
redis-sentinel sentinel.conf

十、Redis 集群

10.1 学习目标

  1. 了解 Redis 集群的概念和作用
  2. 掌握 Redis 集群的配置方法
  3. 理解 Redis 集群的数据分布策略
  4. 了解 Redis 集群的故障转移机制

10.2 集群的概念

Redis 集群是 Redis 提供的分布式数据库方案,它通过将数据分布到多个节点上来实现数据的高可用和横向扩展。

10.3 集群的作用

  • 高可用:集群中的多个节点可以提供服务,当部分节点出现故障时,集群仍然可以正常工作
  • 横向扩展:通过增加节点来提高集群的容量和性能
  • 负载均衡:将请求分散到多个节点,减少单个节点的压力

10.4 集群的数据分布策略

Redis 集群使用哈希槽(hash slot)来分布数据:

  • 整个集群共有 16384 个哈希槽
  • 每个键通过 CRC16 算法计算出一个值,然后对 16384 取模,得到对应的哈希槽
  • 每个节点负责一部分哈希槽

10.5 集群的配置

创建集群

  1. 准备多个 Redis 实例,每个实例使用不同的端口
  2. 修改每个实例的 redis.conf 文件:
bash
# 开启集群模式
cluster-enabled yes

# 集群配置文件
cluster-config-file nodes-6379.conf

# 集群节点超时时间
cluster-node-timeout 15000
  1. 启动所有实例

  2. 使用 redis-cli 创建集群:

bash
redis-cli --cluster create <ip1>:<port1> <ip2>:<port2> <ip3>:<port3> <ip4>:<port4> <ip5>:<port5> <ip6>:<port6> --cluster-replicas 1

10.6 集群的故障转移

当集群中的主节点出现故障时,集群会自动进行故障转移:

  1. 从节点发现主节点故障
  2. 从节点进行选举,选出一个新的主节点
  3. 新的主节点接管原主节点的哈希槽
  4. 集群恢复正常运行

10.7 集群的使用

连接集群

bash
redis-cli -c -h <ip> -p <port>

集群操作

  • 查看集群信息:cluster info
  • 查看集群节点:cluster nodes
  • 添加节点:redis-cli --cluster add-node
  • 移除节点:redis-cli --cluster del-node
  • 重新分片:redis-cli --cluster reshard