重学SpringBoot系列之redis缓存
- 使用docker安装redis
- redis数据结构与应用场景
- 单例哨兵及集群模式整合
- 使用redisTemplate操作数据
- 使用Redis Repository操作数据
- spring cache缓存基本用法
- 详述缓存声明式注解的使用
- 缓存注解配置说明
- 缓存key的书写及取值
使用docker安装redis
本节的目的不在于去教大家理解docker容器(讲docker就脱离了我们课程的核心,我们的课程是Spring Boot 不是docker),而是希望通过docker的方式快速的为大家搭建一个redis数据库,从而方便大家学习使用。
准备工作
- 首先要安装好docker。CentOS7如何安装docker可以自行百度
获取 redis 镜像
docker search redis
docker pull redis:5.0.5
docker images
其实更形象点的理解docker镜像和容器之间的关系,更像是Class类与对象之间的关系。一个类可以构造多个对象,一个镜像可以构造多个容器。类和镜像是实实在在存在的字节码文件;对象和容器是在系统内存里面,作为运行时状态存在。
创建容器
创建持久化存储目录
容器可以运行在内存里面,但是容器存储的数据需要进行持久化。所以在宿主机上创建redis 容器的数据和配置文件存储目录。
# 这里我们在 /home/docker 下创建
mkdir /home/docker/redis/{conf,data} -p
cd /home/docker/redis
注意:后面所有的操作命令都要在这个目录/home/docker/redis
下进行
获取 redis 的默认配置文件模版
# 获取 redis 的默认配置模版
# 这里主要是想设置下 redis 的 log / password / appendonly
# redis 的 docker 运行参数提供了 --appendonly yes 但没 password
wget https://gitee.com/hanxt/boot-launch/raw/master/src/main/resources/otherconfig/redis.conf -O conf/redis.conf
# 直接替换编辑
sed -i 's/logfile ""/logfile "access.log"/' conf/redis.conf;
sed -i 's/# requirepass foobared/requirepass 123456/' conf/redis.conf;
sed -i 's/appendonly no/appendonly yes/' conf/redis.conf;
sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' conf/redis.conf;
- sed -i是linux文件替换命令,替换格式为s/被替换的内容/替换之后的内容/
-
替换
logfile ""
为logfile "access.log"
,指定日志文件名称为access.log
---->指定日志文件的名称 -
替换
# requirepass foobared
为requirepass 123456
,指定访问密码为123456
—>配置登录密码,auth 123456 -
替换
“appendonly no“
为”appendonly yes”
,开启appendonly
模式–》持久化配置 -
替换绑定**
IP“bind 127.0.0.1”为“bind 0.0.0.0”
**—>任意ip可以访问
protected-mode 是在没有显式定义 bind 地址(即监听全网段),又没有设置密码 requirepass时,protected-mode 只允许本地回环 127.0.0.1 访问。所以改为bind 0.0.0.0
使用镜像创建一个容器
创建并运行一个名为 myredis 的容器,放到start-redis.sh脚本里面
# 创建并运行一个名为 myredis 的容器
docker run \
-p 6379:6379 \
-v $PWD/data:/data \
-v $PWD/conf/redis.conf:/etc/redis/redis.conf \
--privileged=true \
--name myredis \
-d redis:5.0.5 redis-server /etc/redis/redis.conf
# 命令分解
docker run \
-p 6379:6379 \ # 端口映射 宿主机:容器
-v $PWD/data:/data:rw \ # 映射磁盘目录 rw 为读写,宿主机目录:容器目录
-v $PWD/conf/redis.conf:/etc/redis/redis.conf:ro \ # 挂载配置文件 ro 为readonly
--privileged=true \ # 给与一些权限
--name myredis \ # 给容器起个名字
-d redis redis-server /etc/redis/redis.conf # deamon 运行容器 并使用配置文件启动容器内的 redis-server
-
$PWD
是当前目录,也就是/home/docker/redis
查看活跃的容器
# 查看活跃的容器
docker ps
# 如果没有 myredis 说明启动失败 查看错误日志
docker logs myredis
# 查看 myredis 的 ip 挂载 端口映射等信息
docker inspect myredis
# 查看 myredis 的端口映射
docker port myredis
访问 redis 容器服务
安装好之后,可以进行访问测试
docker exec -it myredis bash
redis-cli
上面的测试是在宿主机内访问docker容器。如果在宿主机上可以访问到redis服务,在宿主机之外的主机无法访问该redis服务的话,可能是因为宿主机的防火墙没有打开。参考下面的做法。
开启防火墙端口,提供外部访问
开启docker容器所在的宿主机端口,提供给外部服务进行访问
firewall-cmd --zone=public --add-port=6379/tcp --permanent
firewall-cmd --reload
firewall-cmd --query-port=6379/tcp
redis数据结构与应用场景
Redis 是开源免费, key-value 内存数据库,主要解决高并发、大数据场景下,热点数据访问的性能问题,提供高性能的数据快速访问。
项目中部分数据访问比较频繁,对下游 DB(例如 MySQL)造成服务压力,这时候可以使用缓存来提高效率。
Redis 的主要特点包括:
- Redis数据存储在内存中,可以提高热点数据的访问效率
- Redis 除了支持 key-value 类型的数据,同时还支持其他多种数据结构的存储;
- Redis 支持数据持久化存储,可以将数据存储在磁盘中,机器重启数据将从磁盘重新加载数据;
Redis 作为缓存数据库和 MySQL 这种结构化数据库进行对比。
- 从数据库类型上,Redis 是 NoSQL 半结构化缓存数据库, MySQL 是结构化关系型数据库;
- 从读写性能上,MySQL 是持久化硬盘存储,读写速度较慢, Redis数据存储读取都在内存,同时也可以持久化到磁盘,读写速度较快;
- 从使用场景上,Redis 一般作为 MySQL 数据读取性能优化的技术选型,彼此配合使用。Redis用于存储热数据或者缓存数据,并不存在相互替换的关系。
Redis 基本数据结构与实战场景
-
redis的数据结构可以理解为Java数据类型中的
Map<String,Object>
,key是String类型,value是下面的类型。只不过作为一个独立的数据库单独存在,所以Java中的Map怎么用,redis就怎么用,大同小异。 -
字符串类型的数据结构可以理解为
Map<String,String>
-
list类型的数据结构可以理解为
Map<String,List<String>>
-
set类型的数据结构可以理解为
Map<String,Set<String>>
-
hash类型的数据结构可以理解为
Map<String,HashMap<String,String>>
上图中命令行更正:lrange,不是lrang
redis应用场景解析
String 类型使用场景
场景一:商品库存数
从业务上,商品库存数据是热点数据,交易行为会直接影响库存。而 Redis 自身 String 类型提供了:
incr key #增加一个库存
decr key # 减少一个库存
incrby key 10 # 增加20个库存
decrby key 15 # 减少15个库存
- set goods_id 10; 设置 id 为 good_id 的商品的库存初始值为 10;
- decr goods_id; 当商品被购买时候,库存数据减 1。
依此类推的场景:商品的浏览次数,问题或者回复的点赞次数等。这种计数的场景都可以考虑利用 Redis 来实现。
场景二:时效信息存储
Redis 的数据存储具有自动失效能力。也就是存储的 key-value 可以设置过期时间
,SETEX mykey 60 "value"
中的第2个参数就是过期时间。
比如,用户登录某个 App 需要获取登录验证码, 验证码在 30 秒内有效。
- 生成验证码:生成验证码并使用 String 类型在reids存储验证码,同时设置 30 秒的失效时间。如:SETEX validcode 30 “value”
- 验证过程:用户获得验证码之后,我们通过get validcode获取验证码,如果获取不到说明验证码过期了。
List 类型使用场景
list 是按照插入顺序排序的字符串链表。可以在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为 O(1)) 。
场景一:消息队列实现
目前有很多专业的消息队列组件 Kafka、RabbitMQ 等。 我们在这里仅仅是使用 list 的特征来实现消息队列的要求。在实际技术选型的过程中,大家可以慎重思考。
list 存储就是一个队列的存储形式:
- lpush key value; 在 key 对应 list 的头部添加字符串元素;
- rpop key; 移除列表的最后一个元素,返回值为移除的元素。
场景二:最新上架商品
在交易网站首页经常会有新上架产品推荐的模块, 这个模块是存储了最新上架前 100 名。这时候使用 Redis 的 list 数据结构,来进行 TOP 100 新上架产品的存储。
Redis ltrim 指令对一个列表进行修剪(trim),这样 list 就会只包含指定范围的指定元素。
ltrim key start end
start 和 end 都是由 0 开始计数的,这里的 0 是列表里的第一个元素(表头),1 是第二个元素。
如下伪代码演示:
//把新上架商品添加到链表里
ret = r.lpush("new:goods", goodsId)
//保持链表 100 位
ret = r.ltrim("new:goods", 0, 99)
//获得前 100 个最新上架的商品 id 列表
newest_goods_list = r.lrange("new:goods", 0, 99)
set 类型使用场景
set 也是存储了一个集合列表功能。和 list 不同,set 具备去重功能
(和Java的Set数据类型一样)。当需要存储一个列表信息,同时要求列表内的元素不能有重复,这时候使用 set 比较合适。与此同时,set 还提供的交集、并集、差集。
例如,在交易网站,我们会存储用户感兴趣的商品信息,在进行相似用户分析的时候, 可以通过计算两个不同用户之间感兴趣商品的数量来提供一些依据。
//userid 为用户 ID , goodID 为感兴趣的商品信息。
sadd "user:userId" goodID
sadd "user:101" 1
sadd "user:101" 2
sadd "user:102" 1
Sadd "user:102" 3
sinter "user:101" "user:102" # 返回值是1
获取到两个用户相似的产品, 然后确定相似产品的类目就可以进行用户分析。类似的应用场景还有, 社交场景下共同关注好友, 相似兴趣 tag 等场景的支持。
Hash 类型使用场景
Redis 在存储对象(例如:用户信息)的时候需要对对象进行序列化转换然后存储,还有一种形式,就是将对象数据转换为 JSON 结构数据,然后存储 JSON 的字符串到 Redis。
对于一些对象类型,还有另外一种比较方便的类型,那就是按照 Redis 的 Hash 类型进行存储。
hset key field value
例如,我们存储一些网站用户的基本信息, 我们可以使用:
hset user101 name "小明"
hset user101 phone "123456"
hset user101 sex "男"
这样就存储了一个用户基本信息,存储信息有:{name : 小明, phone : “123456”,sex : “男”}
当然这种类似场景还非常多, 比如存储订单的数据,产品的数据,商家基本信息等。大家可以参考来进行存储选型。但是不适合存储关联关系比较复杂的数据,那种场景还得用关系型数据库比较方便。
Sorted Set 类型使用场景
Redis sorted set 的使用场景与 set 类似,区别是 set 不是自动有序的,而 sorted set 可以通过提供一个 score 参数来为存储数据排序
,并且是自动排序,插入既有序。
业务中如果需要一个有序且不重复的集合列表,就可以选择 sorted set 这种数据结构。
比如:商品的购买热度可以将购买总量 num 当做商品列表的 score,这样获取最热门的商品时就是可以自动按售卖总量排好序。
单例哨兵及集群模式整合
redis集群模式和哨兵模式高可用的安装与运维,需要你去专门的redis课程里面去学习。我们的主要是面向Spring Boot开发人员,不讲redis集群高可用及运维知识。
也就是说,本节为大家介绍的内容是:当架构师或者运维人员将redis 哨兵或cluster集群搭建好之后,在Spring Boot应用中你该如何去连接及使用这些redis实例。
spring-data-redis简介
Spring Boot 提供了对 Redis 集成的组件包:spring-boot-starter-data-redis
,它依赖于 spring-data-redis
和 lettuce
。Spring Boot 1.0 默认使用的是 Jedis 客户端,2.0 替换成了 Lettuce,但如果你从 Spring Boot 1.5.X 切换过来,几乎感受不到差异,这是因为 spring-boot-starter-data-redis
为我们隔离了其中的差异性。
-
Lettuce:是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection,它利用优秀
Netty NIO 框架来高效地管理多个连接。 -
Spring Data:是 Spring 框架中的一个主要项目,目的是为了简化构建基于 Spring
框架应用的数据访问,包括非关系数据库、Map-Reduce 框架、云数据服务等,另外也包含对关系数据库的访问支持。 -
Spring Data Redis:是 Spring Data 项目中的一个主要模块,实现了对 Redis 客户端 API
的高度封装,使对 Redis 的操作更加便捷。
整合spring data 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>
引入 commons-pool 2 是因为 Lettuce 需要使用 commons-pool 2 创建 Redis 连接池。
redis单例模式连接配置
application全局配置,使用我们前面安装好的测试redis服务。redis的单节点实例,可以通过下面的配置连接redis单节点实例数据库
spring:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: 192.168.161.3 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: 123456 # Redis 服务器连接密码(默认为空)
timeout: 5000 # 连接超时,单位ms
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-idle: 8 # 连接池中的最大空闲连接 默认 8
min-idle: 0 # 连接池中的最小空闲连接 默认 0
redis哨兵模式连接配置
redis另外一种非常常用的部署模式是哨兵模式,如果你的公司使用的是这种部署模式,它相对于单实例模式更加的高可用。
- redis哨兵模式实际上是两种模式的组合,即主从模式和哨兵模式。当Master节点离线后,哨兵sentinel监控节点会把Slave节点切换为Master节点,保证服务的高可用
- 哨兵模式是在主从模式的基础上增加了哨兵sentinel监控节点。最简单的哨兵模式需要一个redis的Master节点、一个redis的Slave、另外三个哨兵监控节点。
需要注意的是,当我们使用spring boot连接哨兵模式的redis集群,连接的是sentinel节点,而不是redis服务实例节点。注意上图的连接顺序。 Application Client是我们的应用程序,sentinel node是哨兵节点。
spring:
redis:
password: 123456
timeout: 5000
sentinel: # 哨兵模式连接配置
master: mymaster #master节点名称,redis sentinel模式安装的时候会配置
nodes: 192.168.1.201:26379,192.168.1.202:26379,192.168.1.203:26379 # 哨兵的IP:Port列表
lettuce
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
主从模式和哨兵模式参考文章推荐
redis集群模式连接配置
Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。分布式集群首要解决问题是:把整个数据集按照分区规则映射到多个节点上,即把数据集按照一定的规则划分到多个节点上,每个节点只保存整个数据集的一个子集。
之前我们为大家介绍的redis安装模式,无论是单节点还是master-slave,其redis服务都保存了数据集的完整副本。cluster模式不是,其redis实例节点只包含完整数据集的子集。
- 当程序客户端随意访问一个redis node节点时,可能会发现其操作的数据或者应该写入的数据位置,并不在当前node节点上。
- 此时,当前被访问的redis node节点会告知客户端,你应该去哪个节点访问数据或写入数据
- 然后客户端获取目标node节点的地址,重定向到该节点的地址,去访问或写入数据。
下面的配置,是针对redis集群模式连接访问的配置。
spring:
redis:
password: 123456
timeout: 5000
database: 0
cluster: #集群模式配置
nodes: 192.168.1.11:6379,192.168.1.12:6379,192.168.1.13:6379,192.168.1.14:6379,192.168.1.15:6379,192.168.1.16:6379
max-redirects: 3 # 重定向的最大次数
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
cluster集群模式参考文章推荐
使用redisTemplate操作数据
redis模板封装类
RedisTemplate 的封装使我们能够更方便的进行redis数据操作,比直接使用Jedis或者Lettuce的java SDK要方便很多。RedisTemplate作为java 操作redis数据库的API模板更通用,可以操作所有的redis数据类型。
// 注入RedisTemplate,更通用
@Resource
private RedisTemplate<String, Object> redisTemplate;
ValueOperations<String,Object> ValueOperations = redisTemplate.opsForValue();//操作字符串
HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();//操作 hash
ListOperations<String, Object> listOperations = redisTemplate.opsForList();//操作 list
SetOperations<String, Object> setOperations = redisTemplate.opsForSet();//操作 set
ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();//操作有序 set
ListOperations、ValueOperations、HashOperations、SetOperations、ZSetOperations等都是针对专有数据类型进行操作,使用起来更简洁。
@Resource(name = "redisTemplate")
private ValueOperations<String,Object> valueOperations; //以redis string类型存取Java Object(序列化反序列化)
@Resource(name = "redisTemplate")
private HashOperations<String, String, Object> hashOperations; //以redis的hash类型存储java Object
@Resource(name = "redisTemplate")
private ListOperations<String, Object> listOperations; //以redis的list类型存储java Object
@Resource(name = "redisTemplate")
private SetOperations<String, Object> setOperations; //以redis的set类型存储java Object
@Resource(name = "redisTemplate")
private ZSetOperations<String, Object> zSetOperations; //以redis的zset类型存储java Object
基础数据Java类
为了方便后面写代码解释API的使用方法,写测试用例。我们需要先准备数据对象Person,注意要实现Serializable接口,为什么一定要实现这个接口?我们下文解释。
@Data
public class Person implements Serializable {
private static final long serialVersionUID = -8985545025228238754L;
String id;
String firstname;
String lastname;
Address address; //注意这里,不是基础数据类型
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
准备数据对象Address
@Data
public class Address implements Serializable {
private static final long serialVersionUID = -8985545025228238771L;
String city;
String country;
public Address(String city, String country) {
this.city = city;
this.country = country;
}
}
StringRedisTemplate
除了RedisTemplate模板类,还有另一个模板类叫做StringRedisTemplate 。二者都提供了用来操作redis数据库的API。
@SpringBootTest
public class RedisConfigTest {
@Resource
private StringRedisTemplate stringRedisTemplate; //以String序列化方式保存数据的通用模板类
@Resource
private RedisTemplate<String, Person> redisTemplate; //默认以JDK二进制方式保存数据的通用模板类
@Test
public void stringRedisTemplate() {
Person person = new Person("kobe","byrant");
person.setAddress(new Address("洛杉矶","美国"));
//将数据存入redis数据库
stringRedisTemplate.opsForValue().set("player:srt","kobe byrant",20, TimeUnit.SECONDS);
redisTemplate.opsForValue().set("player:rt",person,20, TimeUnit.SECONDS);
}
}
二者的区别在于
-
操作的数据类型不同,以
List
类型为例:RedisTemplate
操作List< Object >
,StringRedisTemplate
操作List< String >
-
序列化数据的方式不同,
RedisTemplate
使用的是JdkSerializationRedisSerializer
存入数据会将数据先序列化成字节数组然后在存入Redis
数据库。StringRedisTemplate
使用的是StringRedisSerializer
回答上文中的问题,redis持久化的java数据类为什么要实现Serializable接口?因为RedisTemplate默认使用的是JdkSerializationRedisSerializer,也就是使用Java JDK默认的序列化方式存储数据。如果不实现Serializable接口,JDK序列化就会报错,这是java基础知识。如果我们可以不使用JDK默认的序列化方式,就不需要实现这个Serializable接口。
需要注意的是因为
RedisTemplate
和StringRedisTemplate
的默认序列化存储方式
不一样,所以二者存储的数据并不能通用。也就是说RedisTemplate存的数据只能用RedisTemplate去取,对于StringRedisTemplate也是一样。
解决key-value乱码问题
其实这个不是严格意义上的乱码,是JDK的二进制序列化之后的存储方式。人看不懂,但是程序是能看懂的。
那有没有人一种人能看懂,程序也能看懂的序列化结果?看下文的配置类代码
-
采用
StringRedisSerializer
对key
进行序列化(字符串格式) -
采用
Jackson2JsonRedisSerializer
对value
将进行序列化(JSON格式)
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//重点在这四行代码
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
乱码问题的症结在于对象的序列化问题:RedisTemplate
默认使用的是JdkSerializationRedisSerializer
(二进制存储),StringRedisTemplate
默认使用的是StringRedisSerializer
(redis字符串格式存储)。
序列化方式对比:
-
JdkSerializationRedisSerializer
: 使用JDK
提供的序列化功能。 优点是反序列化时不需要提供类型信息(class
),但缺点是需要实现Serializable
接口,还有序列化后的结果非常庞大,是JSON
格式的5倍左右,这样就会消耗redis
服务器的大量内存。而且是以二进制形式保存,自然人无法理解。 -
Jackson2JsonRedisSerializer
: 使用Jackson
库将对象序列化为JSON
字符串。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable
接口。似乎没啥缺点。 -
StringRedisSerializer
序列化之后的结果,自然人也是可以理解,但是value
只能是String
类型,不能是Object
。
使用redisTemplate存取redis各种数据类型
下面的各种数据类型操作的api和redis命令行api的含义几乎是一致的。
@SpringBootTest
public class RedisConfigTest2 {
@Resource(name = "redisTemplate")
private ValueOperations<String,Object> valueOperations; //以redis string类型存取Java Object(序列化反序列化)
@Resource(name = "redisTemplate")
private HashOperations<String, String, Object> hashOperations; //以redis的hash类型存储java Object
@Resource(name = "redisTemplate")
private ListOperations<String, Object> listOperations; //以redis的list类型存储java Object
@Resource(name = "redisTemplate")
private SetOperations<String, Object> setOperations; //以redis的set类型存储java Object
@Resource(name = "redisTemplate")
private ZSetOperations<String, Object> zSetOperations; //以redis的zset类型存储java Object
@Test
public void testValueObj() {
Person person = new Person("boke","byrant");
person.setAddress(new Address("南京","中国"));
//向redis数据库保存数据(key,value),数据有效期20秒
valueOperations.set("player:1",person,20, TimeUnit.SECONDS); //20秒之后数据消失
//根据key把数据取出来
Person getBack = (Person)valueOperations.get("player:1");
System.out.println(getBack);
}
@Test
public void testSetOperation() {
Person person = new Person("kobe","byrant");
Person person2 = new Person("curry","stephen");
setOperations.add("playerset",person,person2); //向Set中添加数据项
//members获取Redis Set中的所有记录
Set<Object> result = setOperations.members("playerset");
System.out.println(result); //包含kobe和curry的数组
}
@Test
public void HashOperations() {
Person person = new Person("kobe","byrant");
//使用hash的方法存储对象数据(一个属性一个属性的存,下节教大家简单的方法)
hashOperations.put("hash:player","firstname",person.getFirstname());
hashOperations.put("hash:player","lastname",person.getLastname());
hashOperations.put("hash:player","address",person.getAddress());
//取出一个对象的属性值,有没有办法一次将整个对象取出来?有,下节介绍
String firstName = (String)hashOperations.get("hash:player","firstname");
System.out.println(firstName); //kobe
}
@Test
public void ListOperations() {
//将数据对象放入队列
listOperations.leftPush("list:player",new Person("kobe","byrant"));
listOperations.leftPush("list:player",new Person("Jordan","Mikel"));
listOperations.leftPush("list:player",new Person("curry","stephen"));
//从左侧存,再从左侧取,所以取出来的数据是后放入的curry
Person person = (Person) listOperations.leftPop("list:player");
System.out.println(person); //curry对象
}
}
redisTemplate详细用法参考文章
RedisTemplate操作Redis,这一篇文章就够了(一)
使用Redis Repository操作数据
通过集成spring-boot-starter-data-redis
之后一共有三种redis hash
数据操作方式可以供我们选择
- 一个属性、一个属性的存取
-
使用
Jackson2HashMapper
存取对象 -
使用
RedisRepository
的对象操作
一个属性、一个属性的存取
@Test
public void HashOperations() {
Person person = new Person("kobe","byrant");
person.setAddress(new Address("洛杉矶","美国"));
//使用hash的方法存储对象数据(一个属性一个属性的存,下节教大家简单的方法)
hashOperations.put("hash:player","firstname",person.getFirstname());
hashOperations.put("hash:player","lastname",person.getLastname());
hashOperations.put("hash:player","address",person.getAddress());
String firstName = (String)hashOperations.get("hash:player","firstname");
System.out.println(firstName);
}
数据在redis数据库里面存储结构是下面这样的
- 一个hash代表一个对象的数据
- 一个对象有多个属性key、value键值对数据,每一组键值对都可以单独存取
使用Jackson2HashMapper存取对象
上一小节我们操作hash对象的时候是一个属性一个属性设置的,那我们有没有办法将对象一次性hash入库呢?
我们可以使用jacksonHashOperations
和Jackson2HashMapper
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
public class RedisConfigTest3 {
@Resource(name="redisTemplate")
private HashOperations<String, String, Object> jacksonHashOperations;
//注意这里的false,下文会讲解
private HashMapper<Object, String, Object> jackson2HashMapper = new Jackson2HashMapper(false);
@Test
public void testHashPutAll(){
Person person = new Person("kobe","bryant");
person.setId("kobe");
person.setAddress(new Address("洛杉矶","美国"));
//将对象以hash的形式放入redis数据库
Map<String,Object> mappedHash = jackson2HashMapper.toHash(person);
jacksonHashOperations.putAll("player:" + person.getId(), mappedHash);
//将对象从数据库取出来
Map<String,Object> loadedHash = jacksonHashOperations.entries("player:" + person.getId());
Object map = jackson2HashMapper.fromHash(loadedHash);
Person getback = new ObjectMapper().convertValue(map,Person.class);
//Junit5,验证放进去的和取出来的数据一致
assertEquals(person.getFirstname(),getback.getFirstname());
}
}
使用这种方式可以一次性的存取java 对象为redis数据库的hash数据类型。需要注意的是:执行上面的测试用例,Person和Address一定要有public无参构造方法,在将map转换成Person或Address对象的时候用到,如果没有的话会报错。
-
当
new Jackson2HashMapper(false)
,注意属性对象Address
的存储格式(两张图对比观察)
-
当
new Jackson2HashMapper(true),
注意属性对象Address的存储格式(两张图对比观察)
需要注意的是:使用这种方法存储hash数据,需要多出一个键值对@class
说明该hash
数据对应的java
类。在反序列化的时候会使用到,用于将hash
数据转换成java
对象。
使用RedisRepository的对象操作
下面为大家介绍使用RedisRepository进行redis数据操作,它不只是能简单的存取数据,还可以做很多的CURD操作。使用起来和我们用JPA进行关系型数据库的单表操作,几乎是一样的。
首先,我们需要在需要操作的java
实体类上面加上@RedisHash
注解,并使用@Id
为该实体类指定id
。是不是和JPA
挺像的?
@RedisHash("people") //注意这里的person,下文会说明
public class Person {
@Id
String id;
//其他和上一节代码一样
}
然后写一个PersonRepository ,继承CrudRepository,是不是也和JPA差不多?
//泛型第二个参数是id的数据类型
public interface PersonRepository extends CrudRepository<Person, String> {
// 继承CrudRepository,获取基本的CRUD操作
}
CrudRepository默认为我们提供了下面的这么多方法,我们直接调用就可以了。
在项目入口方法上加上注解@EnableRedisRepositories
(笔者测试,在比较新的版本中这个注解已经不需要添加了,默认支持),然后进行
下面的测试
@SpringBootTest
public class RedisRepositoryTest {
@Resource
PersonRepository personRepository;
@Test
public void test(){
Person rand = new Person("zimug", "汉神");
rand.setAddress(new Address("杭州", "中国"));
personRepository.save(rand); //存
Optional<Person> op = personRepository.findById(rand.getId()); //取
Person p2 = op.get();
personRepository.count(); //统计Person的数量
personRepository.delete(rand); //删除person对象rand
}
}
测试结果:需要注意的是RedisRepository在存取对象数据的时候,实际上使用了redis的2种数据类型
- 第一种是Set类型,用于保存每一个存入redis的对象(Person)的id。我们可以利用这个Set实现person对象集合类的操作,比如说:count()统计,统计redis数据库中一共保存了多少个person。注意:下图中set集合的key名称,就是通过上文代码中
@RedisHash("people")指定的。
- 第二种是Hash类型,是用来保存Java对象的,id是RedisRepository帮我们生成的,这个id和上图中set集合中保存的id是一致的。
spring cache缓存基本用法
为什么要做缓存
- 提升性能
绝大多数情况下,关系型数据库select查询是出现性能问题最大的地方。一方面,select 会有很多像 join、group、order、like 等这样丰富的语义,而这些语义是非常耗性能的;另一方面,大多数应用都是读多写少,所以加剧了慢查询的问题。
分布式系统中远程调用也会耗很多性能,因为有网络开销,会导致整体的响应时间下降。为了挽救这样的性能开销,在业务允许的情况(不需要太实时的数据)下,使用缓存是非常必要的事情。
- 缓解数据库压力
当用户请求增多时,数据库的压力将大大增加,通过缓存能够大大降低数据库的压力。
常用缓存操作流程
使用缓存最关键的一点就是保证:缓存与数据库的数据一致性,该怎么去做?下图是一种最常用的缓存操作模式,来保证数据一致性。
-
更新写数据
:先把数据存到数据库中,然后再让缓存失效或更新。缓存操作失败,数据库事务回滚。 -
删除写数据
: 先从数据库里面删掉数据,再从缓存里面删掉。缓存操作失败,数据库事务回滚。 -
查询读数据
缓存命中
:先去缓存 cache 中取数据,取到后返回结果。缓存失效
:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,在将数据放到缓存中。
如果上面的这些更新、删除、查询操作流程全都由程序员通过编码来完成的话
- 因为加入缓存层,程序员的编码量大大增多
- 缓存层代码和业务代码耦合,造成难以维护的问题。
整合Spring Cache
我们可以使用Spring cache解决上面遇到的两个问题,Spring cache通过注解的方式来操作缓存,一定程度上减少了程序员缓存操作代码编写量。注解添加和移除都很方便,不与业务代码耦合,容易维护。
第一步:pom.xml 添加 Spring Boot 的 jar 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
第二步:添加入口启动类 @EnableCaching
注解开启 Caching,实例如下。
@EnableCaching
在Spring Boot
中通过@EnableCaching
注解自动化配置合适的缓存管理器(CacheManager
),Spring Boot
根据下面的顺序去侦测缓存提供者,也就是说Spring Cache支持下面的这些缓存框架:
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis(因为我们之前引入了Redis,所以使用redis作为缓存)
- Caffeine
- Simple
在ArticleController类上实现一个简单的例子
下面的例子第一次访问走数据库(代码上断点断下来),第二次访问就走缓存了(不走函数代码)。可以自己下断点试一下。
@Cacheable(value="article")
@GetMapping( "/article/{id}")
public @ResponseBody AjaxResponse getArticle(@PathVariable Long id) {
使用redis缓存,被缓存的对象(函数返回值)有几个非常需要注意的点:
- 必须实现无参的构造函数
- 需要实现Serializable 接口和定义serialVersionUID (因为缓存需要使用JDK的方式序列化和反序列化)。
更改Redis缓存的序列化方式
让缓存使用JDK默认的序列化和反序列化方式非常不友好,我们完全可以修改为使用JSON序列化与反序列化的方式,可读性更强,体积更小,速度更快
@Configuration
public class RedisConfig {
//这个函数是上一节的内容
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//重点在这四行代码
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
//本节的重点配置,让Redis缓存的序列化方式使用redisTemplate.getValueSerializer()
//不在使用JDK默认的序列化方式
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}
详述缓存声明式注解的使用
缓存注解-增删改查
一定要把这张图理解的非常透彻,才能把缓存注解用好。
- @Cacheable:针对查询方法配置,能够根据查询方法的请求参数对其结果进行缓存(完成上图中的蓝色连线箭头的缓存流程)
- @CacheEvict:被注解的方法执行前或者执行之后,删除缓存(红色连线箭头3:让缓存失效、删除)
- @CachePut:调用被注解的方法,对其返回结果进行缓存更新(红色连线箭头3:更新数据库后更新缓存)
- @Caching:可以将上面三种注解,组合起来使用
单个对象的查询缓存
仍然以我们之前一直使用的ArtivleServiceImpl为例(包含增删改查方法),添加缓存注解。
被@Cacheable
注解的方法,在第一次被请求的时候执行方法体,并将方法的返回值放入缓存。在第二次请求的时候,由于缓存中已经包含该数据,将不执行被注解的方法的方法体,直接从缓存中获取数据。对于查询过程的缓存操作,要满足上图中的蓝色箭头线指引的操作流程,所有的操作流程只需要加上一个@Cacheable
就可以实现。
public static final String CACHE_OBJECT = "article"; //缓存名称
@Cacheable(value = CACHE_OBJECT,key = "#id") //这里的value和key参考下面的redis数据库截图理解
public ArticleVO getArticle(Long id) {
return dozerMapper.map(articleMapper.selectById(id),ArticleVO.class);
}
需要注意的是:缓存注解的key是一个SPEL表达式
,“#id”表示获取函数的参数id的值作为缓存的key值
。如果参数id=1,那么最终redis缓存的key就是:“article::1”。下图是redis缓存数据库中这条缓存记录的截图:
集合对象的查询缓存
大家要注意Object
和List<Object>
是两种不同的业务数据,所以对应的缓存也是两种缓存。注意下文中,缓存注解的key是字符串list,因为缓存注解默认使用SPEL表达式,如果我们想使用字符串需要加上斜杠。
public static final String CACHE_LIST_KEY = "\"list\"";
@Cacheable(value = CACHE_OBJECT,key = CACHE_LIST_KEY)
public List<ArticleVO> getAll() {
List<Article> articles = articleMapper.selectList(null);
return DozerUtils.mapList(articles,ArticleVO.class);
}
对于查询过程的缓存操作,要满足上图中的蓝色箭头线指引的操作流程,所有的操作流程只需要加上一个@Cacheable
就可以实现。目前MySQL数据库的article表有4条数据,所以缓存结果是一个包含4个article元素的数组
删除单个对象及其缓存
如下面的代码所示,将在函数执行成功之后删除redis key为“article::1”的缓存(假设删除id=1的记录)。
@Override
@Caching(evict = {
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY), //删除List集合缓存
@CacheEvict(value = CACHE_OBJECT,key = "#id") //删除单条记录缓存
})
public void deleteArticle(Long id) {
articleMapper.deleteById(id);
}
- 执行该方法传递参数id=1,先执行方法去操作删除MySQL数据;成功之后在《1.1.单个对象的查询缓存》缓存到redis中的key为“article::1”的缓存也将被删除。
- 任何一个artilce记录被删除,都会引起article::list缓存与MySQL数据库记录不一致的情况,所以需要把article::list集合缓存也删除掉。
-
因为Java 语法不允许在同一个方法上使用两个同样的注解
@CacheEvict
,所以我们用@Caching
注解把两个@CacheEvict
包起来。
新增一个对象
- 新增MySQL数据的时候新增redis缓存么?不是的,缓存是在获得查询结果时候回写到缓存里面的,不在新增的时候加缓存。
- 新增的时候删除缓存么“?是的,因为我们缓存了List的集合,一旦新增一条记录。原来MySQL数据库有4条记录,新增之后MySQL数据库有5条记录,redis缓存数据库缓存结果”article::list“仍然有4条记录。redis缓存中的数据与MYSQL数据库中的数据不一致,所以把”article::list“缓存删掉。
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY) //删除List集合缓存
public void saveArticle(ArticleVO article) {
Article articlePO = dozerMapper.map(article, Article.class);
articleMapper.insert(articlePO);
}
执行完成上面的方法,MySQL数据库新增了一条article记录;成功之后在《1.2.集合对象的查询缓存》缓存到redis中的key为“article::list”的缓存也将被删除。
更新一个对象
注意更新对象的时候,我们在该方法上面加了两个缓存注解。
-
下文的
CachePut
注解的作用是在方法执行成功之后,将其返回值放入缓存。key ="#article.getId()"
表示使用参数article
的id
属性作为缓存key
。 -
下文的
CacheEvict
注解用于将“article::list”
的缓存删除,因为某一条记录的数据更新,就表示原来缓存的List
集合数据与MySQL
数据库中的数据不一致,所以把它删除掉。缓存数据可以没有,但是不能和后端被缓存的关系数据库数据不一致。
@CachePut(value = CACHE_OBJECT,key = "#article.getId()")
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY)
public ArticleVO updateArticle(ArticleVO article) {
Article articlePO = dozerMapper.map(article,Article.class);
articleMapper.updateById(articlePO);
return article; //为了保证一致性,最后返回的更新结果,最好从数据库去查
}
执行完成该方法,假如ArticleVO参数对象的id=1
- MySQL数据库中的id=1的记录将被更新
- redis数据库中”article::1“的记录也将被更新(CachePut)
- redis数据库中”article::list“的记录将被删除(CacheEvict)
更新一个对象(另一种方法)
需要特别注意的是:如果在更新方法上使用CachePut
注解,该方法一定要有数据更新之后返回值,因为返回值就是缓存值
。
比较简单的做法是直接将不一致的缓存删掉,而不是去更新缓存。
这样操作对于程序员的要求更低,不容易出错。
缓存数据可以没有,但是不能和后端被缓存的关系数据库数据不一致。
@Override
@Caching(evict = {
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY), //删除List集合缓存
@CacheEvict(value = CACHE_OBJECT,key = "#article.getId()") //删除单条记录缓存
})
public void updateArticle(ArticleVO article) {
Article articlePO = dozerMapper.map(article,Article.class);
articleMapper.updateById(articlePO);
}
方法不需要有返回值。执行完成该方法,假如ArticleVO参数对象的id=1
- MySQL数据库中的id=1的记录将被更新
- redis数据库中”article::1“的记录将被删除
- redis数据库中”article::list“的记录将被删除
缓存注解配置说明
@Cacheable
通常应用到读取数据的查询方法上:先从缓存中读取,如果没有再调用方法获取数据,然后把数据查询结果添加到缓存中。如果缓存中查找到数据,被注解的方法将不会执行。
@CachePut
通常应用于修改方法配置,能够根据方法的请求参数对其注解的函数返回值进行缓存,和 @Cacheable
不同的是,它每次都会触发被注解方法的调用。
@CachEvict
通常应用于删除方法配置,能够根据一定的条件对缓存进行删除。可以清除一条或多条缓存。
在实际的生产环境中,没有一定之规,哪种注解必须用在哪种方法上,@CachEvict
注解通常也用于更新方法上。数据的缓存策略,要根据资源的使用方式,做出合理的缓存策略规划。保证缓存与业务数据库的数据一致性。并做好测试,对于缓存的正确使用,测试才是王道!