文章目录
- 前言
- 1、缓存 概念知识
- 1.1、什么是缓存
- 1.2、缓存的优缺点
- 1.3、为什么使用缓存
- 2、Redis 概念知识
- 2.1、Redis 简介
- 2.2、为什么用 Redis 作为缓存
- 2.3、Redis 支持的数据类型
- 2.3、Redis是如何判断数据是否过期
- 2.4、过期的数据的删除策略
- 2.5、Redis 事务
- 2.6、Redis 持久化机制
- 2.7、如何保证缓存和数据库数据的⼀致性
- 2.8、Redis 缓存常见问题
- 2.8.1、缓存穿透
- 2.8.2、缓存击穿
- 2.8.3、缓存雪崩
- 2.9、Redis有哪些部署方案
- 3、SpringBoot 整合 Redis 缓存
- 3.1、使用 Redis 缓存
- 3.2、使用 SpringCache 的注解
- 3.2.1、注解说明
- 3.2.2、常用注解配置参数
- 3.2.3、自动缓存案例
前言
下载 Redis
我们下载zip格式,解压之后,就是这样子
启动服务 CMD 执行 redis-server redis.windows.conf
下载 Redis可视化工具 Another Redis DeskTop Manager
1、缓存 概念知识
1.1、什么是缓存
缓存就是数据交换的缓冲区(称作Cache),是临时存贮数据(使用频繁的数据)的地方。当用户查询数据,首先在缓存中寻找,如果找到了则直接执行,如果找不到则去数据库中查找。
缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。
缓存在企业级应用主要作用是信息处理,当需要读取数据时,由于受限于数据库的访问效率,导致整体系统性能偏低。
应用程序直接与数据库打交道,访问效率低
为了改善上述现象,开发者通常会在应用程序与数据库之间建立一种临时的数据存储机制,该区域中的数据在内存中保存,读写速度较快,可以有效解决数据库访问效率低下的问题。这一块临时存储数据的区域就是缓存。
使用缓存后,应用程序与缓存打交道,缓存与数据库打交道,数据访问效率提高。
1.2、缓存的优缺点
优点:
- 加快了响应速度
- 减少了对数据库的读操作,数据库的压力降低
缺点:
- 内存容量相对硬盘小
- 缓存中的数据可能与数据库中数据不一致
- 内存断电就会清空数据,造成数据丢失
1.3、为什么使用缓存
一般在远端服务器上,考虑到客户端请求量多,某些数据请求量大,这些热点数据要频繁的从数据库中读取,给数据库造成压力,导致响应客户端较慢。所以,在一些不考虑实时性的数据中,经常将这些数据存在内存中,当请求时候,能够直接读取内存中的数据及时响应。
缓存主要有解决高性能与高并发与减少数据库压力。缓存本质就是将数据存储在内存中,当数据没有发生本质变化的时候,应尽量避免直接连接数据库进行查询,因为并发高时很可能会将数据库压塌,而是应去缓存中读取数据,只有缓存中未查找到时再去数据库中查询,这样就大大降低了数据库的读写次数,增加系统的性能和能提供的并发量。
2、Redis 概念知识
2.1、Redis 简介
Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库。与传统数据库不同的是,Redis 的数据是存在内存中的,所以读写速度非常快,被广泛应用于缓存方向。Redis可以将数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。
2.2、为什么用 Redis 作为缓存
- 基于内存操作,内存读写速度快。
- 支持多种数据结构: Redis 不仅支持简单的 Key/Value 类型的数据,同时还提供 list、set、zset、hash
等数据结构的存储。 - 支持事务。Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
- 支持主从复制。主节点会自动将数据同步到从节点,可以进行读写分离。
- 支持持久化。Redis支持RDB和AOF两种持久化机制,持久化功能可以有效地避免数据丢失问题。
- Redis命令的处理是单线程的。Redis6.0引入了多线程,需要注意的是,多线程用于处理网络数据的读写和协议解析,Redis命令执行还是单线程的。
2.3、Redis 支持的数据类型
- 字符串(string)
- 哈希表(hash)
- 列表(list)
- 集合(set)
- 有序集合(zset)
为了保证读取效率,Redis 把数据对象存储在内存中,并支持周期性的把更新的数据写入磁盘文件中。而且它还提供了交集和并集,以及一些不同方式排序的操作。
redis的特性决定了它的功能,它可以用来做以下这些事情!
- 排行榜,利用zset可以方便的实现排序功能。
- 计数器,利用redis中原子性的自增操作,可以统计到阅读量,点赞量等功能。
- 简单消息队列,list存储结构,满足先进先出的原则,可以使用lpush/rpop或rpush/lpop实现简单消息队列。
- session共享,分布式系统中,可以利用redis实现session共享。
- 好友关系,利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好之类的功能。
2.3、Redis是如何判断数据是否过期
Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。
过期字典是存储在redisDb这个结构里的:
typedef struct redisDb {
...
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires// 过期字典,保存着键的过期时间
...
} redisDb;
2.4、过期的数据的删除策略
- 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key。
- 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key。
- 当前已用内存超过maxmemory限定时,触发主动清理策略。
2.5、Redis 事务
Redis事务是指将多条命令加入队列,一次批量执行多条命令,每条命令会按顺序执行,事务执行过程中不会受客户端传入的命令请求影响。
Redis事务和关系型数据库的事务不太一样,它不保证原子性,也没有隔离级别的概念。
- Redis事务没有隔离级别的概念
当事务开启时,事务期间的命令并没有执行,而是加入队列,只有执行EXEC命令时,事务中的命令才会按照顺序执行,从而事务间就不会导致数据脏读、不可重复读、幻读的问题,因此就没有隔离级别。
- Redis不保证原子性
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务的相关命令如下:
- MULTI:标识一个事务的开启,即开启事务。
- EXEC:执行事务中的所有命令,即提交。
- DISCARD:放弃事务;和回滚不一样,Redis事务不支持回滚。
- WATCH:监视Key改变,用于实现乐观锁。如果监视的Key的值改变,事务最终会执行失败。
- UNWATCH:放弃监视。
2.6、Redis 持久化机制
持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。
Redis支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。
RDB方式
- RDB,就是把内存数据以快照的形式保存到磁盘上。
RDB持久化,是指在的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。RDB触发机制主要有以下几种:
手动触发 | save | SAVE命令执行快照的过程会阻塞所有客户端的请求,应避免在生产环境使用此命令 |
手动触发 | bgsave | BGSAVE命令可以在后台异步进行快照操作,快照的同时服务器还可以继续响应客户端的请求,因此需要手动执行快照时推荐使用BGSAVE命令 |
自动触发 | save m n | 根据配置规则进行自动快照,如SAVE 100 10,100秒内至少有10个键被修改则进行快照 |
优点:
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式。
- 适合大规模的数据恢复场景,如备份,全量复制等。
缺点:
- 没办法做到实时持久化/秒级持久化,RDB方式数据无法做到实时持久化。因为BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本比较高。
- 新老版本存在RDB格式兼容问题,RDB 文件使用特定二进制格式保存,Redis 版本升级过程中有多个格式的 RDB 版本,存在老版本 Redis 无法兼容新版 RDB 格式的问题。
AOF方式
AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。
AOF的工作流程如下:
- 所有的写入命令会追加到 AOP 缓冲区中。
- AOF 缓冲区根据对应的策略向硬盘同步。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩文件体积的目的。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
- 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。
默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用:appendonly yes。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。
默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。
appendfsync always //每次写入aof文件都会执行同步,最安全最慢,不建议配置
appendfsync everysec //既保证性能也保证安全,建议配置
appendfsync no //由操作系统决定何时进行同步操作
优点:
- 数据的一致性和完整性更高,AOF可以更好的保护数据不丢失,可以配置 AOF 每秒执行一次fsync操作,如果Redis进程挂掉,最多丢失1秒的数据。
- AOF以append-only的模式写入,所以没有磁盘寻址的开销,写入性能非常高。
缺点:
- 对于同一份文件AOF文件比RDB数据快照要大。
- 数据恢复比较慢。
RDB和AOF如何选择
通常来说,应该同时使用两种持久化方案,以保证数据安全。
- 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。
- 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。
- 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。
- 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。
- 当RDB与AOF两种方式都开启时,Redis会优先使用AOF恢复数据,因为AOF保存的文件比RDB文件更完整。
2.7、如何保证缓存和数据库数据的⼀致性
- 先删除缓存再更新数据库
进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。
存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。
- 先更新数据库再删除缓存
进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。
存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。
- 异步更新缓存
数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。
2.8、Redis 缓存常见问题
2.8.1、缓存穿透
缓存穿透是指当用户查询某个数据时,Redis 中不存在该数据,也就是缓存没有命中,此时查询请求就会转向持久层数据库 MySQL,结果发现 MySQL 中也不存在该数据,MySQL 只能返回一个空象,代表此次查询失败。如果这种类请求非常多,或者用户利用这种请求进行恶意攻击,就会给 MySQL 数据库造成很大压力,甚至于崩溃,这种现象就叫缓存穿透。
为了避免缓存穿透问题,下面介绍两种解决方案:
1) 缓存空对象
当 MySQL 返回空对象时, Redis 将该对象缓存起来,同时为其设置一个过期时间。当用户再次发起相同请求时,就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层,从而保护了后端数库,但是这种做法也存在一些问题,虽然请求进不了 MSQL,但是这种策略会占用 Redis 的缓存空间。
2) 布隆过滤器
布隆过滤器判定不存在的数据,那么该数据一定不存在,利用它的这一特点可以防止缓存穿透。
首先将用户可能会访问的热点数据存储在布隆过滤器中(也称缓存预热),当有一个用户请求到来时会先经过布隆过滤器,如果请求的数据,布隆过滤器中不存在,那么该请求将直接被拒绝,否则将继续执行查询。相较于第一种方法,用布隆过滤器方法更为高效、实用。其流程示意图如下:
缓存预热:是指系统启动时,提前将相关的数据加载到 Redis 缓存系统中。这样避免了用户请求的时再去加载数据。
2.8.2、缓存击穿
缓存击穿是指用户查询的数据缓存中不存在,但是后端数据库却存在,这种现象出现原因是一般是由缓存中 key 过期导致的。比如一个热点数据 key,它无时无刻都在接受大量的并发访问,如果某一时刻这个 key 突然失效了,就致使大量的并发请求进入后端数据库,导致其压力瞬间增大。这种现象被称为缓存击穿。
缓存击穿有两种解决方法:
1) 改变过期时间
设置热点数据永不过期。
2) 分布式锁
采用分布式锁的方法,重新设计缓存的使用方式,过程如下:
- 上锁:当我们通过 key去查询数据时,首先查询缓存,如果没有,就通过分布式锁进行加锁,第一个获取锁的进程进入后端数据库查询,并将查询结果缓到Redis 中。
- 解锁:当其他进程发现锁被某个进程占用时,就进入等待状态,直至解锁后,其余进程再依次访问被缓存的 key。
2.8.3、缓存雪崩
缓存雪崩是指缓存中大批量的 key 同时过期,而此时数据访问量又非常大,从而导致后端数据库压力突然暴增,甚至会挂掉,这种现象被称为缓存雪崩。它和缓存击穿不同,缓存击穿是在并发量特别大时,某一个热点 key 突然过期,而缓存雪崩则是大量的 key 同时过期,因此它们根本不是一个量级。
解决方案
缓存雪崩和缓存击穿有相似之处,所以也可以采用热点数据永不过期的方法,来减少大批量的 key 同时过期。再者就是为 key 设置随机过期时间,避免 key 集中过期。
2.9、Redis有哪些部署方案
主从模式
主从模式的结构图如下:
Redis 主机会一直将自己的数据复制给 Redis 从机,从而实现主从同步。在这个过程中,只有 master 主机可执行写命令,其他 salve 从机只能只能执行读命令,这种读写分离的模式可以大大减轻 Redis 主机的数据读取压力,从而提高了Redis 的效率,并同时提供了多个数据备份。主从模式是搭建 Redis Cluster 集群最简单的一种方式。
哨兵模式
在 Redis 主从复制模式中,因为系统不具备自动恢复的功能,所以当主服务器(master)宕机后,需要手动把一台从服务器(slave)切换为主服务器。在这个过程中,不仅需要人为干预,而且还会造成一段时间内服务器处于不可用状态,同时数据安全性也得不到保障,因此主从模式的可用性较低,不适用于线上生产环境。
Redis 官方推荐一种高可用方案,也就是 Redis Sentinel 哨兵模式,它弥补了主从模式的不足。Sentinel 通过监控的方式获取主机的工作状态是否正常,当主机发生故障时, Sentinel 会自动进行 Failover(即故障转移),并将其监控的从机提升主服务器(master),从而保证了系统的高可用性。
哨兵模式是一种特殊的模式,Redis 为其提供了专属的哨兵命令,它是一个独立的进程,能够独立运行。下面使用 Sentinel 搭建 Redis 集群,基本结构图如下所示:
在上图过程中,哨兵主要有两个重要作用:
- 第一:哨兵节点会以每秒一次的频率对每个 Redis 节点发送PING命令,并通过 Redis 节点的回复来判断其运行状态。
- 第二:当哨兵监测到主服务器发生故障时,会自动在从节点中选择一台将机器,并其提升为主服务器,然后使用 PubSub 发布订阅模式,通知其他的从节点,修改配置文件,跟随新的主服务器。
在实际生产情况中,Redis Sentinel 是集群的高可用的保障,为避免 Sentinel 发生意外,它一般是由 3~5 个节点组成,这样就算挂了个别节点,该集群仍然可以正常运转。其结构图如下所示(多哨兵模式):
上图所示,多个哨兵之间也存在互相监控,这就形成了多哨兵模式,现在对该模式的工作过程进行讲解,介绍如下:
1) 主观下线
主观下线,适用于主服务器和从服务器。如果在规定的时间内(配置参数:down-after-milliseconds),Sentinel 节点没有收到目标服务器的有效回复,则判定该服务器为“主观下线”。比如 Sentinel1 向主服务发送了PING命令,在规定时间内没收到主服务器PONG回复,则 Sentinel1 判定主服务器为“主观下线”。
2) 客观下线
客观下线,只适用于主服务器。 Sentinel1 发现主服务器出现了故障,它会通过相应的命令,询问其它 Sentinel 节点对主服务器的状态判断。如果超过半数以上的 Sentinel 节点认为主服务器 down 掉,则 Sentinel1 节点判定主服务为“客观下线”。
3) 投票选举
投票选举,所有 Sentinel 节点会通过投票机制,按照谁发现谁去处理的原则,选举 Sentinel1 为领头节点去做 Failover(故障转移)操作。Sentinel1 节点则按照一定的规则在所有从节点中选择一个最优的作为主服务器,然后通过发布订功能通知其余的从节点(slave)更改配置文件,跟随新上任的主服务器(master)。至此就完成了主从切换的操作。
对上对述过程做简单总结:
Sentinel 负责监控主从节点的“健康”状态。当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接 Redis 集群时,会首先连接 Sentinel,通过 Sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 Sentinel 要地址,Sentinel 会将最新的主节点地址告诉客户端。因此应用程序无需重启即可自动完成主从节点切换。
3、SpringBoot 整合 Redis 缓存
3.1、使用 Redis 缓存
① 在 pom.xml 文件中引入 Redis 依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
② 修改项目启动类
增加注解@EnableCaching,开启缓存功能,如下:
package com.example.canal;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
@MapperScan(basePackages = "com.example.canal.mybatis.mapper")
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
③ 配置redis数据库
在application.properties中配置Redis连接信息,如下:
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=139926
# 连接超时时间(毫秒)
spring.redis.timeout=1000
# 连接池最大连接数(使用负值表示没有限制)
lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接
lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
lettuce.pool.min-idle=0
④ 创建 Redis 配置类
我们除了在application.yaml中加入redis的基本配置外,一般还需要配置redis key和value的序列化方式,如下:
package com.example.canal.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import javax.crypto.KeyGenerator;
import java.lang.reflect.Method;
import java.time.Duration;
/**
* Redis 配置类
*/
@Configuration
@EnableCaching // 开启缓存配置
public class RedisConfig {
/**
* 设置RedisTemplate规则
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// key采用String的序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 支持事物
// template.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 设置CacheManager缓存规则
*
* @param factory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 生成两套默认配置,通过 Config 对象即可对缓存进行自定义配置
// 配置序列化(解决乱码的问题),过期时间10分钟
RedisCacheConfiguration cacheConfig1 = RedisCacheConfiguration.defaultCacheConfig()
// 设置过期时间 10 分钟
.entryTtl(Duration.ofMinutes(10))
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
// 设置 value 序列化
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
// 配置序列化(解决乱码的问题),过期时间30秒
RedisCacheConfiguration cacheConfig2 = RedisCacheConfiguration.defaultCacheConfig()
// 设置过期时间 30 秒
.entryTtl(Duration.ofSeconds(30))
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
// 设置 value 序列化
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
// 返回 Redis 缓存管理器
return RedisCacheManager.builder(factory).withCacheConfiguration("user", cacheConfig1)
.withCacheConfiguration("admin", cacheConfig2).build();
}
}
⑤ 操作 Redis
SpringBoot提供了两个bean来操作redis,分别是 RedisTemplate
和 StringRedisTemplate
,这两者的主要区别如下:
- StringRedisTemplate 继承了 RedisTemplate。
- RedisTemplate 是一个泛型类,而 StringRedisTemplate 则不是。
- StringRedisTemplate 只能对 key=String,value=String 的键值对进行操作,RedisTemplate 可以对任何类型的 key-value 键值对操作。
- 他们各自序列化的方式不同,但最终都是得到了一个字节数组,殊途同归,StringRedisTemplate 使用的是 StringRedisSerializer 类;RedisTemplate 使用的是 JdkSerializationRedisSerializer 类。反序列化,则是一个得到 String,一个得到 Object。
- 两者的数据是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的数据,RedisTemplate 只能管理 RedisTemplate中 的数据。
示例如下:
@RestController
public class UserController {
@Autowired
private UserService userServer;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 查询所有课程
*/
@RequestMapping("/allCourses")
public String findAll() {
List<Courses> courses = userServer.findAll();
// 将查询结果写入redis缓存
stringRedisTemplate.opsForValue().set("courses", String.valueOf(courses));
// 读取redis缓存
System.out.println(stringRedisTemplate.opsForValue().get("courses"));
return "ok";
}
}
3.2、使用 SpringCache 的注解
3.2.1、注解说明
- @CacheConfig(cacheNames = “user”)
一般配置在类上,指定缓存名称,这个名称是和上面“置缓存管理器”中缓存名称的一致。
- @Cacheable
该注解标注的方法每次被调用前都会触发缓存校验,校验指定参数的缓存是否已存在,若存在,直接返回缓存结果,否则执行方法内容,最后将方法执行结果保存到缓存中。
该注解常用参数如下:
-
cacheNames/value :存储方法调用结果的缓存的名称
-
key :缓存数据使用的key,可以用它来指定,
key="#param"可以指定参数值
,也可以是其他属性 -
keyGenerator :key的生成器,用来自定义key的生成,
与key为二选一
,不能兼存 -
condition:用于使方法缓存有条件,默认为"" ,表示方法结果始终被缓存。conditon="#id>1000"表示id>1000的数据才进行缓存
-
unless:用于否决方法缓存,此表达式在方法被调用后计算,因此可以引用方法返回值(result),默认为"" ,这意味着缓存永远不会被否决。unless = "#result==null"表示除非该方法返回值为null,否则将方法返回值进行缓存
-
sync :是否使用异步模式,默认为false不使用异步
- @CachePut
如果缓存中先前存在目标值,则更新缓存中的值为该方法的返回值;如果不存在,则将方法的返回值存入缓存。
该注解常用参数同@Cacheable,不过@CachePut没有sync 这个参数。
- @CacheEvict
如果缓存中存在存在目标值,则将其从缓存中删除。
该注解常用参数如下:
- cacheNames/value、key、keyGenerator、condition同@Cacheable
- allEntries:如果指定allEntries为true,Spring Cache将忽略指定的key清除缓存中的所有元素,默认情况下为false。
- beforeInvocation:删除缓存操作默认是在对应方法成功执行之后触发的,方法如果因为抛出异常而未能成功返回时也不会触发删除操作。如果指定beforeInvocation为true ,则无论方法结果如何,无论方法是否抛出异常都会导致删除缓存。
- @Caching
用于一次性设置多个缓存。
3.2.2、常用注解配置参数
- value:缓存管理器中配置的缓存的名称,这里可以理解为一个组的概念,缓存管理器中可以有多套缓存配置,每套都有一个名称,类似于组名,这个可以配置这个值,选择使用哪个缓存的名称,配置后就会应用那个缓存名称对应的配置。
- key: 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。
- condition: 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存。
- unless: 不缓存的条件,和 condition 一样,也是 SpEL 编写,返回 true 或者 false,为 true 时则不进行缓存。
3.2.3、自动缓存案例
package com.example.canal.mybatis.service;
import com.example.canal.mybatis.entity.User;
import com.example.canal.mybatis.mapper.UserMapper;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserServiceImpl implements IUserService {
@Resource
private UserMapper userMapper;
@Override
@Cacheable(cacheNames = "user", key = "'yang'")
public List<User> allUsers() {
return userMapper.allUsers();
}
@Override
// 如果缓存中先前存在,则更新缓存;如果不存在,则将方法的返回值存入缓存
@CachePut(cacheNames = "user", key = "#user.userId")
public User updateUser(User user) {
userMapper.updateUser(user);
return user;
}
@Override
// 先执行方法体中的代码,成功执行之后删除缓存
@CacheEvict(cacheNames = "user", key = "#userId")
public int delUser(int userId) {
return userMapper.delUser(userId);
}
@Cacheable(cacheNames = "user", key = "#userId", unless = "#result == null")
public User queryUser(int userId) {
return userMapper.queryUser(userId);
}
}