Redis 学习笔记

Redis 学习笔记

Author: Ice Programmer

CreateTime: 2024-2-9

From: 【黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目】 https://www.bilibili.com/video/BV1cr4y1671t/?p=9&share_source=copy_web&vd_source=2144650e86e7db9ca248f5e6041d1476

安装&进入 Redis

安装

对于 Mac 系统,可以直接通过 homebrew 来快速安装 redis,只需在终端中输入以下命令

brew install redis

安装成功后直接在终端中输入下面的命令,redis即正常启动

brew services start redis

Windows 安装可以参考这篇文章:超级详细 的 Redis 安装教程

先不用考虑主从问题,只要安装一个即可,宿主机安装 redis 也一般不用去特意设置密码(若服务器生产环境安装redis,请务必设置密码,你也不想你的 redis 像公共场所一样被所有人任意进出吧

进入

打开终端,只要在终端中输入redis-cli命令就会默认连接宿主机安装的 redis 命令工具,redis默认端口为6379

image-20240209132353368

一、Redis 通用命令

上面我们已经成功连接上了 redis 的命令行,下面介绍一些简单的 redis 命令,我们可以通过以下两种方式来查看 redis 的通用命令

  1. 可以通过访问 redis 官方文档来进行阅读 redis 的所有命令:https://redis.io/commands/?group=generic
  2. 可以在终端中输入命令 help @generic 来查看所有的基本命令

image-20240209130959504

切记不要去记忆命令,编程绝不能死记硬背,用到的时候去查即可,常用命令用的多了自然就熟了😎

KEYS

查看所有符合模版的Key,不建议在生产环境中使用

想要在 redis 中查看有关KEYS的相关命令,通过上面两个方法就能查到,例如我们输入 help keys

image-20240209131725180

命令行工具就会将这个命令的用处和用法展现出来,在官网中也更详细的告诉我们了相关 redis patten 通配符的用法

image-20240209132040788

例如我现在已经提前在 redis 中插入两个键值,我们用 KEYS * 来查出所有的键值

image-20240209132320593

而如果想要进行一个带模糊搜索的键值,灵活运用 patten通配符 的用法就可做到,例如下面我要查所有开头为a的键值

image-20240209132541153

模糊查询效率一般不高,若 redis 的键值达到一定规模时,进行模糊查询的过程耗时长,对服务器的负担大,有因为 redis 是单线程,查询的过程就会阻碍其他读写请求,所以千万不要再生产环境中进行查询🤯

DEL

删除一个(或多个)指定的key

现在输入 DEL name 来删除之前创建的 name 键值

image-20240209133330279

可以看到成功删除了键为 name 的键值,返回值 1 表示成功删除1个键值

那么,如果我们想用 DEL 命令删除不存在的键呢?先用 MSET 批量插入命令来插入多个键值对

image-20240209133740597

这里我们用 DEL 命令删除k1 k2 k3 k4,但实际上我们并没有 k4 的键值,但还是成功执行了命令,并且返回值为 3,说明当我们使用 DEL 删除不存在的键值时,redis 会自动忽略,并且返回成功删除键值的个数

EXISTS

判断 key 是否存在

image-20240209134411775

若 redis 中存在该键值,则返回值为1,反之则为0,当然 EXISTS 也可查询多个键值,返回值与 DEL 类似

image-20240209134923971

EXPIRE & TTL

EXPIRE: 给一个 key 设置有效期,有效期到期时该 key 会被自动删除

TTL: 查看一个KEY的剩余有效期

redis 所有的数据都保存在内存中,存储的容量有限,所以我们不可能一直往 redis 中插值而不删除🥸,设置有效时间能定期帮我们清理 redis 无用的键值

image-20240209140538583

例如下面就将 age 这个字段设置为 20s 的有效时间,并通过 TTL age 命令来查看 age 的剩余有效期,可以发现当过了 20s 之后,age 的剩余有效期变成了 -2,而再次去查询 age 这个键时,便变成了empty array

image-20240209140903452

对于没有设置有效期的键值,如上面 name 键并没有设置有效时间,若用 TTL name 命令查询,如下

image-20240209141631851

发现 name 的有效时间为 -1,这代表 name 键值没有有效时间,永久有效

在 redis 中存值时都带上有效时间🤭

二、Redis 数据结构

1. String 类型

String 类型,也就是最简单的字符串,是 redis 中最简单的储存类型

String类型 其 value 是字符串,不过根据字符串的格式不同,又可以分为3类:

  • string:普通字符串
  • int:整数类型,可以做自增、自减操作
  • float:浮点类型,可以做自增、自减操作

不管是哪种格式,底层都是字节数组形式储存,只不过编码方式不同。字符串类型的最大空间不能超过512m

1) SET & GET

SET: 添加或者修改已经存在的一个 String 类型的键值对

GET: 根据 key 获取 String 类型的 value

这个十分简单,我们通过实际操作就能了解如何去使用🫡

image-20240209144149588

设置长字符串用 或者 " ,否则会报错。如果对已有键值执行 SET 命令,即可对已有的键值进行修改

image-20240209144519882

SET 能够对一个键值进行存储或者修改,上面我们已经介绍过的 MSET 命令可以批量存储和修改键值对,同理还有 MGET 命令来批量查看键的值

image-20240209144834496

设置和查看 String 类型是不是非常简单呢

2) INCR & INCRBY

INCR: 让一个整形的 key 自增1

INCRBY: 让一个整形的 key 自增并指定步长

我们在插入一个键为 age,值为 18 的键值对,用 GET 查看 age 的值为"18",虽然是用 “” 包围,但字符串的值为数值类型

image-20240209145207135

执行 INCR age 命令发现 age 进行了自增,值变成了 19,每执行一次 INCR age age 的值都会➕1

如果我们想要 age 每次自增2,我们就可以用到 INCRBY 命令,如下

image-20240209145816253

这样子我们就能自定义我们自增的步长,当然我们也可以将步长设置为负数,这样参数就可以实现自减

image-20240209145922917

我们也有 DECRDECRBY 命令来实现自减操作,这里就不一一做赘述了

3) INCRBYFLOAT

让一个浮点类型的数字自增并指定步长

INCRBYFLOAT 命令和上面相同,只是针对的是浮点类型的值,就直接看示例就能理解

image-20240209150540422

4) SETNX & SETEX

SETNX: 添加一个 String 类型的键值对,前提是这个 key 不存在,否则不执行

SETEX: 添加一个 String 类型的键值对,并且制定有效期

通过 help SETNX 命令,我们可以看到 SETNX 相关信息

所以,SETNX 才是真正意义上的新增操作,新增当前没有的键值对,例如

image-20240209151047632

SETNX 其实是一个组合命令,它和 SET NX 命令是一样的效果

image-20240209151353300

SETEX 命令就是 SETEXPIRE 两个命令组合使用,添加一个键值对,并且制定有效时间

image-20240209151723532

SETNX 一样的,也可以通过以下的命令来实现,到这里是不是也十分简单呢😎

image-20240209151912333

2. Key 的层级格式

Redis 是键值对类型的数据库,没有类似 MySQL 中的 Table 的概念,我们该如何区分不同类型的 Key 呢?

  • 例如:现在需要储存用户、商品信息到 redis 中,有一个用户的 id 为1,而商品中也有一个 id 恰好也为1

Redis 的 key 允许有多个单词形成层级结构,多个单词之间用 : 隔开,格式如下:

image-20240209153006210

这个格式并非固定,也可以根据自己的需求来删除或添加词条

例如,现在的项目名称为Ice,有 user 和 product 两个不同类型的数据,我们可以这样去定义 key :

  • user 相关的 key:Ice:user:1
  • product 相关的 key:Ice:product:1

如果 Value 是一个 Java 对象,例如 User 对象,则可以将对象序列化为 JSON 字符串后储存

KEYValue
Ice:user:1{“id”: 1, “name”: “IceMan”, “age”: 21}
Ice:product:1{“id”: 1, “name”: “乐事薯片”, “price”: 6}

我们现在在 redis 中实际来插入一些值试试

image-20240209155344314

在命令行中看着并不是很明显,我们可以借助 redis 可视化工具来查看所有的键值对,这里使用的是 QuickRedis

image-20240209155951809

QuickRedis 可视化工具根据 : 来分成了不同的层级,这样就可看到不同的项目下不同类型的数据被分类出来啦😎

3. Hash 类型

Hash 类型,也叫散列,其 value 是一个无序字典,类似 Java 中的 HashMap 结构

String 结构是将对象序列化为 JSON字符串后存储,当需要修改某个字段时很不方便,例如下面的

KEYValue
Ice:user:1{“id”: 1, “name”: “IceMan”}
Ice:user:2{“id”: 1, “name”: “IceProgrammer”}

Hash结构可以将对象中的每个字段独立储存,可以针对单个字段做CRUD,类似下图

image-20240210132810734

1) HSET & HGET

HSET key field value: 添加或者修改 hash 类型 key 的 field 的值

HGET key field value: 获取一个 hash 类型的 key 的 field 的值

HSET 命令来试着创建一个 Hash 类型的键值对

image-20240210140544992

然后我们在 QuickRedis 可视化工具中查看插入的数据

image-20240210140628165

可以看到数据成功插入到了 redis 中,并且以一种类似于 HashMap 的形式储存,现在用 HSET Ice:user:3 age 18 的命令来修改 age 的值,记录被成功修改

image-20240210141128976

HGET 命令取值也是十分简单,类似于 Java 中 Map 的 get 方法一样

image-20240210141458981

与 String 类型一样,在 Hash 类型中,同样有 HMSETHMGET 批量操作,使用 MHSET 命令能同时插入多个字段和值,而 HMGET 能获取多个字段,如下

image-20240210141845895

2) HKEYS & HVALS

HKEYS: 获取一个 hash 类型的 key 的所有的 field

HVALS: 获取一个 hash 类型的 key 的所有的 value

当然如果字段很多,觉得用 HMGET 一个个输入字段名还是太麻烦了,可以使用 HGETALL 命令来获取一个 Hash 类型的 key 中所有的的 field

image-20240210142829243

输出的值是键和值是一一对应输出,在视觉上看起来并不明显,可以使用 HKEYSHVALS 来查看所有的 fieldvalue

image-20240210145837638

如果遇到复杂 Hash 类型的键值对,用上面的方法查看都显得不直观,建议复杂的还是使用 redis 可视化工具查看

3) HINCRBY

让一个 hash 类型 key 的字段值自增并指定步长

这个方法还是和 String 类型中的自增命令一样,例如我们现在让 Ice:user:4 中的 age 进行自增

image-20240210150710828

将步长设置成负数,也可以实现自减操作,这里就不多做赘述

4) HSETNX

添加一个 hash 类型的 key 的 field 值,前提是这个 field 不存在,否则不执行

image-20240210151511340

这个操作还是和 String 类型中的 SETNX 命令一样,但不可以通过 HSET *** NX 组合命令

4. List 类型

Redis 中的 List 类型与 Java 中的 LinkedList 类似,可以看做一个双向链表结构。既可以支持正向检索和也可以支持反向检索

特征也与 LinkedList 类似

  • 有序
  • 元素可以重复
  • 插入和删除快
  • 查询速度一般

通常用来储存一个有序数据,例如朋友圈点赞列表,评论列表等

1) L®PUSH & L®POP

LPUSH key element: 向列表左侧插入一个或多个元素

RPUSH key element: 向列表右侧插入一个或多个元素

LPOP key: 移除并返回列表左侧的第一个元素,没有则返回 nil

RPOP key: 移除并返回列表右侧的第一个元素,没有则返回 nil

image-20240211144153254

使用 LPUSH 命令插入多个元素的顺序如同入栈顺序一般,例如下执行下面命令

LPUSH users 1 2 3

image-20240211145336580

这里的顺序可以看到是 3 2 1,要注意列表中元素的顺序。同理使用 RPUSH 命令也需注意元素顺序,例如执行 RPUSH users 4 5 6

image-20240211145322174

使用 LPOP 或者 RPOP 取值的同时会删除当前这个元素,如执行 LPOP 2 命令会将 2 3取出来,并删除

image-20240211150951683

2) LRANGE

LRANGE key star end: 返回一段角标范围内的所有元素

image-20240211144317744

如果只是想取值而不想删除这个元素,可以使用 LRANGEE 命令来取一个范围的元素,如下

image-20240211151306813

这里要注意的点是和大部分编程语言的数组一样,序列号是从 0 开始,所以 LRANGE users 2 4 是从 [2, 4) 位置的值

3) BLPOP & BRPOP

LPOPRPOP 类似,只不过在没有元素是等待指定时间,而不是直接返回 nil

BLPOPBRPOP 这两个命令是阻塞式的来获取,与 Java 中的 阻塞队列十分类似。

我们现在开两个终端窗口进行实际操作,在第一个终端中执行

BLPOP users2 100

即取 user2 中的值,而若取不到值有 100s 的时间进行等待。

实际上在 redis 中我们没有创建 user2 的 List 类型,等待一段时间后在第二个终端中执行

LPUSH users2 IceMan IceProgrammer CQK

创建出 user2 列表,并插入三个元素

截屏2024-02-11 15.28.07

可以看到,终端一在第 13s 左右取到了值,退出当前等待,RPUSH 一样的道理,就不举例说明了

思考
  1. 如何利用 List 结构模拟一个栈
  • 入口和出口在同一边
  1. 如何利用 List 结构模拟一个队列
  • 入口和出口在不同边
  1. 如何利用 List 结构模拟一个阻塞队列
  • 入口和出口在不同边
  • 出队时采用 BLPOPBRPOP
5. Set 类型

Redis 的 Set 结构与 Java 中的 HashSet 类似,可以看作一个 value 为 null Map。因为也是一个 hash 表,因此具备与 HashSet 类似的特征:

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集等功能
1) 单 Set 基本命令
  1. SADD key member …: 向 set 中添加一个或多个元素
  2. SREM key member …: 移除 set 中的指定元素
  3. SCARD key: 返回 set 中的所有元素
  4. SISMEMBER key member: 判断一个元素是否存在于 set 中

image-20240211181501915

以上命令都是针单个集合增删改查的操作, 命令都十分基础好理解,下面介绍多个集合之间的交互操作

2) 多 Set 基本操作
  1. SINTER key1 key2 …: 求 key1 与 key2 的交集

  2. SDIFF key1 key2 …: 求 key1 与 key2 的差集

  3. SUNION key1 key2 …: 求 key1与 key2 的并集

简单的命令不过多说明,上手一遍便能熟知

image-20240211182940291

6. SortedSet 类型

Redis 中的 SortedSet 是一个可排序的 Set 集合,与 Java 中的 TreeSet 有些类似,但底层数据结构(TreeSet 底层红黑树)差别很大。SorterSet中的每一个元素都带有一个 score 属性,可以居于 score 属性对元素排序,底层的实现是一个跳表(SkipList)+ hash 表

SortedSet 具备下列特性:

  • 可排序
  • 元素不重复
  • 查询速度快

因为 SortedSet 的可排序性,经常用来实现排行榜这样的功能

1) 常见命令
  1. ZADD key score member: 添加一个或多个元素到 sorted set,如果已经存在更新其 score 值
  2. ZREM key member: 删除 sorted set 中的一个指定元素
  3. ZSCORE key member: 获取 sortet set 中的指定元素
  4. ZRANK key member: 获取 sorted set 中的指定元素排名
  5. ZCARD key: 获取 sorted set 中的元素个数
  6. ZCOUNT key min max: 统计 score 值在给定范围内的所有元素的个数
  7. ZINCRBY key increment member: 让 sorted set 中的指定元素自增,步长为指定的 increment 值
  8. ZRANGE key min max: 按照 score 排序后,获取指定 score 范围内的元素
  9. ZDIFF、ZINTER、ZUNION: 求差集、交集、并集

⚠️⚠️⚠️: 所有的排名默认都是升序,如果要降序则在命令的 Z 后面添加 REV 即可(如ZREVRANK)🫵🫵🫵

这里的命令和 Set 类型的命令如出一辙,只是增加了 score 值作为排序的标准,例如现在将 " Jack 85, Lucy 89, Tom 95, Jerry 78, Amy 92, Miles 76" 作为 Sorted Set 类型储存到 Redis

ZADD scoreList 85 Jack 89 Lucy 95 Tom 78 Jerry 92 Amy 76 Miles

这里注意插入时顺序是 score + 元素,通过可视化工具可以发现 redis 自动帮我们进行了升序排序

image-20240212154329968

下面是一些常用命令的展示

image-20240212154633930

三、Redis 的 Java 客户端

在 Redis 官网中提供了各种语言的客户端,地址: https://redis.io/clients

以下是三种 redis 官方推荐使用的 Java 连接 redis 的客户端

客户端优缺点
Jedis以 Redis 命令作为方法名称,学习成本低,简单实用。但是 Jedis 实例是线程不安全的,多线程环境下要基于连接池实用
LettuceLettuce 是基于 Netty 实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持 Redis 的哨兵模式、集群模式和管道模式
RedissonRedisson 是一个基于 Redis 实现的分布式、可伸缩的 Java 数据结构集合。包含了诸如 Map、Queue、Lock、Semaphore、AtomicLong 等强大功能

其中 Spring 种的 Spring Data Redis 整合了 Jedis 和 Lettuce,Redisson 则运用在分布式情况下

1. Jedis

快速上手

Jedis 的官网: https://github.com/redis/jedis

#1 创建 Maven 工程

在 IDEA 中,创建一个简单的 Maven 工程

image-20240213142642320

#2 引入依赖

在 pom.xml 中引入 Jedis 客户端依赖,再引入 junit 单元测试来帮助我们测试相关功能

<dependencies>
		<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
		<dependency>
		    <groupId>redis.clients</groupId>
		    <artifactId>jedis</artifactId>
		    <version>3.7.0</version>
		</dependency>
		
		<!-- 单元测试 -->
		<dependency>
		    <groupId>org.junit.jupiter</groupId>
		    <artifactId>junit-jupiter</artifactId>
		    <version>5.7.0</version>
		    <scope>test</scope>
		</dependency>
</dependencies>
#3 建立连接

首先在 test 文件夹下创建测试类 JedisTest

image-20240213144124276

@BeforeEach
void setUp() {
    // 1. 建立连接
    jedis = new Jedis("localhost", 6379);

    // 2. 设置密码(若有的话)
    // jedis.auth("1234567");

    // 3. 选择库
    jedis.select(0);
}
#4 测试 String

这里就选取 redis 中最常见的 String 类型进行测试,往 redis 中插入键为 “name” 的值

@Test
void testString() {
    // 插入数据,方法名就是redis命令名称,非常简单
    String result = jedis.set("name", "IceProgrammer");
    System.out.println("result = " + result);
    
    // 获取数据
    String name = jedis.get("name");
    System.out.println("name = " + name);
}
#5 测试资源

最后要释放资源,防止占用线程资源

@AfterEach
void tearDown() {
    if (jedis != null) {
        jedis.close();
    }
}

单元测试结果如下,成功在 redis 中插入并读取键为 name 的值

image-20240213150525369

这里我们已经成功导入 Jedis 到 redis 中,并测试了 String 类的插入与获取,这里再测试 Hash 类型比较复杂的数据类型,Jedis 命令和 redis 原生命令一样,学习成本很低,代码如下

@Test
void testHash() {
    // 插入 hash 数据
    jedis.hset("user:1", "name", "IceProgrammer");
    jedis.hset("user:1", "age", "22");
    
    // 获取数据
    Map<String, String> map = jedis.hgetAll("user:1");
    System.out.println(map);
}

运行结果如下

image-20240213161627091

Jedis 连接池

Jedis 本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐使用 Jedis 连接池代替 Jedis的直连方式

创建 JedisConnectionFactory 类,配置 Jedis 连接池的相关信息

public class JedisConnectionFactory {
    private final static JedisPool jedisPool;

    static {
        // 配置连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        // 最大连接数
        poolConfig.setMaxTotal(8);
        // 最大空闲连接
        poolConfig.setMaxIdle(8);
        // 最小空闲连接
        poolConfig.setMinIdle(0);
        // 最大等待时长
        poolConfig.setMaxWaitMillis(1000);
        // 创建连接池对象
        jedisPool = new JedisPool(poolConfig, "localhost",
                6379, 1000);
    }

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

在之前测试类中,把 Jedis 与 redis 直连的代码变成从我们创建的连接池中获取 Jedis

image-20240213211242535

这里扩展一点,在最后释放资源的代码中,我们不需要进行修改 jedis.close() 代码来释放资源,下面是 close 方法的源码

image-20240213215414140

当判断条件判断为从 Jedis 连接池中获取 Jedis 时,便会执行 returnResource 方法来归还连接资源

2. SpringDataRedis

SpringData 是 Spring 中数据操作的模块,包括对各种数据库的集成,其中对 Redis 的集成模块就叫做 SpringDataRedis,官网地址: https://spring.io/projects/spring-data-redis

  • 提供对不同 Redis 客户端的整合(Lettuce 和 Jedis)
  • 提供了 RedisTemplate 统一 API 来操作 Redis
  • 支持 Redis 的发布订阅模型
  • 支持 Redis 哨兵和 Redis 集群
  • 支持基于 Lettuce 的响应式编程
  • 支持基于 JDK、JSON、字符串、String 对象的数据序列化及反序列化
  • 支持基于 Redis 的 JDKCollection 实现
1) SpringDataRedis 快速上手

SpringDataRedis 中提供了 RedisTemplate 工具类,其中封装了各种对 Redis 的操作,并且将不同数据结构的操作 API 封装到了不同的类型:

API返回值类型说明
redisTemplate.opsForValue()ValueOpeations操作 String 类型数据
redisTemplate.opsForHash()HashOperations操作 Hash 类型数据
redisTemplate.opsForList()ListOperations操作 List 类型数据
redisTemplate.opsForSet()SetOperations操作 Set 类型数据
redisTemplate.opsForZSet()ZSetOperations操作 SortedSet 类型数据
redisTemplate通用的命令

SpringBoot 已经提供了对 SpringDataRedis 的支持,使用非常简单,首先需要创建一个 SpringBoot 项目,在 IDEA 中选择 Spring 项目

image-20240214120237312

在配置时选择一些需要的依赖

image-20240214120356229

#1 引入依赖

这里我们在初始化依赖时已经勾选了 SpringDataRedis 依赖,还需要加上连接池依赖 commons-pool2

<!-- Redis 依赖 -->
<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>
#2 配置文件

SpringBoot 需要在 resource 包下的 application.properties 文件下进行以下配置

截屏2024-02-14 12.28.15

#3 注入 RedisTemplate

在 SpringBoot 中配置的依赖都需要通过注入的方式来进行引用

@Resource
private RedisTemplate redisTemplate;
#4 编写测试方法
@Test
void testString() {
    // 写入一条 String 类型数据
    redisTemplate.opsForValue().set("name", "IceMan");
    // 获取 String 类型数据
    Object name = redisTemplate.opsForValue().get("name");
    System.out.println("name = " + name);
}

运行测试类,也成功往 Redis 中插入数据并读取

截屏2024-02-14 12.47.31

总结

SpringDataRedis 的使用步骤:

  1. 引入 spring-boot-starter-data-redis 依赖
  2. 在 application.properties 配置 redis 信息
  3. 注入 RedisTemplate
2) RedisSerializer

上面我们通过 RedisTemplate 来操作 Redis数据库插入了键为 name,值为 IceMan 的 String 类型键值对。现在我们通过命令行来读取 name 的值

image-20240214200737998

很有趣的是,redis 中的 name 是 IceProgrammer,并不是我们代码中写的 IceMan,这是怎么回事?我们先用 KEYS * 命令来看一下我们插入的数据究竟去哪儿了

image-20240214200910663

这里我们发现,除了键为 name 的键值,还有一个 name 前面加了一堆编码的键值,这是什么?通过 GET 命令,我们来获取它的值

image-20240214201728468

它的值也是 IceMan 前面加上了一堆不知所然的编码,为什么会莫名其妙加上一堆乱码?这里我们就要提到序列化

image-20240214202847022

redisTemplate.opsForValue() 的 set 方法接收的并不是 String 类型,而是 Object。这是因为 SpringDataRedis 会帮我对所传入的对象进行序列化,而 redisTemplate 默认序列化的方法是 JDK 的序列化方法

image-20240214222105054

image-20240214222212417

根据上面的 redisTemplate 源码可知所有的序列化工具都为默认 JDK 序列化工具,而 JDK 默认的序列化方式是通过 ObjectOutputStream 方法来写入(将 Java 对象转成字节)

默认序列化方式的缺点:

  • 结果可读性差,都为编码形式
  • 内存占用较大

我们查看所有适合 RedisSerializer 的序列化工具

image-20240215195536966

现在我们需要来设置 RedisTemplate 默认的序列化方式,防止编码出现在 redis 中

SpringDataRedis 的序列化方式

我们可以自定义 RedisTemplate 的序列化方式,代码如下

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建 redisTemplate 对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // 设置连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);

        // 创建 JSON 序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        // 设置 Key 的序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        // 设置 Value 的序列化
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jsonRedisSerializer);

        // 返回
        return redisTemplate;
    }
}

在对于 Key 的序列化,直接使用 StringRedisSerializer 序列化方式即可,而对于 Value 有可能会进行 Java 类的序列化,因此我们使用 GenericJackson2JsonRedisSerializer 序列化方式,能够将 Java 对象转成 JSON 格式储存到 redis 中

现在我们返回测试类,先给 redisTemplate 输入指定类型,如下

image-20240215201318619

其次我们需要引入 Jackson 相关依赖,我们上面用到了 Jackson 的类

<!-- Jackson -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

现在重新执行我们的代码,并在 redis中查看 name 的值

image-20240215202402059

现在存入的便是正常编码下的字符串,那再来测试一下 Java 类转 JSON 字符串是否顺利,在项目下创建 User 类

image-20240215202716912

测试类方法代码如下:

@Test
void testSaveUser() {
    // 写入数据
    redisTemplate.opsForValue().set("user:100", new User("IceMan", 22));
    // 获取数据
    User user = (User) redisTemplate.opsForValue().get("user:100");
    System.out.println("user = " + user);
}

运行该方法,可以看到输出结果是我们传入的 Java 对象

image-20240215203157115

image-20240215203240249

在 redis 中查看也是以 JSON 的形式保存。还能发现在 JSON 中,除了我们设置的两个字段,还有一个 @class": "com.ice.redis.entity.User 字段,这个字段的作用是当我们 redisTemplate 读取 JSON 字段时,能够根据类的地址来进行反序列化(想要了解可以学习 Java 的 反射

3) StringRedisTemplate

尽管 JSON 的序列化方式可以满足我们的需求,但依然存在一些问题,便是 @class": "com.ice.redis.entity.User 字段

为了在反序列化时知道对象的类型,JSON 序列化器会将类的 class 类型写入 JSON 结果中,存入 redis,会带来额外的内存开销

因此为了节省内存空间,我们并不会使用 JSON 序列化器来处理 value,而是使用统一的 String 序列化器,要求只能储存 String 类型的 key 和 value。当需要存储 Java 对象时,showing完成对象的序列化和反序列化

Spring 默认提供了一个 StringRedisTemplate 类,它的 key 和 value 的反序列化方式默认就是 String 方式,省去了我们自定义 RedisTemplate 的过程

这里用到的序列化工具为 GSON,当然也可以使用其他的序列化工具,例如 Fastjson、ObjectMapper 等等,引入以下依赖

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.9.1</version>
</dependency>

修改测试类中的代码,如下

@Resource
private StringRedisTemplate stringRedisTemplate;

private final static Gson GSON = new Gson();

@Test
void testSaveUser() {
    // 创建对象
    User user = new User("IceMan", 22);

    // 手动序列化
    String userJSON = GSON.toJson(user);

    // 写入数据
    stringRedisTemplate.opsForValue().set("user:200", userJSON);
    
    // 获取数据
    String JSONUser = stringRedisTemplate.opsForValue().get("user:200");

    // 手动反序列化
    User userFromRedis = GSON.fromJson(JSONUser, new TypeToken<User>() {
    }.getType());

    System.out.println("user = " + userFromRedis);
}

这里有关 GSON 序列化的代码不需要特意了解,重新执行代码,再来看数据库中储存的信息

image-20240215212539819

这样,我们就只存入了我们需要的 JSON 字段,并没有多余的信息

总结

RedisTemplate 的两种序列化方式

方案一:

  1. 自定义 RedisTemplate
  2. 修改 RedisTemplate 的序列化器为 GenericJackson2JsonRedisSerializer

方案二:

  1. 使用 StringRedisTemplate
  2. 写入 Redis 时,手动把对象序列化为 JSON
  3. 读取 Redis 时,手动把读取到的 JSON 反序列化为对象
4) RedisTemplate 操作 Hash 类型

使用 RedisTemplate 操作 Hash 类型并没有特别之处,要注意的是将 opsForValue 变成 opsForHash 方法,即可操作 Hash 类型的值,测试方法代码如下

@Test
void testHash() {
    stringRedisTemplate.opsForHash().put("user:400", "name", "IceProgrammer");
    stringRedisTemplate.opsForHash().put("user:400", "age", "21");

    Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("user:400");
    System.out.println("userMap = " + userMap);
}

运行代码,成功将 Hash 类型的值存入 redis 中,并且以 HashMap 的形式从 redis 中获取,运行结果如下

image-20240215215901219

image-20240215215840544

四、缓存

缓存就是数据交换的缓冲区(称作 Cache),是储存数据的临时地方,一般读写性能较高

image-20240218183917862

我们可以在上面任意一层使用缓存,缓存在为我们提供高度读写数据的功能时,也存在一些不可忽视的缺点

1. 添加 Redis 缓存

image-20240218184338498

以下是针对商铺信息缓存的示例代码

@Override
public Result queryById(Long id) {
    String key = CACHE_SHOP_KEY + id;
    // 1. 从 redis 中查询商铺缓存
    String shopJSON = stringRedisTemplate.opsForValue().get(key);

    // 2. 判断是否存在
    if (StrUtil.isNotBlank(shopJSON)) {
        // 3. 存在,直接返回
        Shop shop = JSONUtil.toBean(shopJSON, Shop.class);

        return Result.ok(shop);
    }
    // 4. 不存在,根据 id 查询数据库
    Shop shop = baseMapper.selectOne(Wrappers.<Shop>lambdaQuery()
            .eq(Shop::getId, id)
            .last("limit 1"));

    // 5. 数据库不存在,返回错误
    if (shop == null) {
        return Result.fail("店铺不存在!");
    }
    String JSONShop = JSONUtil.toJsonStr(shop);
    // 6. 数据库存在,写入 redis
    stringRedisTemplate.opsForValue().set(key, JSONShop);

    // 7. 返回
    return Result.ok(shop);
}

2. 缓存更新策略

以下是三种针对使用缓存解决数据一致性的策略

image-20240218185007641

业务场景

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
主动更新策略

目前在企业中主动更新策略主要使用以下三种策略:

  1. Cache Aside Pattern

由缓存的调用者,在更新数据库的同时更新缓存

  1. Read/Write Through Pattern

缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题

  1. Write Behind Caching Pattern

调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致性

由于后两者方法在实现和数据一致性保证上都不及第一种方法方便,因此企业中大部分采用第一种策略来保证数据的一致性

操作数据库时有三个问题需要考虑:

  1. 删除缓存还是更新缓存?

    • 更新缓存:每次更新数据库都更新,无效写操作较多(100次写操作但之间没有任何读操作)❌
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 ✅
  2. 如何保证缓存与数据库的操作同时成功或失败?(保证原子性)

    • 单体系统:将缓存与数据库操作放在一个事务
    • 分布式系统:利用 TTC 等分布式事务方案
  3. 先操作缓存还是先操作数据库?

    • 先删除缓存,再操作数据库(出现异常可能性大)

    • 先操作数据库,再删除缓存(出现异常可能性小)✅

      image-20240218194434120

总结

缓存更新策略等最佳实践方案

  1. 低一致性需求:使用 Redis 自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
    • 读操作
      • 缓存命中直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性操作

3. 缓存击穿

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库

企业里常见的解决方案:

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用较少,没有多余 key
    • 缺点:
      • 实现复杂
      • 存在误判可能

image-20240218204201184

  • 增强 id 的复杂度,避免被猜测 id 规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

4. 缓存雪崩

缓存雪崩是指在一时段大量的缓存 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力

image-20240218214726021

解决方案:

  • 给不同的 key 的 TTL 添加随机性
  • 利用 Redis 集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

5. 缓存击穿

缓存击穿问题也叫热点 key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击

image-20240218220604186

常见的解决方案:

  • 互斥锁
  • 逻辑过期

image-20240219111402146

image-20240219111521728

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ice Programmer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值