1.为什么要用缓存
1.1.缓存定义
缓存是一个高速数据交换的存储器
,使用它可以快速的访问和操作数据
1.2. 程序中的缓存
当没有使⽤缓存时,程序的调⽤流程是这样的:
但随着业务的发展,公司的框架慢慢变成了多个程序调⽤⼀个数据库的情况了
此时,最可能出现性能瓶颈的就是数据库。数据库的资源同时也是程序中最昂贵的资源,因此为了防⽌数据库被过度的浪费,我们就需要给它雇⼀个“助理”了,这个助理就是缓存系统。
加⼊缓存后,程序的交互流程如下图所示:
这样改造之后,所有的程序不会直接调⽤数据库,⽽是会先调⽤缓存,当缓存中有数据时会直接返回,当缓存中没有数据时才去查询数据库,这样就⼤⼤的降低了数据库的压⼒,并加速了程序的响应速度
1.3.缓存的优点
- 缓存key-value,
存储结构简单
,所以查询效率比较快
- 缓存是
存储在内存中
的,而一般的数据库是将数据存储在磁盘,内存操作>磁盘的操作速度的,因此缓存的操作和读取比较快
- 缓存
可扩展比较强
,因此它的负载能力更强,查询效率更快
2.缓存的分类
- 本地缓存
- 分布式缓存
2.1.本地缓存
也就是单机缓存
,也就是说可以应⽤在单机环境下的缓存
。所谓的单机环境是指,将服务部署到⼀台服务器上
,如下图所示:
注意点:本地缓存的特征只适用于当前系统
2.2.分布式缓存
是指可以应用在分布式系统中的缓存
,分布式系统是指将一套服务器部署到多台服务器,并且通过负载分发将用户的请求按照一定的规则分发到不同的服务器,适用于所有服务器
⽐如我们在分布式系统中的服务器 A 中存储了⼀个缓存 key=laowang,那么在服务器 B 中也可以读取到key=laowang 的数据,这样情况就是分布式缓存的作⽤
3.常见缓存的使用
- 本地缓存:SpringCache,MyBatis
- 分布式缓存:Redis和Memcached
3.1.本地缓存:Spring Cache
在Spring Boot项目中,可以直接使用Spring的内置Cache(本地缓存)
Spring Cache使用步骤:
- 开启缓存
- 操作缓存
- 调用缓存
//(1)开启缓存
@SpringBootApplication
@EnableCaching # 开启缓存功能
public class BiteApplication {
public static void main(String[] args) {
SpringApplication.run(BiteApplication.class, args);
}
}
//(2)操作缓存
@Service
public class UserService {
/**
* 查询⽤户信息(⾛缓存)
*/
@Cacheable(cacheNames = "getuser", key = "#id")
public UserDO getUserById(int id) throws InterruptedException{
System.out.println("进⼊ get user ⽅法了。。。。。。。");
UserDO userDO = new UserDO();
userDO.setId(id);
userDO.setName("Java");
userDO.setAge(18);
return userDO;
}
/**
* 修改⽤户信息
* @return
*/
@CachePut(cacheNames = "getuser", key = "#id")
public UserDO updateUser(int id, String name) {
UserDO userDO = new UserDO();
userDO.setId(id);
userDO.setName(name);
return userDO;
}
/**
* 删除⽤户信息
* @param id
* @return
*/
@CacheEvict(cacheNames = "getuser", key = "#id")
public boolean delUser(int id) {
return true;
}
}
//(3)使用缓存
@RestController
@RequestMapping("/user")
public class UserController {
// 获得 UserService 对象
@Autowired
private UserService userService;
@RequestMapping("/getuser")
public UserDO getUserById(int id) throws InterruptedException{
return userService.getUserById(id);
}
@RequestMapping("/up")
public UserDO updateUserById(int id, String name){
return userService.updateUser(id, name);
}
@RequestMapping("/del")
public boolean delUserById(int id) {
return userService.delUser(id);
}
}
说明:
当用户访问user/get的时候,会去调用userService里面的getUser方法,getUser方法在第一次没有缓存的时候
,会查询数据库,查询到了就返回信息并且将信息存储到缓存中
,然后此时,当我们第二次进行调用的时候就直接从缓存中进行数据的一个查询,而无需进行数据库的查询操作了
,而当用户id改变了的时候,因为这时候此id并没有在缓存中,所以会查询再次数据库
3.2.分布式缓存:Redis
Redis和Memcached有什么区别
- 存储方式不同,
memcache把数据全部存在内存中
,断电之后会挂掉,数据不能超过内存大小,redis有部分存在硬盘上
,这样能保证数据的持久性 - 数据支持类型:
memcache对数据类型支持相对简单,Redis有复杂的数据类型
- 存储值大小不同:
Redis最大可以达到512mb,memcache只有1mb
总结:
通常情况下,如果是单机Spring项目,会直接使用Spring作为本地缓存,如果是分布式环境一般会使用Redis
4.Redis数据类型和使用
- String:字符串类型
- Hash:字典类型
- List:列表类型
- Set:集合类型
- ZSet:有序集合类型
4.1.字符串类型
简单动态字符串,以键值对key-value的形式进行存储的
,根据key来存储和获取value值
在云服务器上操作:
127.0.0.1:6379> set key1 hello # 添加数据
OK
127.0.0.1:6379> get key1 # 查询数据
"hello"
127.0.0.1:6379> get key2 # 查询数据
(nil) #表示不存在
127.0.0.1:6379> set key1 world ex 5
OK
127.0.0.1:6379> get key1
"world"
......
127.0.0.1:6379> get key1
(nil)
解释:
设置ex 5的目的是5秒之后过期,等过完5秒之后,这个键值对将会被销毁,再去读取就为(nil)了
字符串的常见使用场景
- 存放用户(登录)信息
- 存放文章详情和列表信息
- 存放和累计网页的统计信息
4.2.字典类型
被称为散列类型或者是哈希表类型,它是将一个键值(key)和一个特殊的“哈希表”关联起来,这个“哈希表”表包含两列数据:字段和值,它就相当于Java中的Map<String,Map<String,String>>
结构
同理我们也可以使用字典类型来存储用户信息,并且使用字典类型来存储此类信息就无需手动序列化和反序列化数据了,所以使用起来更加的方便和高效:
127.0.0.1:6379> hset myhash key1 value1 # 添加数据
(integer) 1
127.0.0.1:6379> hget myhash key1 # 查询数据
"value1"
通常情况下字典类型会使⽤数组的⽅式来存储相关的数据,但发⽣哈希冲突时才会使⽤链表的结构来存储数据。
4.3.列表类型
是一个使用链表结构存储
的有序
结构,它的元素插入会按照先后顺序存储到链表结构中,因此它的元素操作(插入和删除)时间复杂度为O(1)
,所以相对来说速度还是比较快的,但它的查询时间复杂度为O(n)
,因此查询可能会比较慢
127.0.0.1:6379> lpush list 1 2 3 # 添加数据
(integer) 3
127.0.0.1:6379> lpop list # 获取并删除列表的第⼀个元素
1
使用场景:
- 消息队列:列表类型可以使⽤ rpush 实现
先进先出
的功能,同时⼜可以使⽤ lpop 轻松的弹出(查询并删除)第⼀个元素,所以列表类型可以⽤来实现消息队列; - ⽂章列表:对于博客站点来说,当⽤户和⽂章都越来越多时,为了加快程序的响应速度,我们可以把⽤户⾃⼰的⽂章存⼊到 List 中,因为 List 是有序的结构,所以这样⼜可以完美的实现分⻚功能,从⽽加速了程序的响应速度。
4.4.集合类型
集合类型 (Set) 是⼀个⽆序并唯⼀
的键值集合。
127.0.0.1:6379> sadd myset v1 v2 v3 v3# 添加数据
(integer) 3
127.0.0.1:6379> smembers myset # 查询集合中的所有数据
1) "v1"
2) "v3"
3) "v2"
使⽤场景如下:
- 微博关注我的⼈和我关注的⼈都适合⽤集合存储,可以保证⼈员不会重复;
- 中奖⼈信息也适合⽤集合类型存储,这样可以保证⼀个⼈不会重复中奖。
- 集合类型(Set)和列表类型(List)的
区别
如下:
(1)列表可以存储重复元素,集合只能存储⾮重复元素;
(2)列表是按照元素的先后顺序存储元素的,⽽集合则是⽆序⽅式存储元素的
4.5.有序集合类型
有序集合类型 (Sorted Set) 相⽐于集合类型多了⼀个排序属性
score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,⼀个是有序结合的元素值
,⼀个是排序值
。有序集合的存储元素值也是不能重复的,但分值是可以重复的
127.0.0.1:6379> zadd zset1 30 xiaoming 40 xiaoli 50 laowang # 添加数据
(integer) 3
127.0.0.1:6379> zrange zset 0 -1 # 查询所有数据
1) "xiaoming"
2) "xiaoli"
3) "laowang"
使用场景:
- 学生成绩排名
- 粉丝列表,根据关注的先后时间排序
5.持久化
就是将数据从内存保存到磁盘的过程,它的目的是为了防止数据丢失,因为内存中的数据在服务器重启之后就会丢失,而磁盘的数据则不会,因此为了系统的稳定,我们需要将数据进行持久化 注意点:Redis支持持久化,而Memcached不支持5.1.持久化方式
- 快照方式:
将某一个时刻的内存数据,以二进制的方式写入磁盘
- 文件追加方式:
记录所有的操作命令,并以文本的形式追加到文件中
- 混合持久化方式:结合了以上两种方式的优点,在
写入的时候,先把当前的数据以快照形式写入文件的开头
,再将后续的操作命令以文件追加方式存入文件
,这样既能保证Redis重启时的速度,又能降低数据丢失的风险
5.2.持久化策略设置
可以在 redis-cli
命令⾏中执⾏ config set aof-use-rdb-preamble yes
来开启混合持久化,当开启混合持久化时 Redis 就以混合持久化⽅式来作为持久化策略;当没有开启混合持久化的情况下,使⽤config set appendonly yes
来开启 AOF 持久化的策略,当 AOF 和混合持久化都没开启的情况下默认会是 RDB 持久化的⽅式。
5.3.RDB
(1)RDB优点
- RDB的内容为
二进制的数据,占用内存更小,更紧凑,
更适合作为备份文件 - RDB
对灾难恢复非常有用
,它是一个紧凑的文件,可以更快的传输到远程服务器进行服务恢复 - RDB可以
更大程度的提高Redis的运行速度
,因为每次持久化时主进程都会fork()一个子进程,进行数据持久化到磁盘,Redis主进程并不会执行磁盘I/O等操作 - 与AOF格式的文件相比,
RDB文件可以更快的重启
(2)RDB缺点
- 因为RDB
只能保存某个事件间隔的数据
,如果中途Redis服务被意外终止了,则会丢失一段时间内的Redis数据 - RDB需要经常fork()才能使用子进程将其持久化在磁盘上,如果数据集很大,fork()可能很耗时,并且如果数据集很大且CPU性能不佳,则可能导致Redis停止为客户端服务几毫秒甚至一秒钟
5.4.AOF
(1)优点
- AOF持久化保存的数据更加完整,AOF提供了三种保存策略:
每次操作保存,每秒钟保存一次,跟随系统的持久化策略保存
,其中每秒保存一次,从数据的安全性和性能两方面考虑是一个不错的选择,也是AOF默认的策略,即使发生了意外情况,最多只会丢失1s钟的数据 - AOF采用的是命令追加的写入方式,所以不会出现文件损坏的问题,即使由于某些意外原因,导致了最后操作的持久化数据写入了一半,也可以通过
redis-check-aof
工具轻松的修复 - AOF持久化文件,非常容易理解和解析,它是把所有Redis键值操作命令,以文件的方式存入了磁盘,即使不小心使用
flushall
命令删除了所有键值信息,只要使用AOF文件,删除最后的flushall
命令,重启Redis即可恢复之前误删的信息
(2)缺点
- 对于相同的数据集来说,
AOF文件要大于RDB文件
- 在Redis
负载比较高
的情况下,RDB比AOF性能更好
- RDB使用快照的形式来持久化整个Redis数据,而AOF只是将每次执行的命令追加到AOF文件中,因此从理论上说,
RDB比AOF更健壮
5.5.混合持久化
(1)优点
- 混合持久化
结合了RDB和AOF的优点
,开头为RDB的格式,使得Redis可以更快的启动,同时结合AOF的优点
,又减低了大量数据丢失的风险
(2)缺点
- AOF文件中添加了RDB格式的内容,使得
AOF文件的可读性变得很差
兼容性差
,如果开启混合持久化,那么此混合持久化AOF文件,就不能用在Redis4.0之前的版本了
6.常见问题
6.1.缓存雪崩
缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能导致数据库宕机的情况正常情况下系统的执行流程如图所示:
缓存雪崩的执⾏流程,如下图所示
解决办法
(1)加锁排队
加锁排队可以起到缓冲的作用,放置大量的请求同时操作数据库,但是他的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了一部分用户体验
(2)随机化过期时间
为了避免缓存同时过期,可在设置缓存时添加随机时间,这样就可以极大的避免大量的缓存同时失效
(3)设置二级缓存
二级缓存指的是除了Redis本身的缓存,再设置一层缓存,当Redis失效之后,先去查询二级缓存,例如可以设置一个本地缓存,在Redis缓存失效的时候先去查询本地缓存而非数据库,加入二级缓存之后程序执行流程,如下图所示:
6.2.缓存穿透
缓存穿透是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透
其中红色路径表示缓存穿透的执行路径,可以看出缓存穿透会给数据库造成很大的压力
解决方案
(1)缓存空结果
(2)可以把每次从数据库查询的数据都保存到缓存中,为了提高前台用户的使用体验(解决长时间内查询不到任何信息的情况),我们可以将空结果的缓存时间设置的短一些,例如3-5分钟
6.3.缓存击穿
指的是某个热点缓存,在某一时刻恰好失效了,然后此时刚好有大量的并发请求,此时这些请求将会给数据库造成巨大的压力,这种情况就叫做缓存击穿
解决方案:
(1)加锁排队,在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力
(2)设置永不过期,对于某些热点缓存,我们可以设置用不过期,这样就能保证缓存的稳定性,但需要注意在数据更改之后,要及时更新热点缓存,不然就会造成查询结果的误差
6.4.缓存预热
首先来说,缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验
缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间
缓存预热的实现思路:
(1)把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动加载数据并缓存数据
(2)把需要缓存的方法挂载到某个页面或祸端接口上,手动触发缓存预热
(3)设置定时任务,定时自动进行缓存预热
7.Redis集群
随着业务的不断发展,单机Redis的性能已经不能满足我们的请求了,此时我们需要将单机Redis扩展为多级服务,Redis多机服务主要包含以下三个内容:
(1)Redis主从同步
(2)Redis哨兵模式
(3)Redis集群服务(Redis3.0新增功能)
7.1.主从同步
主从同步(主从复制)是Redis高可用服务的基石,也是多机运行中最基础的一个,我们把主要存储数据的结点叫做主节点
,把其他通过复制主节点数据的副本结点叫做从结点
在 Redis 中⼀个主节点可以拥有多个从节点,⼀个从节点也可以是其他服务器的主节点
、