小明是一家饭店的厨师,在饭店发展的初期,因为顾客不多因此小明生活得很悠闲,上班打卡玩手机,下班打卡逛夜店,生活过得十分惬意,但随着饭店的日益发展,顾客也越来越多了,因此出现了高峰期顾客长时间等待的情况,满意度也大幅下降。
因此老板就找到了小明,询问了相关的情况,小明说在高峰期自己要洗菜、切菜和炒菜,每个人的工作量都很大所以就忙不过了,因此上菜就比较慢了。
于是精明的老板就想到了一个好主意,他让厨师在平常不忙的时候把顾客长点的菜提前做好,然后妥善的保存起来,在高峰期直接拿出来热一下就好了,这样一来,果然工作效率就大大提升了,每到高峰期也能轻松应对了。
缓存是一个高速数据交换的存储器,使用它可以快速的访问和操作数据。
但随着业务的发展,公司的框架慢慢变成了多个程序调用一个数据库的情况了:
这是多部分公司的普遍的架构流程图,因此当公司业务发展到一定规模之后,最可能出现性能瓶颈的地方就是数据库。
数据库的资源同时也是程序中最昂贵的资源,因此为了防止数据库被过度的浪费,我们就需要给它雇一个“助理”了,这个助理就是缓存系统。
这样改造之后,所有的程序不会直接调用数据库,而是会先调用缓存,当缓存中有数据时会直接返回,当缓存中没有数据时才去查询数据库,这样就大大的降低了数据库的压力,并加速了程序的响应速度。
相比于数据库而言,缓存的操作性能更高,缓存性能高的主要原因有以下几个:
缓存大致可以分为两大类:
本地缓存也叫单机缓存,也就是说可以应用在单机环境下的缓存。所谓的单机环境是指,将服务部署到一台服务器上,如下图所示:
举个例子
本地缓存相当于每家企业的公司规定一样,不同的公司规定也是不同的,比如上班时间,不同的公司上班时间规定也是不同的,对于企事业单位来说一般要求9:00-17:00上班,而对于酒吧来说,这个时间就完全不适合了。
所以,本地缓存的特征是只适用于当前系统。
分布式缓存是指可以应用在分布式系统中的缓存。所谓的分布式系统是指将一套服务器部署到多台服务器,并且通过负载分发将用户的请求按照一定的规则分发到不同服务器,如下图所示:
举个例子
分布式缓存相当于适用于所有公司的规定,比如无论是任何公司都不能偷税漏税,不能做违反法律的事情,这种情况就和分布式缓存很像,适用于所有的系统。
比如我们在分布式系统中的服务器A中存储了一个缓存key=laowang,那么在服务器B中也可以读取到 key=laowang 的数据,这样情况就是分布式缓存的作用。
本地缓存的常见使用: Spring Cache、MyBatis的缓存等。
分布式缓存的常见使用: Redis 和 Memcached。
在Spring Boot项目,可以直接使用Spring 的内置Cache(本地缓存),只需要完成以下三个步骤就可以正常使用了:
开启缓存
在Spring Boot的启动类上添加如下代码,开启缓存:
@SpringBootApplication
@Enablecaching #开启缓存功能
public class BiteApplication {
public static void main(String[] args) {
SpringApplication.run(BiteApplication.class,args );
}
}
操作缓存
@service
public class UserService {
@Cacheable(cacheNames = "user" ,key = "#id" )
public string getUser(int id){
//伪代码
System.out.println("我进入了getUser方法");
return "ID: "+id;
}
}
使用缓存
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
Userservice userService;
@RequestMapping("/get")
public string getUser(int id) {
return userService.getUser(id);
}
}
使用Postman进行测试
结果分析:第一次调用的时候,调用伪代码进入Dao层,控制台会打印“我进入了getUser方法”。第二次、第三次往后再调用的时候就不会进入Dao层,而是进入缓存中读取数据,也就不会打印“我进入了getUser方法”。
在Spring框架中我们也可以直接操作Redis 缓存,它的操作流程如下图所示:
Redis和Memcached有什么区别?
总结:通常情况下,如果是单机Spring项目,会直接使用Spring Cache作为本地缓存,如果是分布式环境一般会使用Redis。
Redis有5大基础数据类型:
其中最常用的是字符串和字典类型。
字符串类型(Simple Dynamic Strings简称SDS),译为:简单动态字符串,它是以键值对 key-value的形式进行存储的,根据key 来存储和获取 value值,它的使用相对来说比较简单,但在实际项目中应用非常广泛。
字符串的使用如下:
1 127.e.0.1:6379> set k1 v1#添加数据
2 OK
3 127.0.0.1:6379>get k1 #查询数据
4 "v1"
5 127.8.8.1:6379> strlen k1 #查询字符串的长度
6 (Integer) 5
我们也可以使用ex (expires)参数来设置字符串的过期时间,如下代码所示:
1 127.0.0.1:6379> set k1 v1 ex 1000 #设置 k1 1000s后过期(删除)
2 OK
字符串的常见使用场景:
字典类型(Hash)又被成为散列类型或者是哈希表类型,它是将一个键值(key)和一个特殊的“哈希表”关联起来,这个“哈希表”表包含两列数据:字段和值,它就相当于Java中的Map
结构。
假如我们使用字典类型来存储一篇文章的详情信息,存储结构如下图所示:
同理我们也可以使用字典类型来存储用户信息,并且使用字典类型来存储此类信息就无需手动序列化和反序列化数据了,所以使用起来更加的方便和高效。
字典类型的使用如下:
1 127.0.0.1:6379> hset myhash key1 value1 #添加数据
2 (integer) 1
3 127.e.0.1:6379> hget myhash key1 #查询数据
4 "value1"
列表类型(List)是一个使用链表结构存储的有序结构,它的元素插入会按照先后顺序存储到链表结构中,因此它的元素操作(插入和删除)时间复杂度为O(1),所以相对来说速度还是比较快的,但它的查询时间复杂度为O(n),因此查询可能会比较慢。
列表类型的使用如下:
1 127.e.e.1:6379> lpush list 1 2 3#添加数据
2 (integer) 3
3 127.e.0.1:6379> lpop list #获取并删除列表的第一个元素
4 1
列表的典型使用场景有以下两个:
集合类型(Set)是一个无序并唯一的键值集合。
集合类型的使用如下:
1 127.0.0.1:6379> sadd myset v1 v2 v3#添加数据
2 (integer) 3
3 127.8.0.1:6379> smembers myset #查询集合中的所有数据
4 1) "v1"
5 2) "v3"
6 3) "v2"
集合类型的经典使用场景如下:
集合类型(Set)和列表类型(List)的区别如下:
有序集合类型(Sorted Set)相比于集合类型多了一个排序属性score (分值),对于有序集合ZSet来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。有序集合的存储元素值也是不能重复的,但分值是可以重复的。
当我们把学生的成绩存储在有序集合中时,它的存储结构如下图所示:
有序集合类型的使用如下:
1 127.0.0.1:6379> zadd zset1 3 golang 4 sql 1 redis #添加数据
2 (integer) 3
3 127.0.0.1:6379> zrange zset 0 -1 #查询所有数据
4 1) "redis"
5 2) "mysql"
6 3) "java"
有序集合的经典使用场景如下:
所谓的持久化就是将数据从内存保存到磁盘的过程,它的目的就是为了防止数据丢失。因为内存中的数据在服务器重启之后就会丢失,而磁盘的数据则不会,因此为了系统的稳定起见,我们需要将数据进行持久化。同时持久化功能又是Redis和 Memcached最主要的区别之一,因为Redis支持持久化而Memcached 不支持。
Redis持久化的方式有以下3种:
可以在redis-cli命令行中执行config set aof-use-rdb-preamble yes
来开启混合持久化,当开启混合持久化时Redis 就以混合持久化方式来作为持久化策略;当没有开启混合持久化的情况下,使用config set appendonly yes
来开启AOF持久化的策略,当AOF和混合持久化都没开启的情况下默认会是RDB持久化的方式。
flushall
命令删除了所有键值信息,只要使用AOF文件,删除最后的flushall
命令,重启Redis 即可恢复之前误删的数据。缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。
我们先来看下正常情况下和缓存雪崩时程序的执行流程图,正常情况下系统的执行流程如下图所示:
缓存雪崩的执行流程,如下图所示:
以上对比图可以看出缓存雪崩对系统造成的影响,那如何解决缓存雪崩的问题?缓存雪崩的常用解决方案有以下几个。
加锁排队
加锁排队可以起到缓冲的作用,防止大量的请求同时操作数据库,但它的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了一部分用户体验。
随机化过期时间
为了避免缓存同时过期,可在设置缓存时添加随机时间,这样就可以极大的避免大量的缓存同时失效。
示例代码如下:
1 l/缓存原本的失效时间
2 int exTime = 10 * 60;
3 //随机数生成类
4 Random random = new Random( );
5 /缓存设置
6 jedis.setex(cacheKey,exTime+random.nextInt(1000) , value) ;
设置二级缓存
二级缓存指的是除了Redis 本身的缓存,再设置一层缓存,当Redis 失效之后,先去查询二级缓存。
例如可以设置一个本地缓存,在Redis缓存失效的时候先去查询本地缓存而非查询数据库。加入二级缓存之后程序执行流程,如下图所示:
缓存穿透是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。
缓存穿透执行流程如下图所示:
其中红色路径表示缓存穿透的执行路径,可以看出缓存穿透会给数据库造成很大的压力。缓存穿透的解决方案有以下几个。
缓存空结果
另一种方式是我们可以把每次从数据库查询的数据都保存到缓存中,为了提高前台用户的使用体验(解决长时间内查询不到任何信息的情况),我们可以将空结果的缓存时间设置的短一些,例如3-5分钟。
缓存击穿的执行流程如下图所示:
它的解决方案有以下2个。
加锁排队
此处理方式和缓存雪崩加锁排队的方法类似,都是在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力。
设置永不过期
对于某些热点缓存,我们可以设置永不过期,这样就能保证缓存的稳定性,但需要注意在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。
首先来说,缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验。
缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。
缓存预热的执行流程,如下图所示:
缓存预热的实现思路有以下三种: