开发中通常会使用缓存中间件:redis、memcached、ehcache.
redis可以用作数据库、缓存和消息中间件,本文中使用redis作为缓存
一、安装redis
1、docker中安装redis:
sudo docker pull redis
2、启动redis并映射端口
sudo docker run -d -p 6379:6379 --name myredis 镜像名
映射容器服务的 6379 端口到宿主机的 6379 端口。外部可以直接通过宿主机ip:6379 访问到 Redis 的服务
3、windows中安装
4、连接虚拟机
右击redis打开控制台console可以输入命令
二、配置redis
1、pom.xml中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、application.properties中配置
springboot中有redis的自动配置类,对使用spring.redis前缀的配置进行自动配置
#连接虚拟机中的redis
spring.redis.host=192.168.0.103
spring.redis.port=6379
3、在测试中注入redis操作的模板:
@Autowired
StringRedisTemplate stringRedisTemplate; //模板:操作key-value都是字符串
@Autowired
RedisTemplate redisTemplate; //模板:操作key-value,都是对象
模板中的方法:
/**
* Redis常见的五大数据类型
* String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
* stringRedisTemplate.opsForValue() [操作String 字符串的]
* stringRedisTemplate.opsForList() [List(列表)]
* stringRedisTemplate.opsForSet()
* stringRedisTemplate.opsForHash()
* stringRedisTemplate.opsForZSet()
* redisTemplate.opsForValue() //两个模板使用方法都一样
*
*/
4、测试用例:
在redis中添加字符串key-value
@Test
public void test01() {
//给redis中保存数据
stringRedisTemplate.opsForValue().append("msg", "hello");
//String str=stringRedisTemplate.opsForValue().get("msg");
//System.out.println(str);
}
5、测试例子:利用RedisTemplate类模板的实例保存对象object
注意:保存的对象employee需要是一个可序列化的类,默认如果保存对象,使用jdk序列化机制,序列化后的数据保存到redis中,如下图所示:
@Test
public void test02() {
//给redis中保存对象
Employee employee=employeemapper.getEmpById(2);
redisTemplate.opsForValue().set("emp-01", employee); //需要一个可序列化的类
}
@Test
void contextLoads() {
Employee employee=employeemapper.getEmpById(2);
System.out.println(employee);
}
在Employee类中继承序列化(Serializable)接口:
6、改变默认的序列化规则(不使用默认的jdk序列化机制)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//import org.springframework.context.annotation.Primary;
//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.Jackson2JsonRedisSerializer;
//import springboot.bean.Department;
import springboot.bean.Employee;
import java.net.UnknownHostException;
@Configuration
public class MyRedisConfig {
@Bean //注入模板到容器
public RedisTemplate<Object, Employee> empRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
template.setDefaultSerializer(ser);
return template;
}
}
在测试中注入模板
@Autowired
RedisTemplate<Object, Employee> empRedisTemplate;
利用该模板保存对象:
@Test
public void test02() {
//给redis中保存对象
Employee employee=employeemapper.getEmpById(2);
empRedisTemplate.opsForValue().set("emp-01", employee); //需要一个可序列化的类
}
可以直接保存对象
7、一个固定的自定义RedisTemplate模板:
三、测试缓存
缓存的原理:缓存管理器CacheManager–>创建出缓存组件Cache,缓存组件Cache来实际给缓存中CRUD(增删改查)数据
1、引入redis的starter后,容器中保存的是RedisCacheManager
2、RedisCacheManager创建RedisCache作为缓存组件;RedisCache通过操作redis缓存数据
(默认配置类使用SimpleCacheConfiguration,配置完redis后使用的配置类为RedisCacheConfiguration,缓存管理器使用RedisCacheManager)
3、redis中添加缓存,默认保存数据的key-value都是Object,利用序列化保存.如何将保存在缓存中的对象保存为json:
(下图为浏览器中查询2号员工:http://localhost:8080/emp/2
后的redis中缓存的数据)
- 引入了redis的starter,cacheManager变为RedisCacheManager
- 默认创建的RedisCacheManager操作redis的时候使用的是RedisTemplate<Object,Object>
- RedisTemplate<Object,Object>是默认使用jdk序列化机制(所以都是乱码)
四、Redis持久化
redis是基于内存的数据库,如果不将内存中的数据库状态保存到磁盘中,一旦服务器进程退出,服务器中的数据库状态也将丢失,所以需要持久化功能。
1、RDB
在指定的时间间隔内将内存中的数据集以快照的方式写入磁盘,恢复时将快照文件直接读到内存中。Redis会单独创建一个子进程来进行持久化,将数据写入到临时文件中,待持久化过程结束后用临时文件替换上次持久化好的文件,整个过程中,主进程不进行任何IO操作。如果需要大规模数据恢复,且对数据恢复完整性不是非常敏感,RDB方式比AOF方式更高效,RDB缺点是最后一次持久化后的数据可能丢失。一般情况下默认RDB。
(1)RDB触发机制:
- redis配置文件中save规则满足的情况下,自动触发rdb
- 执行flushall命令,触发rdb
- 退出redis,产生rdb文件dump.rdb
(2) 恢复rdb文件:
将rdb文件放在redis启动目录下(/usr/local/bin
),redis启动时会自动检查dump.rdb恢复数据
(3)优点:
- 适合大规模的数据恢复
(4)缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机,最后一次修改的数据丢失
- fork进程的时候,会占用一定的内存空间
2、AOF
以日志的形式记录每个写操作,将redis执行过的所有命令都记录下来,只许追加文件但是不可以修改文件,redis启动时会读取该文件重新构建数据,根据日志文件中的内容将写指令全部执行一次,完成数据的恢复。
默认不开启,在redis配置文件中修改appendonly 为yes
如果aof文件有错误,redis会无法启动,这时候需要修复aof文件,使用redis-check-aof --fix appendonly.aof
指令。如果aof文件大于64m,文件太大时,会fork一个新的进程将文件进行重写
(1)优点:
- 每一次修改都同步,文件完整性会更好
- 每秒同步一次,可能会丢失一秒数据
- 可修改从不同步,效率更高
(2)缺点:
- 相对于数据文件来说,aof文件远大于rdb文件,修复速度也比rdb慢
- aof运行效率也比rdb慢,所以redis默认使用rdb持久化
3、扩展
五、Redis主从复制
主从复制:将一台服务器的数据复制到其他的redis服务器中,前者为主节点,后者为从节点,数据复制都是单向的,只能由主节点到从节点。master节点以写为主,slave节点以读为主。
1、主从复制的作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复:当主节点出现问题时,可以由从节点实现快速的故障恢复
- 负载均衡:主从复制配合读写分离,由主节点提供写服务,从节点提供读服务,分担服务器负载,可以大大提高redis服务器的并发量
- 高可用(集群):主从复制时哨兵、集群的基础,所以主从复制是redis高可用的基础
2、使用多台redis服务器的原因
- 单节点的redis服务器会发生单点故障,且一台服务器需要处理所有的请求,负载压力大
- 单台redis服务器的内存容量有限
3、主从复制配置
只配置从库,不用配置主库(一开始每个redis服务器都是主节点)
只有主库可以写,从库不能写只能读
- 复制多份配置文件
- 修改每个配置文件的端口
- 修改配置文件的pid 名字
- 修改配置文件的log文件名字
- 修改配置文件中dump.rdb名字
- 启动多个redis服务器
- 从节点中使用
SLAVEOF 主机ip 主机port
成为主机的从节点(这只是暂时的配置) - 永久配置主从结构:在配置文件中找到这一行,修改主服务器的ip和端口号
ps:如果某时刻主机宕机了,从节点还是从节点,可以保存之前主节点的所有信息,等主节点恢复后,依然是该从节点的主节点
4、复制原理
slave启动成功连接到master后会发送一个sync命令,master接到命令后,启动后台的存盘进程,同时收集所有接受到的用于修改数据的命令,在后台进程执行完毕之后,master将整个数据文件传送到slave,完成一次完全同步
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:master继续将所有收集到的修改命令依次传给slave,完成同步(初次连接master时都会执行)
只要slave重新连接master,将执行一次全量复制,所有的数据都能在从节点中看到
六、哨兵模式
哨兵模式可以监控主机是否出现故障,如果发生故障,根据投票数自动将从节点变成主节点,是自动选举主节点的模式
1、哨兵的作用:
- 通过发送命令,让redis服务器返回监控其的运行状态,包括主服务器和从服务器
- 当哨兵检测到master宕机,自动将slave切换成master,然后通过发布订阅模式通知其他从服务器,修改配置文件,他们切换主机
只使用一个哨兵监控会出现问题,因此需要多个哨兵进行监控,各个哨兵之间也形成监控。
2、哨兵集群执行过程:
当一个哨兵监测到主节点宕机,并不会马上执行重新选举的过程,仅仅是该哨兵的主观判断主机下线,当其他的哨兵也监测到主服务器不可用时,当数量到达一定值时,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行重新选举(故障转移),切换主节点后,通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这样主机才是客观下线。若之前的主机恢复后,只能变成新主机的从机。
3、优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点他都有
- 主从可以切换,故障可以转移,系统可用性提高
- 哨兵模式就是主从模式的升级,自动配置
4、缺点:
- redis不方便在线扩容
- 哨兵模式配置复杂
七、缓存穿透与雪崩(服务器高可用)
1、缓存穿透(查询不到)
当用户查询一个数据,发现redis缓存中没有,于是会向持久层数据库查询,如果数据库中也没有,本次查询失败,如果用户很多,都会去直接查询数据库,会给数据库带来很大的压力,这就是缓存穿透
解决方法:
(1)使用布隆过滤器,对所有可能查询到的参数以hash形式存储,在控制层先进行校验,不符合则丢弃该请求,从而可以避免对底层存储系统的查询压力
(2)缓存空对象
当查询数据库失败后,即使是返回的null空对象,也将这个空对象进行缓存,并设置一个过期时间,之后再访问这个数据会从缓存中获取,减少了对数据库的访问。
存在的问题:
- 可能会缓存很多的空值作为键
- 即使对空值设置了过期时间,还是会导致缓存和数据库的数据的不一致。
2、缓存击穿(访问量大,缓存过期)
缓存击穿:对于一个热点key,会接受大量的并发访问,当这个key在失效的瞬间,持续的大并发就会穿破缓存层,直接请求数据库。
解决方案:
- 设置热点数据永不过期
- 分布式锁:保证对于每个key同时只有一个线程去访问后端服务,其他线程没有获得分布式锁的权限只能等待。
3、缓存雪崩
缓存雪崩:在某一时间段,缓存集中过期失效,redis宕机
解决方案:
- 增加redis服务器,一台宕机了还有其他的工作
- 限流降级:缓存失效后,通过加锁或者队列来控制读取数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 数据预热:在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效时间均匀。