一:缓存简介
1.1 缓存定义
缓存是一个高速数据交换的存储器,使用它可以快速的访问和操作数据 .
1.2 程序中的缓存
当没有使用缓存时 :
但随着业务的发展,公司的框架慢慢变成了多个程序调用一个数据库的情况了:
这是大部分公司的普遍的架构流程图,因此当公司业务发展到一定规模之后,最可能出现性能瓶颈的地方就是数据库。数据库的资源同时也是程序中最昂贵的资源,因此为了防止数据库被过度的浪费,我们就需要给它雇一个“助理”了,这个助理就是缓存系统。
加入缓存后,程序的交互流程如下图所示:
改进之后 , 当有程序访问数据库时 , 会先向缓存中请求数据 , 如果缓存中有要请求的数据 , 就不需要访问数据库了 ; 当缓存中没有数据时 , 才会访问数据库 , 这样大大降低了数据库的压力 , 同时也提高了程序的反应速度 .
1.3 缓存优点
相比于数据库而言,缓存的操作性能更高,缓存性能高的主要原因有以下几个:
- 缓存一般都是 key-value 查询数据的,因为不像数据库一样还有查询的条件等因素,所以查询的性能一般会比数据库高;
- 缓存的数据是存储在内存中的,而数据库的数据是存储在磁盘中的,因为内存的操作性能远远大于磁盘,因此缓存的查询效率会高很多;
- 缓存更容易做分布式部署(当一台服务器变成多台相连的服务器集群),而数据库一般比较难实现分布式部署,因此缓存的负载和性能更容易平行扩展和增加。
1.4 缓存分类
缓存可分为本地缓存和分布式缓存 .
本地缓存也叫单机缓存,也就是说可以应用在单机环境下的缓存。所谓的单机环境是指,将服务部署到一台服务器上,如下图所示 :
举个栗子
本地缓存相当于学校的规定 , 每个学校有不同的规定 , 例如熄灯时间 , 西科大晚上23点熄灯 , 西石油晚上12点熄灯, 西交大晚上直接不熄灯 . 对于学校的规定 , 只适用于本校 .
同样地 , 本地缓存的特征是只适用于当前系统 .
分布式缓存是指可以应用在分布式系统中的缓存。所谓的分布式系统是指将一套服务部署到多台服务器,并且通过负载分发将用户的请求按照一定的规则分发到不同服务器,如下图所示 :
举个栗子
分布式系统相当于国家的规定 , 例如非法种植罂粟三千株以上或者其他毒品原植物数量大的,处五年以上有期徒刑,并处罚金或者没收财产。作为中国公民 , 都应当遵守这样的规定 .
分布式缓存就适用于所有的系统 . 比如我们在分布式系统中的服务器 A 中存储了一个缓存 key=laobai,那么在服务器 B 中也可以读取到key=laobai 的数据,这就是分布式缓存的作用 .
1.5 常见缓存使用
本地缓存的常见使用:Spring Cache、MyBatis 的缓存等。
分布式缓存的常见使用:Redis 和 Memcached。
1.5.1 本地缓存—Spring Cache
在 Spring Boot 项目,可以直接使用 Spring 的内置 Cache(本地缓存),只需要完成以下三个步骤就可以正常使用了:
- 开启缓存
- 操作缓存
- 调用缓存
1.开启缓存 : 在 Spring Boot 的启动类上添加如下代码,开启缓存:
@SpringBootApplication
@EnableCaching # 开启缓存功能
public class BiteApplication {
public static void main (String[] args) {
SpringApplication run BiteApplication class args
}
}
2.编写缓存操作代码 : 在 Service 层增加三个缓存操作的方法:添加缓存、修改缓存、删除缓存,示例代码如下:
package com.example.demo.service;
import com.example.demo.model.User;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
//查询用户信息[走缓存]
@Cacheable(cacheNames = "getuser",key = "#id")
public User getUserId(int id) {
System.out.println("进入getUserId方法~~");
User user = new User();
user.setId(id);
user.setName("Java");
user.setPassword("123456");
return user;
}
//修改用户信息
@Cacheable(cacheNames = "getuser",key="#id")
public User updateUser(int id,String name,String password) {
System.out.println("进入updateUser方法~~");
User user = new User();
user.setId(id);
user.setName(name);
user.setPassword(password);
return user;
}
//删除用户信息
@Cacheable(cacheNames = "getuser",key="#id")
public boolean delUser(int id) {
System.out.println("进入delUser方法~~");
return true;
}
}
3.编写触发代码
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getuser")
public User getUserId(int id) {
return userService.getUserId(id);
}
@RequestMapping("/update")
public User updateUser(int id,String name,String password) {
return userService.updateUser(id,name,password);
}
@RequestMapping("/del")
public boolean delUser(int id) {
return userService.delUser(id);
}
}
以上步骤执行完之后,可以使用 Postman 模拟调用来查看缓存 .
1.5.2 分布式缓存—Redis
在 Spring 框架中我们也可以直接操作 Redis 缓存,它的操作流程如下图所示:
Q : Redis 和 Memcached 有什么区别?
A :
- 存储方式不同:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部分存在硬盘上,这样能保证数据的持久性;
- 数据支持类型:memcache 对数据类型支持相对简单;Redis 有复杂的数据类型;
- 存储值大小不同:Redis 最大可以达到 512MB,memcache 只有 1MB。
总结:通常情况下,如果是单机 Spring 项目,会直接使用 Spring Cache 作为本地缓存,如果是分布式环境一般会使用 Redis。
二 : Redis
REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。
2.1 Redis数据类型和使用
Redis 有 5 大基础数据类型:
- String——字符串类型
- Hash——字典类型
- List——列表类型
- Set——集合类型
- ZSet——有序集合类型
其中最常用的是字符串和字典类型。
2.1.1 字符串类型
字符串类型 ( Simple Dynamic Strings) , 简称 SDS ,译为:简单动态字符串,它是以键值对 key-value 的形式进行存储的,根据 key 来存储和获取 value 值,它的使用相对来说比较简单,但在实际项目中应用非常广泛。
字符串的使用 :
注意 , 第一个命令来了 , 使用下面的命令可以连接Redis服务器 :
redis-cli
我们也可以使用 ex(expires)参数来设置字符串的过期时间,如下代码所示:
设置key3 1000s后过期 .
字符串的常见使用场景:
-
存放用户(登录)信息;
-
存放文章详情和列表信息;
-
存放和累计网页的统计信息。
2.1.2 字典类型
字典类型 (Hash) 又被称为散列类型或者是哈希表类型,它是将一个键值 (key) 和一个特殊的“哈希表”关联起来,这个“哈希表”表包含两列数据:字段和值,它就相当于 Java 中的
Map<String,Map<String,String>>
结构。
假如我们使用字典类型来存储一篇文章的详情信息,存储结构如下图所示:
字典类型的数据结构,如下图所示:
通常情况下字典类型会使用数组的方式来存储相关的数据,但发生哈希冲突时才会使用链表的结构来存储数据。
2.2 持久化
所谓的持久化就是将数据从内存保存到磁盘的过程,它的目的就是为了防止数据丢失 。因为内存中的数据在服务器重启之后就会消失 , 而磁盘的数据则不会,因此为了系统的稳定起见,我们需要将数据进行持久化。同时持久化功能又是 Redis 和 Memcached 最主要的区别之一,因为 Redis 支持持久化而 Memcached 不支持 .
Redis持久化的方式有以下3种 :
- 快照方式(RDB,Redis DataBase)将某一个时刻的内存数据,以二进制的方式写入磁盘;
- 文件追加方式(AOF,Append Only File),记录所有的操作命令,并以文本的形式追加到文件中;
- 混合持久化方式,Redis 4.0之后新增的方式,混合持久化是结合了RDB和AOF的优点,在写入的时候,先把当前的数据以RDB的形式写入文件的开头,再将后续的操作命令以AOF的格式存入文件,这样既能保证 Redis重启时的速度,又能降低数据丢失的风险。
2.2.1 RDB优点
RDB 的内容为二进制的数据,占用内存更小,更紧凑,更适合做为备份文件;
RDB对灾难恢复非常有用,它是一个紧凑的文件,可以更快的传输到远程服务器进行Redis服务恢复;
RDB可以更大程度的提高Redis 的运行速度,因为每次持久化时Redis 主进程都会fork()一个子进程,进行数据持久化到磁盘,Redis主进程并不会执行磁盘l/O等操作;与AOF格式的文件相比,RDB文件可以更快的重启。
2.2.2 RDB缺点
因为RDB只能保存某个时间间隔的数据,如果中途Redis服务被意外终止了,则会丢失一段时间内的Redis数据;
RDB需要经常 fork()才能使用子进程将其持久化在磁盘上。如果数据集很大,fork()可能很耗时,并且如果数据集很大且CPU性能不佳,则可能导致Redis 停止为客户端服务几毫秒甚至一秒钟。
2.2.3 AOF优点
AOF持久化保存的数据更加完整,AOF提供了三种保存策略︰
- 每次操作保存
- 每秒钟保存一次
- 跟随系统的持久化策略保存
其中每秒保存一次,从数据的安全性和性能两方面考虑是一个不错的选择,也是AOF默认的策略,即使发生了意外情况,最多只会丢失1s 钟的数据;
AOF采用的是命令追加的写入方式,所以不会出现文件损坏的问题,即使由于某些意外原因,导致了最后操作的持久化数据写入了一半,也可以通过redis-check-aof工具轻松的修复;
AOF持久化文件,非常容易理解和解析,它是把所有Redis键值操作命令,以文件的方式存入了磁盘。即使不小心使用flushall命令删除了所有键值信息,只要使用AOF文件,删除最后的flushall命令,重启Redis 即可恢复之前误删的数据。
2.2.4 AOF缺点
对于相同的数据集来说,AOF文件要大于RDB文件;
在Redis 负载比较高的情况下,RDB 比 AOF性能更好;
RDB使用快照的形式来持久化整个Redis 数据,而AOF 只是将每次执行的命令追加到AOF 文件中,因此从理论上说,RDB 比AOF更健壮。
( 健壮性具体指的是系统在不正常的输入或不正常的外部环境下仍能表现出正常的程度.)
2.2.5 混合持久化优点
混合持久化结合了RDB 和AOF持久化的优点,开头为RDB 的格式,使得Redis可以更快的启动,同时结合AOF 的优点,有效降低了大量数据丢失的风险。
2.2.6 混合持久化缺点
AOF文件中添加了RDB格式的内容,使得AOF文件的可读性变得很差;
兼容性差,如果开启混合持久化,那么此混合持久化AOF文件,就不能用在Redis 4.0之前版本了。
2.3 安装Redis
2.3.1 yum安装Redis
使用命令行方式安装Redis . 使用以下命令,直接将 redis 安装到 linux 服务器:
yum -y install redis
显示Complete! 即表示安装成功了 !
2.3.2 启动Redis
使用以下方式 , 以后台运行方式启动redis :
redis-server /etc/redis.conf &
使用下列命令可以查看redis进程 :
ps -ef|grep redis
2.3.3 操作Redis
使用以下命令启动 redis 客户端:
redis-cli
2.3.4 设置远程连接
- 将 redis 配置文件下载到本地:redis 配置文件是 linux 下的 /etc/redis.conf ;
- 将 redis.conf 中的 “bind 127.0.0.1”注释掉;
- 将 redis.conf 中的“protected-mode yes” 改为“protected-mode no”;
- 将修改后的 redis.conf 上传至 liunx 下的 /etc 目录;
- 使用命令“redis-cli shutdown”先关闭 redis 服务,再使用“redis-server /etc/redis.conf &”启动 redis 服务 .
使用 Another Redis Desktop Manager 查看连接效果 :
顺便说一下 , Redis的默认端口是6379 , 但是后来我的6379端口出现了安全性问题 , 所以我将Redis的端口改到了6378(在配置文件中修改即可) .
2.4 SpringBoot集成Redis
2.4.1 添加Redis依赖
2.4.2 配置 redis
spring.redis.database=1
spring.redis.port=6378
spring.redis.host=121.4.87.61
spring.session.store-type=redis
server.servlet.session.timeout=1800
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session
2.4.3 操作Redis
package com.example.demo.controller;
import com.example.demo.model.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RedisController {
private User user;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ObjectMapper objectMapper;
private final String object_redis_key ="user1";
private final String object_redis_key2 ="user2";
//在Redis中存储字符串
@RequestMapping("/setstr")
public String setRedis(String key,String value) {
if(StringUtils.hasLength(key) && StringUtils.hasLength(value)) {
stringRedisTemplate.opsForValue().set(key,value);
return "Redis设置成功!";
} else {
return "参数有误!";
}
}
//读取Redis中的字符串
@RequestMapping("/getstr")
public String getRedis(String key) {
String value = null;
if(StringUtils.hasLength(key)) {
value = stringRedisTemplate.opsForValue().get(key);
}
return "结果是: " + value;
}
//单例模式
public User getUser() {
if(user == null) {
synchronized (this) {
if(user == null) {
user = new User();
user.setId(1);
user.setName("白居易");
user.setPassword("123456");
}
}
}
return user;
}
//将对象存储到 redis
@RequestMapping("/setobj")
public String setObj() throws JsonProcessingException {
User user = getUser();
System.out.println(user);
String userStr = objectMapper.writeValueAsString(user);
stringRedisTemplate.opsForValue().set(object_redis_key, userStr);
return "操作成功!";
}
@RequestMapping("/getobj")
public User getObj() throws JsonProcessingException {
String userStr = (String) stringRedisTemplate.opsForValue().get(object_redis_key);
User user = objectMapper.readValue(userStr, User.class);
return user;
}
//将对象存储到 redis hash 中
@RequestMapping("/sethash")
public boolean setHash() {
User user = getUser();
stringRedisTemplate.opsForHash().put(object_redis_key2, "id", String.valueOf(user.getId()));
stringRedisTemplate.opsForHash().put(object_redis_key2, "name", user.getName());
stringRedisTemplate.opsForHash().put(object_redis_key2, "password", user.getPassword());
return true;
}
@RequestMapping("/gethash")
public String getHash() {
return stringRedisTemplate.opsForHash().get(object_redis_key2, "name").toString();
}
}
存储字符串 :
存储对象 :
存储字典类型 :
2.5 Spring Session持久化
2.5.1 添加依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.5.2 修改配置
spring.redis.database=2
spring.redis.port=6378
spring.redis.host=121.4.87.61
spring.session.store-type=redis
server.servlet.session.timeout=1800
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session
Redis默认16个db , 此处使用数据库2 . 对于db正确的理解应为“命名空间”,多个应用程序不应使用同一个Redis不同库,而应一个应用程序对应一个Redis实例,不同的数据库可用于存储不同环境的数据。关于为什么Redis默认16个库, 可以参考这篇文章 .
2.5.3 存储和读取代码
package com.example.demo.model;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
public int id;
public String name;
public String password;
}
注意进行序列化 !
package com.example.demo.controller;
import com.example.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
public class UserController {
private String user_session_key = "session1";
@RequestMapping("/login")
public boolean login(HttpSession session) {
User user = new User();
user.setId(2);
user.setName("张三");
user.setPassword("123456");
session.setAttribute(user_session_key,user);
return true;
}
@RequestMapping("/getsession")
public User getSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session != null) {
return (User) session.getAttribute(user_session_key);
}
return null;
}
}
会话信息是否缓存了呢 ? 借助工具查看 :
这时候调用getsession :
此时重启IDEA , 如果用户信息被缓存 , 那么再次调用getsession , 应该还是可以得到同样的信息 :
这就是使用Redis进行session持久化的整个过程 !
2.6 Redis相关面试题
参考文章 : Redis相关面试题
2.7 改进博客项目
修改完毕 !!!