重学SpringBoot系列之redis与spring cache缓存
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学SpringBoot系列之redis与spring cache缓存相关的知识,希望对你有一定的参考价值。
重学SpringBoot系列之redis缓存
使用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)重学SpringBoot系列之redis与spring cache缓存