重学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 -ilinux文件替换命令,替换格式为s/被替换的内容/替换之后的内容/
  • 替换logfile ""logfile "access.log",指定日志文件名称为access.log---->指定日志文件的名称
  • 替换# requirepass foobaredrequirepass 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-redislettuce。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

主从模式和哨兵模式参考文章推荐

Redis6—主从复制篇


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集群模式参考文章推荐

Redis–集群


使用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接口。

需要注意的是因为RedisTemplateStringRedisTemplate默认序列化存储方式不一样,所以二者存储的数据并不能通用。也就是说RedisTemplate存的数据只能用RedisTemplate去取,对于StringRedisTemplate也是一样。


解决key-value乱码问题

其实这个不是严格意义上的乱码,是JDK的二进制序列化之后的存储方式。人看不懂,但是程序是能看懂的。

那有没有人一种人能看懂,程序也能看懂的序列化结果?看下文的配置类代码

  • 采用StringRedisSerializerkey进行序列化(字符串格式)
  • 采用Jackson2JsonRedisSerializervalue将进行序列化(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缓存

重学SpringBoot系列之整合静态资源与模板引擎

重学SpringBoot系列之异步任务与定时任务

重学SpringBoot系列之嵌入式容器的配置与应用

重学SpringBoot系列之日志框架与全局日志管理

重学Springboot系列之邮件发送的整合与使用