学习Redis基础必须掌握的内容:
缓存定义:缓存是⼀个高速数据交换的存储器,使用它可以快速的访问和操作数据。
当程序没有使用缓存:
这是多部分公司的普遍的架构流程图,因此当公司业务发展到⼀定规模之后,最可能出现性能瓶颈的地方就是数据库。
数据库的资源同时也是程序中最昂贵的资源,因此为了防止数据库被过度的浪费,我们就需要给它雇⼀ 个“助理”了,这个助理就是缓存系统。
加入缓存后的程序:
这样改造之后,所有的程序不会直接调用数据库,而是会先调用缓存,当缓存中有数据时会直接返回, 当缓存中没有数据时才去查询数据库,这样就大大的降低了数据库的压力,并加速了程序的响应速度。
相比于数据库而言,缓存的操作性能更高,缓存性能高的主要原因有以下几个:
缓存大致可以分为两⼤类: 本地缓存、分布式缓存
本地缓存常见使用:Spring Cache、Mybatis的缓存等。
本地缓存也叫单机缓存,也就是说可以应用在单机环境下的缓存。所谓的单机环境是指将服务部署到一台服务器上。
本地缓存的特征是只适⽤于当前系统。
举个栗子:本地缓存相当于每家企业的公司规定⼀样,不同的公司规定也是不同的,比如上班时间,不同的公司上班时间规定也是不同的,对于企事业单位来说⼀般要求 9:00-17:00 上班,而对于酒吧来说,这个时间就完全不适合了。
分布式缓存的常见使用:Redis和Memcached(已退出历史舞台),Redis属于分布式缓存的一种。
分布式缓存是指可以应用在分布式系统中的缓存。所谓的分布式系统是指将⼀套服务器部署到多台服务器,并且通过负载分发将用户的请求按照⼀定的规则分发到不同服务器。
举个栗子:分布式缓存相当于适用于所有公司的规定,比如无论是任何公司都不能偷税漏税,不能做违反法律的事情,这种情况就和分布式缓存很像,适用于所有的系统。 比如我们在分布式系统中的服务器 A 中存储了⼀个缓存 key=xiaoming,那么在服务器 B 中也可以读取 到 key=xiaoming 的数据,这样情况就是分布式缓存的作用。
在 Spring Boot 项目,可以直接使用 Spring 的内置 Cache(本地缓存),只需要完成以下三个步骤就可以正常使用了:
开启缓存只需要在启动类上添加如下代码:
@SpringBootApplication
@EnableCaching # 开启缓存功能
public class BiteApplication {
public static void main(String[] args) {
SpringApplication.run(BiteApplication.class, args);
}
}
在 Service 层增加三个缓存操作的方法:添加缓存、修改缓存、删除缓存,示例代码如下:
import com.example.bittemplate.pojo.UserDO;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@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;
}
/**
* 修改⽤户信息
*/
@CachePut(cacheNames = "getuser", key = "#id")
public UserDO updateUser(int id, String name) {
UserDO userDO = new UserDO();
userDO.setId(id);
userDO.setName(name);
return userDO;
}
/**
* 删除⽤户信息
*/
@CacheEvict(cacheNames = "getuser", key = "#id")
public boolean delUser(int id) {
return true;
}
}
编写触发代码,在 controller 添加如下代码:
import com.example.bite.pojo.UserDO;
import com.example.bite.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@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);
}
}
以上步骤执行完之后,可以使用 Postman 模拟调用来查看缓存。
使用以下命令,直接将 redis 安装到 linux 服务器:
yum -y install redis
redis-server /etc/redis.conf &
使用以下命令启动 redis 客户端:
redis-cli![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)
redis-cli shutdown
先关闭 redis 服务,再使用 redis-server /etc/redis.conf &
启动 redis 服务。Redis图形管理工具: Another Redis Desktop Manager
Redis管理工具无法连接的可能原因:
String——字符串类型(常用)
Hash——字典类型(常用)
List——列表类型
Set——集合类型
ZSet——有序集合类型
字符串类型(Simple Dynamic Strings 简称 SDS),译为:简单动态字符串,它是以键值对 keyvalue 的形式进⾏存储的,根据 key 来存储和获取 value 值,它的使用相对来说⽐较简单,但在实际项目中应非常⼴泛。
127.0.0.1:6379> set k1 v1 # 添加数据
OK
127.0.0.1:6379> get k1 # 查询数据
"v1"
127.0.0.1:6379> strlen k1 # 查询字符串的⻓度
(integer) 2
127.0.0.1:6379> set k1 v1 ex 1000 # 设置 k1 1000s 后过期(删除) exceed缩写
OK
字符串的常见使用场景:
假如我们使⽤字典类型来存储⼀篇⽂章的详情信息,存储结构如下图所示:
127.0.0.1:6379> hset myhash key1 value1 # 添加数据
(integer) 1
127.0.0.1:6379> hget myhash key1 # 查询数据
"value1"
列表类型 (List) 是⼀个使用链表结构存储的有序结构,它的元素插入会按照先后顺序存储到链表结构中,因此它的元素操作 (插入和删除) 时间复杂度为 O(1),所以相对来说速度还是比较快的,但它的查询时间复杂度为 O(n),因此查询可能会比较慢。
127.0.0.1:6379> lpush list 1 2 3 # 添加数据
(integer) 3
127.0.0.1:6379> lpop list # 获取并删除列表的第⼀个元素
1
列表的典型使⽤场景有以下两个:
集合类型 (Set) 是⼀个无序并唯⼀的键值集合。
127.0.0.1:6379> sadd myset v1 v2 v3 # 添加数据
(integer) 3
127.0.0.1:6379> smembers myset # 查询集合中的所有数据
1) "v1"
2) "v3"
3) "v2"
集合类型的经典使⽤场景如下:
微博关注我的人和我关注的人都适合用集合存储,可以保证人员不会重复;
中奖人信息也适合用集合类型存储,这样可以保证⼀个人不会重复中奖。
集合类型(Set)和列表类型(List)的区别如下:
有序集合类型 (Sorted Set) 相比于集合类型多了⼀个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,⼀个是有序结合的元素值,⼀个是排序值。有序集合的存储元素值也是不能重复的,但分值是可以重复的。
当我们把学生的成绩存储在有序集合中时,它的存储结构如下图所示:
127.0.0.1:6379> zadd zset1 3 golang 4 sql 1 redis # 添加数据
(integer) 3
127.0.0.1:6379> zrange zset 0 -1 # 查询所有数据,从0开始,-1是个特殊的值表示查询所有
1) "redis"
2) "mysql"
3) "java"
有序集合的经典使用场景如下:
所谓的持久化就是将数据从内存保存到磁盘的过程,它的目的就是为了防止数据丢失。因为内存中的数据在服务器重启之后就会丢失,而磁盘的数据则不会,因此为了系统的稳定起见,我们需要将数据进行持久化。
同时持久化功能⼜是 Redis 和 Memcached 最主要的区别之⼀,因为 Redis ⽀持持久化⽽ Memcached 不⽀持。
Redis 持久化的方式有以下 3 种:
1️⃣快照方式(RDB, Redis DataBase):将某⼀个时刻的内存数据,以⼆进制的方式写入磁盘;
2️⃣文件追加方式(AOF, Append Only File):记录所有的操作命令,并以文本的形式追加到文件中;
3️⃣混合持久化方式:Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,⼜能减低数据丢失的风险。
持久化策略设置:
混合持久化:可以在 redis-cli
命令行中执行 config set aof-use-rdb-preamble yes
来开启混合持久化,当开启混合持久化时 Redis 就以混合持久化方式来作为持久化策略;
AOF:当没有开启混合持久化的情况下,使用 config set appendonly yes
来开启 AOF 持久化的策略;
RDB:当 AOF 和混合持久化都没开启的情况下默认会是 RDB 持久化的方式。
优点
缺点
优点
缺点
优点
缺点
Spring Data Redis(Access+Driver)
spring.redis.database=0
spring.redis.port=6379
spring.redis.host=82.157.146.10
#可省略
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=1ms
spring.redis.lettuce.shutdown-timeout=100ms
了解即可
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class RedisController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 在 redis 存储数据
@RequestMapping("/setrs")
public String setRedis(String name, String value) {
stringRedisTemplate.opsForValue().set(name, value,30, TimeUnit.SECONDS);
return "Set redis success.";
}
// 读取 redis 中的数据
@RequestMapping("/getrs")
public String getRedis(String name) {
Object valObj = stringRedisTemplate.opsForValue().get(name);
if (valObj != null) return valObj.toString();
return "Null";
}
}
相关注解
@EnableCaching:开启全局注解缓存。
@Cacheable:查询/添加操作,判断Redis是否有缓存,如果没有那么就把当前放啊返回值存到Redis;如果存在缓存直接返回。
@CachePut:修改操作,将方法返回值更新到Redis缓存中。
@CacheEvict:删除操作,将Redis中的对应缓存删除。
1.开启全局注解缓存
package com.example.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
2.实现Redis的增删查改
package com.example.redis.controller;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RedisController {
/**
* Cacheable 查询后 有结果就返回结果
* 没有结果就 add一个缓存,value为返回值
* @param name 姓名 tel 电话
* @key 使用#传递参数,和下方参数相同,可以为多个,注意分隔方式
*/
@RequestMapping("/get-and-add")
@Cacheable(value = "cache", key = "#name+'-'+#tel")
public String getAndAdd(String name, String tel) {
System.out.println("执行了 getAndAdd");
//非空判断
if (!StringUtils.hasLength(name) && !StringUtils.hasLength(tel)) {
return "请先输入name和age";
}
//返回的内容即为这个key的value值
return "name" + name + " | age" + tel;
}
/**
* CachePut 通过传入的name和value定位需要修改的对象
* 修改后的结果为 返回值
*/
@RequestMapping("/put")
@CachePut(value = "cache", key = "#name+'-'+#tel")
public String put(String name, String tel) {
System.out.println("执行了 put");
return "[name=="+name+"#tel=="+tel+"]";
}
/**
* CacheEvict 删除这个对象
*/
@RequestMapping("/del")
@CacheEvict(value = "cache", key = "#name+'-'+#tel")
public void del(String name, String tel) {
System.out.println("执行了 del");
}
spring:
#Redis相关配置
redis:
database: 0
host: 120.53.20.213 #Redis的地址
port: 6379 #端口号,6379为默认的端口号
#Session相关配置
session:
store-type: redis #存放在 Redis 中,这样就不会存储到内存中了
timeout: 1800
redis:
flush-mode: on_save
namespace: spring:session #放置的位置,相当于文件夹
其实操作和我们之前的操作一模一样。
设置默认保存位置为 Redis 后,框架会自动帮我们存储到 Redis 中,其实也就实现了持久化。
package com.example.redis.controller;
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("redis-session")
public class RedisSessionController {
private final static String SESSION_KEY = "USER_SESSION_KEY";//设置Key值
@RequestMapping("/login")
public String login(HttpSession session) {
//会自动存储到Redis中
session.setAttribute(SESSION_KEY, "liMing");
return "登录成功";
}
@RequestMapping("get")
public String get(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
//从Redis中获取用户
Object user = session.getAttribute(SESSION_KEY);
if (user != null) {
return user.toString();
}
}
return "暂无Session,未登录";
}
}
在上文中已经提到
1.什么是缓存?
2.缓存的优点和分类?
3.Redis五大基础数据类型?
4.持久化的三种方式?三种方式的优缺点?
缓存雪崩是指在短时间内,有大量缓存同时过期,导致⼤量的请求直接查询数据库,从⽽对数据库造成 了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。
正常情况下系统的执行流程如下图所示:
缓存雪崩的执行流程,如下图所示:
以上对比图可以看出缓存雪崩对系统造成的影响,那如何解决缓存雪崩的问题? 主要有 加锁排队、随机化过期时间、设置⼆级缓存。
加锁排队
加锁排队可以起到缓冲的作用,防止大量的请求同时操作数据库,但它的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了⼀部分用户体验。
随机化过期时间
为了避免缓存同时过期,可在设置缓存时添加随机时间,这样就可以极大的避免⼤量的缓存同时失效。 示例代码如下:
// 缓存原本的失效时间
int exTime = 10 * 60;
// 随机数⽣成类
Random random = new Random();
// 缓存设置
jedis.setex(cacheKey, exTime+random.nextInt(1000) , value);
设置二级缓存
⼆级缓存指的是除了 Redis 本身的缓存,再设置⼀层缓存,当 Redis 失效之后,先去查询⼆级缓存。
例如可以设置⼀个本地缓存,在 Redis 缓存失效的时候先去查询本地缓存而非查询数据库。
缓存穿透是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。
缓存穿透执行流程如下图所示:(其中红⾊路径表示缓存穿透的执行路径,可以看出缓存穿透会给数据库造成很大的压力)
解决方案:缓存空结果
我们可以把每次从数据库查询的数据都保存到缓存中,为了提⾼前台用户的使用体验 (解决长时间内查询不到任何信息的情况),我们可以将空结果的缓存时间设置的短一些,例如 3-5 分钟。
缓存击穿指的是某个热点缓存,在某⼀时刻恰好失效了,然后此时刚好有⼤量的并发请求,此时这些请求将会给数据库造成巨大的压力,这种情况就叫做缓存击穿。
缓存击穿的执行流程如下图所示:
解决方案有:加锁排队、设置永不过期
加锁排队
此处理⽅式和缓存雪崩加锁排队的⽅法类似,都是在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力
设置永不过期
对于某些热点缓存,我们可以设置永不过期,这样就能保证缓存的稳定性,但需要注意在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。
首先来说,缓存预热并不是⼀个问题,而是使用缓存时的⼀个优化方案,它可以提高前台用户的使用体验。
缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。
缓存预热的执行流程,如下图所示:
缓存预热的实现思路有以下三种:
随着业务的不断发展,单机 Redis 的性能已经不能满足我们的需求了,此时我们需要将单机 Redis 扩 展为多机服务,Redis 多机服务主要包含以下 3 个内容:
下⾯我们分别来看这 3 部分的内容。
主从同步 (主从复制) 是 Redis 高可用服务的基石,也是多机运行中最基础的⼀个。
我们把主要存储数据的节点叫做主节点 (master),把其他通过复制主节点数据的副本节点叫做从节点 (slave),如下图所示:
在 Redis 中⼀个主节点可以拥有多个从节点,⼀个从节点也可以是其他服务器的主节点,如下图所示:
在 Redis 运行过程中,我们可以使⽤ replicaof host port
命令,把自己设置为目标 IP 的从服务器,执行命令如下:
127.0.0.1:6379> replicaof 127.0.0.1 6380
OK
如果主服务设置了密码,需要在从服务器输入主服务器的密码,使⽤ config set masterauth
主服务密码命令的方式,例如:
127.0.0.1:6377> config set masterauth pwd654321
OK
主从同步具有以下 3 个优点:
主从同步的缺点:
这种模式本身存在⼀个致命的问题,当主节点奔溃之后,需要人工干预才能恢复 Redis 的正常使用。
解决这一问题我们可以使用哨兵模式
假如晚上发生了主从服务器宕机的情况,尤其是在主从服务器比较多的情况下,如果需要人工恢复,那么需要的时间和难度是很大的,因此我们需要⼀个自动的⼯具——Redis Sentinel (哨兵模式) 来把手动的过程变成自动的,让 Redis 拥有自动容灾恢复 (failover) 的能力。
也就是说:使用哨兵模式可以用来监控主从同步服务器节点,并在主从服务器出现问题的时候实现自动容灾恢复。
哨兵模式如下所示(⼩贴⼠:Redis Sentinel 的最⼩分配单位是⼀主⼀从。):
哨兵的⼯作原理是,首先每个 Sentinel 会以每秒钟 1 次的频率,向已知的主服务器、从服务器和以及其他 Sentinel 实例,发送⼀个 PING 命令。
如果最后⼀次有效回复 PING 命令的时间超过 down-after-milliseconds
所配置的值 (默认 30s),那么这个实例会被 Sentinel 标记为主观下线。
如果⼀个主服务器被标记为主观下线,那么正在监视这个主服务器的所有 Sentinel 节点,要以每秒 1 次的频率确认主服务器的确进入了主观下线状态。
如果有足够数量 (quorum 配置值) 的 Sentinel 在指定的时间范围内同意这⼀判断,那么这个主服务器被标记为客观下线。此时所有的 Sentinel 会按照规则协商自动选出新的主节点。
注意:⼀个有效的 PING 回复可以是:
+PONG
、-LOADING
或者-MASTERDOWN
。如果返回值非以上三种回复,或者在指定时间内没有回复 PING 命令, 那么 Sentinel 认为服务器返回的回复无效 (non-valid )。
Redis主从同步 + 哨兵模式 并不是 Redis 多机运行最完美的解决方案,集群模式才是现在常用的
Redis 集群(Redis Cluster)是 Redis 多机运行最完美的终极方案,它是 Redis 3.0 之后推出的服务,它的出现可以让我们完全抛弃主从同步和哨兵模式来实现 Redis 多机运行。
Redis Cluster 是无代理模式去中心化的运行模式,客户端发送的绝大数命令会直接交给相关节点执行,这样大部分情况请求命令无需转发,或仅转发⼀次的情况下就能完成请求与响应,所以集群单个节点的性能与单机 Redis 服务器的性能是非常接近的,因此在理论情况下,当水平扩展⼀倍的主节点就相当于请求处理的性能也提高了⼀倍,所以 Redis Cluster 的性能是非常高的。
Redis Cluster 架构图如下所示:
如果⼀个主服务器被标记为主观下线,那么正在监视这个主服务器的所有 Sentinel 节点,要以每秒 1 次的频率确认主服务器的确进入了主观下线状态。
如果有足够数量 (quorum 配置值) 的 Sentinel 在指定的时间范围内同意这⼀判断,那么这个主服务器被标记为客观下线。此时所有的 Sentinel 会按照规则协商自动选出新的主节点。
注意:⼀个有效的 PING 回复可以是:
+PONG
、-LOADING
或者-MASTERDOWN
。如果返回值非以上三种回复,或者在指定时间内没有回复 PING 命令, 那么 Sentinel 认为服务器返回的回复无效 (non-valid )。
Redis主从同步 + 哨兵模式 并不是 Redis 多机运行最完美的解决方案,集群模式才是现在常用的
Redis 集群(Redis Cluster)是 Redis 多机运行最完美的终极方案,它是 Redis 3.0 之后推出的服务,它的出现可以让我们完全抛弃主从同步和哨兵模式来实现 Redis 多机运行。
Redis Cluster 是无代理模式去中心化的运行模式,客户端发送的绝大数命令会直接交给相关节点执行,这样大部分情况请求命令无需转发,或仅转发⼀次的情况下就能完成请求与响应,所以集群单个节点的性能与单机 Redis 服务器的性能是非常接近的,因此在理论情况下,当水平扩展⼀倍的主节点就相当于请求处理的性能也提高了⼀倍,所以 Redis Cluster 的性能是非常高的。
Redis Cluster 架构图如下所示:
从上图可以看出 Redis 的主从同步只能有⼀个主节点,而 Redis Cluster 可以拥有无数个主从节点,因此 Redis Cluster 拥有更强大的平行扩展能力,也就是说当 Redis Cluster 拥有两个主从节点时,从理论上来讲 Redis 的性能相比于单机服务来说性能提升了 2 倍。