java学习day31(redis)

redis环境配置

Redis是一种键值型的NoSql数据库,这里有两个关键字:

  • 键值型
  • NoSql

其中键值型,是指Redis中存储的数据都是以key、value对的形式存储,而value的形式多种多样,可以是字符串、数值、甚至json:

而NoSql则是相对于传统关系型数据库而言,有很大差异的一种数据库。

上面这种拆分了存的方式有点松散

用这种方式就更加紧凑,这种方式也是key value 形式只不过value变成了更长的josn字符串

我们先来认识以下什么叫nosql

认识NoSQL

NoSql 可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库

1.1.1.结构化与非结构化

传统关系型数据库是结构化数据,每一张表都有严格的约束信息:字段名、字段数据类型、字段约束等等信息,插入的数据必须遵守这些约束:

而NoSql则对数据库格式没有严格约束,往往形式松散,自由。

可以是键值型:

也可以是文档型:

甚至可以是图格式:

1.1.2.关联和非关联

传统数据库的表与表之间往往存在关联,例如外键:

而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合:

复制代码
{
    id: 1,
    name: "张三",
    orders: [
    {
        id: 1,
        item: {
            id: 10, title: "荣耀6", price: 4999
            }
    },
    {
        id: 2,
        item: {
            id: 20, title: "小米11", price: 3999
            }
    }
    ]
}

此处要维护"张三"的订单与商品"荣耀"和"小米11"的关系,不得不冗余的将这两个商品保存在张三的订单文档中,不够优雅。还是建议用业务来维护关联关系。

1.1.3.查询方式

传统关系型数据库会基于Sql语句做查询,语法有统一标准;

而不同的非关系数据库查询语法差异极大,五花八门各种各样。

1.1.4.事务

传统关系型数据库能满足事务ACID的原则。

非关系型数据库往往不支持事务,或者不能严格保证ACID的特性,只能实现基本的一致性。

1.1.5.总结

除了上述四点以外,在存储方式、扩展性、查询性能上关系型与非关系型也都有着显著差异,总结如下:

  • 存储方式
    • 关系型数据库基于磁盘进行存储,会有大量的磁盘IO,对性能有一定影响
    • 非关系型数据库,他们的操作更多的是依赖于内存来操作,内存的读写速度会非常快,性能自然会好一些
  • 扩展性
    • 关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
    • 非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
    • 关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦

安装redis 这里到了很关键的一点

之前我们在苍穹外卖学redis的时候用的是windows版的redis ,实际上redis官方只发布了linux版的,我们在这里使用的windows版的redis是微软自己写的,但是这个早就不维护了

所以如果我们想用linux版的redis 这里有几种解决方法

1.用虚拟机运行linux(这个也是黑马教程使用的方法,但是我实在不想,搞的电脑好卡)

2.购买linux云服务(在这里我将购买阿里云服务器,使用这种方式)强烈建议你选择 Ubuntu

请点击右上角的 Ubuntu 图标,然后在下拉菜单中保持默认的 24.04 (或者如果你看到下拉框里有 22.04 ,选 22.04 也可以,都很稳定)。

为什么强烈推荐选 Ubuntu?

  1. 避开 CentOS 的"停更坑": 你学的《苍穹外卖》或其他老教程里,老师演示用的基本都是 CentOS 7。但现实情况是,CentOS 官方已经停止维护了。对于新手来说,现在装 CentOS 经常会遇到 yum****源失效、无法下载软件的报错,非常搞心态。
  2. 新手最友好,社区最庞大: Ubuntu 是目前全世界开发者用得最多的 Linux 系统。你在学习中遇到任何报错,去百度或 Google 搜索,出来的解决办法 90% 都是基于 Ubuntu 的,抄代码都能直接抄。
  3. 完美适配 Docker: Ubuntu 安装 Docker 的过程极其丝滑,官方支持得最好。

3.用本地用docker 装reidis的容器(这个也是大伙很推荐的一种方式)

4.用云服务器用docker安装redis的容器

因为我本人想玩云服务器 ,又想学习docker所以在这里我选择了第四种方式,而且最重要的是: 这正是目前企业里最主流、最标准的现代后端部署方案。

启动

redis的启动方式有很多种,例如:

  • 默认启动
  • 指定配置启动
  • 开机自启

默认启动

安装完成后,在任意目录输入redis-server命令即可启动Redis:

redis-server

如图:

这种启动属于前台启动,会阻塞整个会话窗口,窗口关闭或者按下CTRL + C则Redis停止。不推荐使用。

之前我们在苍穹外卖用的就是这种方式

教程里面用的是指定配置启动 (好像是改了守护进程改成yes,意思就是可以让redis在后台运行)

其实这些我们都不用管了,现在是云服务器 + Docker 运行 Redis,不需要再单独管教程里那个 daemonize yesdocker run -d 已经起到了后台运行的作用。并且因为我们用的是用云服务器启动的,只要云服务器不关机,就一直会开着docker,和redis 不用我们手动去启动

我们第一次启动redis就是通过这条命令启动的

复制代码
docker run --name my-redis -p 6379:6379 -d redis

而docker是我们通过这条命令启动的

复制代码
systemctl start docker

并且我们的docker和reids都设置了开机自启

设置开机自启

怎么检查有没有开机自启

执行:

dockerinspect-f'{{.HostConfig.RestartPolicy.Name}}'my-redis

如果返回:

no

说明 没有 设置开机自启。

如果返回:

always

说明已经设置了。

怎么给 Docker 版 Redis 设置开机自启

如果容器已经创建好了,可以直接执行:

dockerupdate--restart=alwaysmy-redis

接下来黑马教程开启了日志,和修改了redis的密码,但是docker默认就是开启的状态

Docker 里通常默认就能看日志,但这不是"默认开启了 Redis 文件日志",而是 Docker 默认收集了 Redis 输出到控制台的日志。

所以我们只需要用

docker logs my-redis

去查看我们的日志

然后在这里我们修改密码采取这种方式

方式 2:推荐,重建容器并带密码启动

先停掉并删除当前容器:

复制代码
docker stop my-redis
docker rm my-redis

然后重新创建:

复制代码
docker run -d --name my-redis --restart=always -p 6379:6379 redis redis-server --requirepass 123456

这样意思是:

  • 后台运行
  • 容器名还是 my-redis
  • 开机自动恢复
  • Redis 启动时直接带密码

以后连接时就要这样:

复制代码
docker exec -it my-redis redis-cli -a 123456

或者进交互后:

复制代码
AUTH 123456

改密码后怎么验证

1. 不带密码连

docker exec -it my-redisredis-cli

然后:

PING

如果提示需要认证,说明密码生效了。

2. 带密码连

docker exec-it my-redis redis-cli-a123456

然后:

PING

返回:

PONG

安装完成Redis,我们就可以操作Redis,实现数据的CRUD了。这需要用到Redis客户端,包括:

  • 命令行客户端
  • 图形化桌面客户端
  • 编程客户端

Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:

redis-cli [options] [commonds]

我们发现在docker里面是这样的,后面基本是一样的

复制代码
docker exec -it my-redis redis-cli -a 123456

这里我来解释一下

这多出来的 docker exec -it my-redis,其实就是 Docker 专属的**"开门进房"暗号**。

我们把它拆开来,一个词一个词给你翻译成大白话:

🔍 逐词拆解:

  1. docker exec
    • 字面意思: 执行(execute)。
    • 大白话: 它的意思是**"我要在一个正在运行的集装箱(容器)里面,执行一个动作"**。相当于你拿起了对讲机,准备对集装箱里面下达指令。
  1. -it (最关键,也是新手最迷惑的)
    • 字面意思: 它是两个字母的缩写,-i (interactive,保持交互) 和 -t (tty,分配一个伪终端屏幕)。
    • 大白话: 相当于**"给我接通视频通话,并且允许我持续讲话"**。
    • 如果不加它会怎样? 如果你不加 -it,系统确实会把后面的指令传进容器,但容器执行完立刻就"挂断电话"了,你根本看不到反馈,也没法连续敲下一条命令。加上 -it,你才能像面对面一样,看到那个熟悉的 127.0.0.1:6379> 交互界面。
  1. my-redis
    • 字面意思: 容器名称。
    • 大白话: 这是你给集装箱起的**"门牌号"**。因为服务器上可能同时跑着好几个 Redis、好几个 MySQL,你得明确告诉 Docker,你要进的是叫 my-redis 的这个房间。

其中常见的options有:

  • -h 127.0.0.1:指定要连接的redis节点的IP地址,默认是127.0.0.1
  • -p 6379:指定要连接的redis节点的端口,默认是6379
  • -a 123321:指定redis的访问密码

其中的commonds就是Redis的操作命令,例如:

  • ping:与redis服务端做心跳测试,服务端正常会返回pong

不指定commond时,会进入redis-cli的交互控制台:

然后这里有一个易混淆的点

在你敲下 docker exec -it my-redis redis-cli****并且没指定 IP 的这种情况下,它连的是**「Docker 容器(玻璃房)内部专属的 127.0.0.1」**。

并不是我服务器的公网IP

接下来我们要去看看redis图形化客户端,这个并不是由官方编写的,而是由一位GitHub大神编写的,现在已经开源了

其实之前苍穹外卖也用到了这个

这个我们准备连接到我们云服务器里面的redis

  • Host: 你的云服务器公网IP
  • Port: 6379
  • Password: 你的密码
  • Username:留空
  • Connection Name: my-redis
  • Separator: :

但是这里有一个点要注意

我们要去服务器开放这个6379端口

但是有一个疑惑,之前不是通过ssh访问到redis了吗

但这不等于"你的本地电脑也能直接连 Redis"

因为你现在成功的链路其实是:

你的电脑 -> SSH -> 云服务器 -> Redis

其实就相当于我们在云服务器本身连 redis了

这只能证明放行了22端口

而不是:

你的电脑 -> 直接连 Redis 的 6379 端口

这两件事不是一回事。

我们来到阿里云的防火墙模板,准备放行这个

这样填写 0.0.0.0意思就是放行所有ip

创建之后应用至示例就可以了

简单来说,你的公网 IP 能连上 Docker 里的 Redis,是因为你在启动容器时那句神奇的 -p 6379:6379

为什么能连上?(核心逻辑)

我们可以把这个过程想象成一个**"转接电话"**的过程:

  1. 公网 IP 是你的"大门": 当你在桌面端输入公网 IP 时,你的请求实际上是发送到了 云服务器的网卡 上。
  2. 宿主机(服务器)是"接线员": 云服务器收到 6379 端口的请求后,它并不知道怎么处理,但它查了一下自己的"转接表"。
  3. 端口映射是"转接规则": 因为你运行了 -p 6379:6379**,这就相当于告诉服务器:"只要有人在大门口找 6379 分机,你就把电话直接转接到 Docker 内部那个叫** my-redis****的房间的 6379 端口去。"
  4. Docker 内部的 127.0.0.1: Redis 确实在它自己的小房间里听着 127.0.0.1:6379**,但因为它接到了宿主机转接过来的"电话",连接就建立成功了。**

关键点:网络隔离

  • 容器视角: Redis 认为自己运行在本地( 127.0.0.1**),它是安全的、隔离的。**
  • 宿主机视角: 宿主机(你的 Ubuntu)在 6379 端口开启了一个"监听员",专门负责把流量往容器里搬。
  • 外部视角: 你只需要知道服务器的公网 IP 即可,Docker 的内部细节对你来说是透明的。

当时就是这条命令设置的转接表

复制代码
docker run --name my-redis -p 6379:6379 -d redis

这些redis环境配置就大功告成了

接下来来到命令学习

Redis常见命令

下面三种特殊类型,其实本质上也是字符串,所以为什么叫基本类型

然后这里的Hash 见的比较少它是一个键值对的集合(field-value mapping)。

这些命令我们可以在官方文档 https://redis.io/docs/latest/commands/

通过组来查询

当我们输入一个key然后按tab键 它是会自动补全的

之前好像苍穹外面redis也接触过这个

String结构

keys是模糊查询,而且redis是单线程的,如果没有设置主从节点,在主节点上跑会直接卡死必须得查完才能进行接下来操作,所以在生产上不建议使用这个keys

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

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

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

需要注意的是这里面的mget返回的应该是一个数组 在redis 里面数组就是这样1),2)这种方式这样展示出来

现在有一个这个问题

我们发现这个和hash其实非常像,但是并不是hash

1. Hash 图里的大括号:只是为了给人类看的(视觉伪装)

看你发的这张图,Hash 后面跟着 {name: "Jack", age: 21}真相是:在 Redis 的底层 Hash 结构中,根本不存在大括号 { **}**,也不存在冒号 :****! PPT 之所以这么画,仅仅是为了用一种程序员熟悉的格式(伪代码)告诉你:"这是一个包含多个属性的对象"。

如果你真要往 Hash 里存这个数据,你敲的真实命令是这样的:

Plaintext

复制代码
HSET user:1 name "Jack" age 21

看!完全没有大括号! Redis 会在内部建一个小表格,左边是 name,右边是 Jack。

2. String 里的 JSON 大括号:是实打实的真实字符

回到你上一个问题(图2),如果你把 Java 对象变成 JSON 字符串存进 String 里,你敲的命令是这样的:

Plaintext

复制代码
SET user:1 "{\"id\":1, \"name\":\"Jack\", \"age\":21}"

在这里,大括号 {****、双引号 "****、冒号 :****,统统都是真实的文字字符! 它们实实在在地占据着内存。Redis 根本不认识里面的 name 和 age,它只觉得这是一个总长度为 37 个字符的"长句子"。

这里有两种写法第一种是教程里面的写法

穿法一:外层穿"单引号"(你的写法,强推! 🌟

  • Plaintext

    SET heima:user:1 '{"id":1, "name":"Jack", "age": 21}'

因为外层是单引号 ',内层是双引号 ",两者长得不一样,不会打架。所以完全不需要加 \ 去转义。非常清爽!在 Redis 命令行(CLI)里测试时,我们都这么干。

穿法二:外层穿"双引号"(我刚才的写法,看起来很乱)

加\是因为两个双引号,里面双引号需要 \作为转义字符

  • Plaintext

    SET heima:user:1 "{"id":1, "name":"Jack", "age": 21}"

Hash结构

List结构

为什么用 BLPOP**/** BRPOP****就可以?

在 Redis 中,LPOPRPOP 是普通的出队命令(从左边/右边弹出数据)。如果 List 为空,它们会立刻返回 nil(空)。 如果在实际开发中,你的程序想第一时间处理队列里的新消息,使用普通的 RPOP,你的代码就必须写成一个死循环(轮询),不断地去问 Redis:"有新数据了吗?"

  • 弊端: 这种不断发问的方式极其浪费网络资源和 CPU 性能。

BLPOP**(Blocking Left POP) 和** BRPOP**(Blocking Right POP) 则是带有"阻塞"特性的命令。** 当使用 BRPOP 去读取一个空的 List 时,Redis 不会立刻返回空,而是会把这个客户端的连接挂起(进入休眠状态)。这也就是图片中提到"出队时采用 BLPOP 或 BRPOP"就能模拟阻塞队列的原因。

timeout****参数设置的是"最长等待时间",而不是"固定等待时间"。

1. 设置 5 秒:数据来了会立即返回吗?

会,而且是秒回。 如果你执行 BLPOP queue 5

  • 如果数据在第 0.1 秒就来了: 消费者会立即带着数据返回,整个过程只耗时 0.1 秒。它绝不会傻等够 5 秒才出来。
  • 如果这 5 秒内一直没数据: 它才会一直等到第 5 秒结束,然后两手空空地返回一个 nil

所以,这个 5 秒的意思是:"我最多等你 5 秒,这期间你只要敢出现,我立刻抓着你走。"

2. 设置 0 秒:数据来了不管吗?

完全相反,设置 0 秒是为了"哪怕等一辈子也要等到你"。 在 Redis 中,timeout0 表示无限期阻塞

  • 只要队列里没有数据,消费者就永远停在那行代码不动,不消耗 CPU,也不返回结果。
  • 但是 ,一旦有任何数据进入队列,消费者会瞬间被唤醒并立即弹出数据

所以,设置 0 秒的意思是:"我会一直等下去,直到你出现的那一刻,我第一时间处理。"

它在这里会一直等待

1. 超时后数据才来,会立即 Pop 吗?

不会。

2. 如果用普通的 RPOP****会怎么样?

对比之下,普通的 RPOP****甚至连那 5 秒钟都不会等:

  • 瞬间失败: 当你执行 RPOP****的那一刻,如果队列里没数据,它在 0.001 秒内就直接返回 nil****了。
  • 擦肩而过: 如果数据在第 0.1 秒来了,普通 RPOP****也拿不到,因为它早在 0.1 秒之前就执行完毕并离开了。

两者的本质区别:

  • 普通 RPOP**:** 是"快照查询"。它只看执行那一瞬间队列有没有东西,没有就撤。
  • BLPOP 5**: 是"持续监听"。它会像开了一个限时的捕捉网,在 5 秒内任何时刻进来的数据都会被网住;但网一旦时间到了收回来,之后进来的鱼就只能留在池子里了。**

这个阻塞是对于消费者而言的

3. 一个绝对清晰的比喻:回转寿司店

Redis 队列 想象成寿司店里的 传送带 。 把 消费者(你的代码) 想象成 吃货顾客 。 把 生产者 想象成 后厨师傅

  • 普通出队(RPOP): 顾客走到传送带前,看了一眼,没有三文鱼寿司。顾客立刻转身走出店门,去干别的事了。( 不阻塞
  • 阻塞出队(BLPOP 5秒): 顾客走到传送带前,没有三文鱼。顾客没有走,而是拉了个板凳坐在传送带旁边,死死盯着出菜口等 5 分钟。(顾客被阻塞了! 这 5 分钟里他什么也干不了)

set类型

sortedset类型

这个经常用来被执行排行榜功能,之前苍穹外卖用的就是这个

Redis Java客户端

下面是官方文档推荐的redis客户端

我们不用一个去学习这三个java客户端,因为spring data 可以整合、,学习了它等于两个都会了

我们发现这里有一个我们从来没有见过的注解@ before each

它是 JUnit 5(Java 常用的单元测试框架)中的一个核心注解。它的主要作用是:

在当前测试类中的每一个 @Test****测试方法执行之前,都会先自动执行一次被它修饰的方法。

在你这张代码截图中的具体作用:

@BeforeEach 修饰的 setUp() 方法在这里被用来初始化测试环境。具体的好处如下:

  • 避免代码重复: 之后你肯定会在这个测试类里编写多个测试方法(比如测试 Redis 的读数据、写数据、删除数据等操作)。如果没有这个注解,你需要在每一个测试方法开头都写一遍"建立连接"、"验证密码"和"选择库"的代码。

  • 因为你但凡执行任何操作之前都得先建立连接

  • 如果不使用 @BeforeEach 注解,你必须在每一个测试方法里手动编写连接 Redis 的代码。假设我们要写两个测试:一个测试存数据,一个测试取数据,代码看起来就会像这样:

    package com.heima.test;

    import org.junit.jupiter.api.Test;
    import redis.clients.jedis.Jedis;

    public class JedisTest {

    复制代码
      @Test
      void testSaveData() {
          // 【重复代码开始】每次都要手动建立连接
          Jedis jedis = new Jedis("192.168.150.101", 6379);
          jedis.auth("123321");
          jedis.select(0);
          // 【重复代码结束】
    
          // 真正的测试逻辑
          jedis.set("name", "Gemini");
          System.out.println("数据保存成功");
          
          // 用完还得记得关
          jedis.close();
      }
    
      @Test
      void testGetData() {
          // 【重复代码开始】换个测试方法,还得再写一遍一模一样的连接代码
          Jedis jedis = new Jedis("192.168.150.101", 6379);
          jedis.auth("123321");
          jedis.select(0);
          // 【重复代码结束】
    
          // 真正的测试逻辑
          String name = jedis.get("name");
          System.out.println("获取到的名字是: " + name);
    
          // 用完还得记得关
          jedis.close();
      }

    }

这里select(0)是选择redis的一个库,然后过程有点像之前命令 redis cli那个命令

@AfterEach****的核心作用

在当前测试类中的每一个 @Test 测试方法执行完毕之后,都会自动执行一次被它修饰的方法。

它的主要使用场景是资源释放与清理,比如:

  1. 关闭连接: 关闭数据库连接、Redis 连接、网络连接等(防止连接池占满)。
  2. 清理测试数据: 如果你的测试往数据库里写入了脏数据,可以在这里删掉,保证不影响别人。
  3. 关闭文件流: 释放被占用的文件句柄。

为什么jedis 需要连接池就可以解决线程安全的问题

因为连接池不是让同一个 Jedis 对象变成线程安全,而是避免多个线程共享同一个 Jedis 对象。

Jedis 本身可以理解为一个 Redis 连接对象,内部维护着 socket、输入输出流、请求响应状态等。如果多个线程同时用同一个 Jedis 实例,可能出现这种问题:

复制代码
// 线程 A
jedis.set("name", "Tom");

// 线程 B
jedis.get("age");

两个线程同时往同一个连接里写命令、读响应,可能导致命令和响应错乱。例如线程 A 发了 SET,线程 B 发了 GET,结果线程 A 读到了线程 B 的响应,线程 B 又读到了别的响应,这就是线程不安全。

连接池的做法是:

复制代码
Jedis jedis = jedisPool.getResource();

每个线程需要操作 Redis 时,从池子里借一个 Jedis****连接。用完之后关闭:

需要用的时候就给你

前面这些其实稍微看看就行,真正需要我们学的是这个SpringDataRedis ,现在它来了

SpringDataRedis

6.1.快速入门

SpringBoot已经提供了对SpringDataRedis的支持,使用非常简单:

6.1.1.导入pom坐标

复制代码
<!--common-pool-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

6.1.2 .配置文件

复制代码
spring:
redis:
host: 192.168.150.101
port: 6379
password: 123321
lettuce:
pool:
max-active: 8  #最大连接
max-idle: 8   #最大空闲连接
min-idle: 0   #最小空闲连接
max-wait: 100ms #连接等待时间

我们看到这里有一个 lecttuce pool其实就是配置lecttuce 连接池

在这里连接池里面有两套,可以选择是否是lecttuce连接池,还是jedis连接池,spring默认使用的是lecttuce连接池,如果你想要选择jedis的依赖的话,还得在pom 文件另外在引入jedis的依赖

|-------------------|---------------------------------------------------------------|
| 参数 | 含义 |
| max-active: 8 | 连接池最多能同时创建 8 个 Redis 连接。如果同时有很多线程访问 Redis,最多只能有 8 个连接被使用。 |
| max-idle: 8 | 连接池中最多保留 8 个空闲连接。用完的连接不会马上销毁,会先放回池里复用,但最多保留 8 个。 |
| min-idle: 0 | 连接池中最少保留 0 个空闲连接。也就是空闲时可以不提前准备连接。 |
| max-wait: 100ms | 当连接池里的连接都被占用时,新的请求最多等待 100 毫秒。超过还拿不到连接,就会报错。 |

6.1.3.测试代码

复制代码
@SpringBootTest
class RedisDemoApplicationTests {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    void testString() {
        // 写入一条String数据
        redisTemplate.opsForValue().set("name", "虎哥");
        // 获取string数据
        Object name = redisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }
}

贴心小提示:SpringDataJpa使用起来非常简单,记住如下几个步骤即可

SpringDataRedis的使用步骤:

  • 引入spring-boot-starter-data-redis依赖
  • 在application.yml配置Redis信息
  • 注入RedisTemplate
6.2 .数据序列化器

你如果像代码这样去存,你get最后还是会拿到虎哥,但是

其实在redis里面 其实存进去的东西已经被序列号剁碎了,

复制代码
Java里的字符串 "虎哥"
        ↓ 序列化
Redis中保存的数据

如果你使用原生的话reids中保存的数据就是一串乱码

\xac\xed\x00\x05t\x00\x06虎哥

但是只要序列化规则不变,我们可以把这串乱码拿回到java对象输出序列化之前的"虎哥"

复制代码
Redis中保存的数据
        ↓ 反序列化
Java里的对象 "虎哥"

RedisTemplate可以接收任意Object作为值写入Redis:

只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的:

缺点:

  • 可读性差
  • 内存占用较大

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

复制代码
@Configuration
public class RedisConfig {

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

以后用这个 RedisTemplate<String, Object> 存 value 的时候:

复制代码
redisTemplate.opsForValue().set("user:1", user);

会发生:

复制代码
Java 对象 user
        ↓
GenericJackson2JsonRedisSerializer
        ↓
JSON 格式的数据
        ↓
存入 Redis

取的时候:

复制代码
Object obj = redisTemplate.opsForValue().get("user:1");

会发生:

复制代码
Redis 里的 JSON 数据
        ↓
GenericJackson2JsonRedisSerializer
        ↓
Java 对象

所以整体就是:

复制代码
存:Java 对象 -> JSON
取:JSON -> Java 对象

这里采用了JSON序列化来代替默认的JDK序列化方式。最终结果如图:

整体可读性有了很大提升,并且能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。不过,其中记录了序列化时对应的class名称,目的是为了查询时实现自动反序列化。这会带来额外的内存开销。

6.3 StringRedisTemplate

尽管JSON的序列化方式可以满足我们的需求,但依然存在一些问题,如图:

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

为了减少内存的消耗,我们可以采用手动序列化的方式,换句话说,就是不借助默认的序列化器,而是我们自己来控制序列化的动作,同时,我们只采用String的序列化器,这样,在存储value时,我们就不需要在内存中就不用多存储数据,从而节约我们的内存空间

这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。

省去了我们自定义RedisTemplate的序列化方式的步骤,而是直接使用:

复制代码
@SpringBootTest
class RedisStringTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void testString() {
        // 写入一条String数据
        stringRedisTemplate.opsForValue().set("verify:phone:13600527634", "124143");
        // 获取string数据
        Object name = stringRedisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }

    private static final ObjectMapper mapper = new ObjectMapper();

    @Test
    void testSaveUser() throws JsonProcessingException {
        // 创建对象
        User user = new User("虎哥", 21);
        // 手动序列化
        String json = mapper.writeValueAsString(user);
        // 写入数据
        stringRedisTemplate.opsForValue().set("user:200", json);

        // 获取数据
        String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
        // 手动反序列化
        User user1 = mapper.readValue(jsonUser, User.class);
        System.out.println("user1 = " + user1);
    }

}

但是这个手动转换String对象,和手动把String 对象 转换成java对象这个过程还是不能省略

这里用到了一个工具mapper就可进行手动转换,然后这个工具其实和我们之前学过fastjosn是一样的

此时我们再来看一看存储的数据,小伙伴们就会发现那个class数据已经不在了,节约了我们的空间~

最后小总结:

RedisTemplate的两种序列化实践方案:

6.4 Hash结构操作

在基础篇的最后,咱们对Hash结构操作一下,收一个小尾巴,这个代码咱们就不再解释啦

马上就开始新的篇章~~~进入到我们的Redis实战篇

复制代码
@SpringBootTest
class RedisStringTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


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

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

小技巧

选中按ctrl + H 有哪些实现

1. 普通 String(随便写的字)

如果你在这张白纸上随便写一句:"今天天气真不错,验证码是 123456"。

  • 这张纸就是 String
  • 内容没有任何严格的格式,人类能看懂,但机器很难从里面准确抠出"验证码"这三个字。

2. JSON(画好的表格)

如果你在同样的一张白纸上,用尺子画了一个严格的表格:

JSON

复制代码
{
  "天气": "不错",
  "验证码": "123456"
}
  • 这张纸依然是 String(数据类型没变,还是纸)。
  • 但是,你赋予了这张纸严格的语法格式 (大括号、双引号、冒号),这就是 JSON

JSON 不是一种独立于 String 之外的全新数据类型,JSON 仅仅是"一种按照特定标点符号规则写出来的、长得很特殊的 String"。 这就好比"诗歌"和"汉字"的关系。诗歌(JSON)和日常大白话(普通 String)读起来感觉完全不一样,但它们本质上都是由汉字(String)组成的。

|---------------------|------------------------------------------------------------------------|--------------------------------------------------------|---------------------------------------------------------|
| 阶段 / 步骤 | 方案 A:老做法RedisTemplate<String, Object> | 方案 B:当前推荐做法StringRedisTemplate | 核心区别点 |
| 1. 初始形态 (Java 内存中) | Java 对象 立体结构:User | Java 对象 立体结构:User | 完全一样。此时它们存在于 Java 虚拟机中,Redis 看不懂。 |
| 2. 触发动作 (谁负责转换) | 隐式/自动 调用 set() 时,底层的 GenericJackson2JsonRedisSerializer 偷偷接手。 | 显式/手动 你在代码里自己写了 mapper.writeValueAsString(user)。 | 方案 A 是框架帮你干; 方案 B 是你自己亲手干。 |
| 3. 转换结果的 Java 类型 | java.lang.String | java.lang.String | 重点! 无论谁去转,在 Java 里生成的都是一个普通的 String 变量。 |
| 4. 转换结果的 具体内容 (格式) | 带赘肉的 JSON 格式: '{"@class":"com.xxx.User", "name":"虎哥", "age":21}' | 纯净的 JSON 格式: '{"name":"虎哥", "age":21}' | 虽然都是 String,也符合 JSON 语法,但 方案 A 强行塞入了长长的一段 Java 类路径。 |
| 5. 存入 Redis (传输动作) | 将上面的长 String 发给 Redis。 | 将上面的短 String 发给 Redis。 | StringRedisTemplate 直接放行短 String,不加任何干预。 |
| 6. 最终归宿 (Redis 仓库中) | Redis String 类型 占用内存较大。 | Redis String 类型 占用内存极小。 | Redis 根本不知道什么是 JSON,它只负责把这一长串字符当做 String 数据结构存盘。 |

此外**'{"@class":"com.xxx.User", "name":"虎哥", "age":21}'**

这个既是string 也是josn

1. 从"物理外壳"来看:它是绝对的 String(字符串)

不管这段文字长得多么有规律,只要它被引号包起来(单引号或双引号),在 Java 虚拟机和 Redis 的底层看来,它就是一串 纯文本字符

  • 计算机不会管里面写的是"虎哥"还是 @class**,它只知道这里有 50 个字符,需要占用一点内存空间。**
  • 它的 数据类型 就是 java.lang.String**。**

2. 从"内部灵魂"来看:它是完美的 JSON(格式规范)

虽然它的外壳是字符串,但它的内容并没有乱写,而是 严格遵守了 JSON 的国际通用语法规则

  • 最外层有大括号 **{}**包裹。
  • 里面的属性名(如 "name"****、 "age"****)都加了双引号。
  • 属性名和属性值之间用冒号 **:**隔开。
  • 不同的属性之间用逗号 **,**隔开。

再比如

template.setHashKeySerializer(RedisSerializer.string());

// 设置Value的序列化

template.setValueSerializer(jsonRedisSerializer);

这连个其实都是转string,后面一个也是转string 只不过是josn 格式的string 前面一个是纯string 不带josn格式的吗

实验:把 Java 字符串 **"hello"**存进 Redis

1. 交给 RedisSerializer.string()****(专门处理 Key 的)

  • 它的性格:极度耿直,原样输出。
  • 处理过程 :它看到你传进来一个字符串 "hello",它什么废话都不加。

最终发给 Redis 的底层文本

  • Plaintext

    hello

  • 结果 :非常干净!在 Redis 里,这个 Key 的名字就叫 hello

1. 定义时的双引号 = 快递包装盒

当你在 IDEA 里敲下这行代码:

Java

复制代码
String str = "hello";

这里的双引号 "" 根本不是数据的一部分,它是 Java 语言规定的语法记号(快递包装盒)

  • 为什么需要这个包装盒? 因为 Java 编译器(那个负责检查你代码有没有错的监工)是个死脑筋。如果代码里写的是:String str = hello;(没加引号),编译器就会到处找:"有没有一个叫 hello 的变量啊?有没有叫 hello 的类啊?" 找不到它就会报错!
  • 所以,你加上双引号,就是明确告诉编译器:"大哥,这是一个纯文本,不是代码指令,你别管它什么意思,直接打包收下就行!"

2. 传进内存后 = 拆快递把包装扔了

当你的程序跑起来,编译器把你的代码翻译成了计算机能懂的机器码,并把这个字符串真正存进内存(RAM)时,它会把作为记号的双引号(包装盒)直接扔掉!

计算机内存里,真正存下来的只有"货物本身",也就是 h``e``l``l``o 这 5 个字母。

2. 交给 jsonRedisSerializer**(专门处理 Value 的)**

  • 它的性格:职业病严重,万物皆 JSON。
  • 处理过程 :它看到你传进来一个字符串 "hello"。它心想:"我的职责是把一切变成标准 JSON。标准的 JSON 字符串,外层必须有双引号包围!"

最终发给 Redis 的底层文本

  • Plaintext

    "hello"

JSON 官方支持的 6 种基本类型

根据国际通用标准,一个合法的 JSON 字符串,它的最外层可以是以下 6 种情况中的任何一种

  1. 对象 (Object):也就是你最熟悉的带有大括号的。
    • 举例:{"name": "虎哥"}
    • 对应 Java:普通的实体类(如 User 对象、Map)。
  1. 数组 (Array):带有中括号的。
    • 举例:["苹果", "香蕉"] 或者 [{"name":"张三"}, {"name":"李四"}]
    • 对应 Java:ListSet、数组。
  1. 字符串 (String) :带有双引号的纯文本。( 👉 也就是我们上一回合聊的情况)
    • 举例:"hello"
    • 对应 Java:String
  1. 数字 (Number):光秃秃的数字。
    • 举例:21 或者 3.14
    • 对应 Java:int, Integer, Double 等。
  1. 布尔值 (Boolean)
    • 举例:true 或者 false
    • 对应 Java:boolean, Boolean
  1. 空值 (Null)
    • 举例:null
    • 对应 Java:null

之后我们会做我们的第二个项目黑马点评项目

这个会单独开一个文档记录

相关推荐
小碗羊肉1 小时前
【从零开始学Java | 第四十一篇】深入多线程
java·开发语言
xuhaoyu_cpp_java1 小时前
MyBatis学习(一)
java·经验分享·笔记·学习·mybatis
wuxinyan1232 小时前
Java面试题50:Kubernetes 全栈知识体系之一
java·kubernetes·面试题
不吃肥肉的傲寒2 小时前
Graphify安装与结合claude code使用指南
java·python·ai编程·图搜索
seven97_top2 小时前
Tomcat的架构设计和启动过程详解
java·tomcat
我是无敌小恐龙2 小时前
Java SE 零基础入门 Day05 类与对象核心详解(封装+构造方法+内存+变量)
java·开发语言·人工智能·python·机器学习·计算机视觉·数据挖掘
va学弟2 小时前
Agent入门开发(2):个性化功能添加
java·服务器·ai
8486981192 小时前
Cursor 用 Java + Vue3 做了一个可落地的酒店管理系统(HMS),支持多门店、RBAC、财务结算,源码开源!
java·开发语言·开源
爱上好庆祝2 小时前
学习js的第三天
前端·css·人工智能·学习·计算机外设·js