Redis学习笔记

image-20230707201252111

Redis学习笔记

  • 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
  • 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

redis安装教程

1.安装redis依赖环境

1
yum install -y gcc tcl

2.将tar.gz包放入对应目录中**/usr/local/src**,并解压

1
tar -zxvf redis-6.2.7.tar.gz

3.到解压目录中执行编译代码

1
cd redis-6.2.7
1
make && make install

前台启动redis

1
redis-server #直接启动

redis的配置

进入以下文件配置

1
vim /usr/local/src/redis-6.2.7/redis.conf

image-20230609002046055

这样可以设置在后台运行,并设置访问密码

运行并指定配置文件

1
cd /usr/local/src/redis-6.2.7 #进入启动目录
1
redis-server redis.conf #启动并指定配置文件

redis开机自动

1.建立配置文件,将redis写入系统进程

image-20230609104428208

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server
/usr/local/src/redis-6.2.7/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

如果报错一定要注意ExecStart里面地址是否错误!!!!

2.重新加载服务

1
systemctl daemon-reload

3.启动redis服务

1
2
3
systemctl start redis

systemctl status redis #查看redis是否运行

4.实现开机自启动

1
systemctl enable redis

redis命令行客户端

1
cd /usr/local/bin/
1
redis-cil -h [120.0.0.1] -p [6379] -a [137125]
  • -h 连接的Ip地址
  • -p 链接的端口号
  • -a 链接的密码
1
2
3
set name fanfan
get name
SELECT 0 #选择0号库

redis帮助文档

1
https://redis.io/commands/
1
help [指令] #展现提示 

redis常用命令

image-20230609160202510

1
2
expire age 20 #20为秒
ttl age #返回值为-1则永久有效,返回值为-2则已删除

String常用指令

image-20230609161022653

Hash常用指令

image-20230609163650354

List常用指令

image-20230609172326658

Set常用指令

image-20230609193804310

SortedSet常用指令

image-20230609195218583

redis的java客户端——jedis

点击查看jedis官网

image-20230610190853358

image-20230610214606913

jedis依赖

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.0</version>
</dependency>

spring -Data redis使用

1
2
3
4
5
6
7
8
9
<!--redis的依赖默认使用自带版本不然容易报错-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
image-20230611195955922
1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: localhost # Redis 服务器地址,默认值为localhost
port: 6379 # Redis 服务器端口,默认值为6379
password: "" # Redis 服务器的密码,如果没有设置则留空
lettuce:
pool:
max-active: 8 # 最大连接数(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
max-wait: -1ms # 等待连接的最大时间,使用负值表示没有限制

在这里会产生一个问题:插入的key—>person在Redis中变成了\xAC\xED\x00\x05t\x00\x06person,而且数据也是\xED\x00\x05\x74\x00\x06\x66\x61\x6E\x73\x65\x61的java序列化后的格式

1
2
3
4
5
6
7
8
9
10
@Autowired
RedisTemplate redisTemplate;
@Test
void contextLoads() {
String o = (String) redisTemplate.opsForHash().get("big_key:12:person", "city");
redisTemplate.opsForValue().set("person","fansea");
String o3 = (String) redisTemplate.opsForValue().get("person");
System.out.println(o);
System.out.println(o3);
}

序列化问题

序列化常用工具

  1. json字符串转为对象
1
Shop shop = JSONUtil.toBean(shopString, Shop.class);
  1. 对象转化为json字符串
1
String json = JSONUtil.toJsonStr(shop)

方式一,手动序列化反序列化

image-20230707181552933

因为redis中默认的序列化器是jdk序列化器,所以存取数据需要自己序列化和反序列化:

使用stringRedisTemplate可以自动将序列化器转为字符串序列化器,所以需要对存数据value(String)进行转json自己序列化处理,对于取数据则就需要对其自己反序列化

image-20230707181728690

方式二,自定义序列化器

定义配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class RedisConfig {

@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);

// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);

template.afterPropertiesSet();
return template;
}
}

注意需要添加序列化坐标:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.20</version>
</dependency>

主动更新

保持数据库和缓存信息的一致性

image-20230715090430287

缓存穿透

客户端发起的请求在Redis和数据库中都不存在,多线程多次请求到达数据库,有风险将数据库击溃

c5f45358eb82351e3abfc0885d48fce6

image-20230715230026883

缓存穿透解决方案

  1. 未命中则返回数据库查询
  2. 如果再无数据,将key-空字符串返回保存在Redis中,并设置TTL

缓存雪崩

img

缓存中保存的数据同时失效,或者Redis服务宕机,所有请求同时打到数据库造成很大的安全隐患。

image-20230717093957319

简单解决办法:给不同的key的TTL设置随机值

添加多级缓存

多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:

  • 浏览器访问静态资源时,优先读取浏览器本地缓存
  • 访问非静态资源(ajax查询数据)时,访问服务端
  • 请求到达Nginx后,优先读取Nginx本地缓存
  • 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
  • 如果Redis查询未命中,则查询Tomcat
  • 请求进入Tomcat后,优先查询JVM进程缓存
  • 如果JVM进程缓存未命中,则查询数据库

img

可见,多级缓存的关键有两个:

  • 一个是nginx中编写业务,实现nginx本地缓存、RedisTomcat的查询
  • 另一个就是Tomcat中实现JVM进程缓存

原文链接:https://blog.csdn.net/weixin_52223770/article/details/128633716

Caffeine(JVM进程缓存)

缓存使用的基本API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
void testBasicOps() {
// 构建cache对象
Cache<String, String> cache = Caffeine.newBuilder().build();

// 存数据
cache.put("gf", "迪丽热巴");

// 取数据
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);

// 取数据,包含两个参数:
// 参数一:缓存的key
// 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
// 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
String defaultGF = cache.get("defaultGF", key -> {
// 根据key去数据库查询数据
System.out.println("从数据库查询数据");
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}

Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。
Caffeine提供了三种缓存驱逐策略:

  1. 基于容量:设置缓存的数量上限
1
2
3
4
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1) // 设置缓存大小上限为 1
.build();
  1. 基于时间:设置缓存的有效时间
1
2
3
4
5
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存有效期为 10 秒,从最后一次写入开始计时
.expireAfterWrite(Duration.ofSeconds(10))
.build();
  1. 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

缓存击穿

  • 互斥锁
  • 逻辑过期:如果过期把过期数据返回,然后再尝试获取锁,后台进行缓存重建

高并发访问的key失效,多线程访问接口,会在每个线程中同时访问数据库,如果访问时间较长,数据库的工作压力就会导致服务宕机

image-20230717101321755

设置互斥锁

1
2
3
4
5
6
7
8
9
//尝试获取锁
private boolean tryLock(String key){
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
//释放锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}

互斥锁解决方案(保障一致性)

  1. 未查询到缓存中的数据,则先去尝试获取锁,得到锁的线程才能执行查数据库的操作。
  2. 其他线程未得到锁则进行休眠,休眠一段时间,重新去查询Redis缓存,如果未查到,则继续休眠
  3. 当得到锁的线程查询完数据库,则将数据保存在Redis缓存释放锁,并返回数据

逻辑过期解决方案(保障程序流畅性)

此方案针对热点问题,事先需要将数据保存在Redis中,所以在程序里面默认他一直存在

  1. 定义RedisData类封装对象数据过期时间
1
2
3
4
5
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object redisData;
}
  1. 查询Redis中的数据,判断是否过期,如果未过期,直接将数据返回
1
2
3
4
5
6
RedisData redisData = JSONUtil.toBean(shopString, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getRedisData(), Shop.class);

if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
return shop;
}
  1. 如果已经过期,尝试获取锁,并将过期数据返回

  2. 获取到锁则开辟一个新线程去进行缓存重建

1
2
3
4
5
6
7
8
9
10
//创立线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
if (tryLock(lockKey)) {
//线程池
CACHE_REBUILD_EXECUTOR.submit(() ->{
saveShop2Redis(id, 1800L);
unLock(lockKey);
}
);
}

全局id生成器

生成策略:时间戳+redis自增长数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//全局ID生成器
//生成策略:时间戳+redis自增长数
@Component
public class RedisIdWorker {
@Autowired
StringRedisTemplate stringRedisTemplate;

public static final long BEGIN_TIMESTAMP=1672531200L;

public long nextId(String keyPrefix){
//1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
//long类型数能最高保存64
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
String date = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
//2. redis自增
Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3. 拼接返回(timeStamp左移32位相当于将后面的32位置0,再用或运算符拼接increment)
//日期和常自增数拼接保证全局id唯一性
return timeStamp << 32 | increment;
}

}

理解分布式缓存

什么是分布式缓存

分布式缓存:指将应用系统和缓存组件进行分离的缓存机制,这样多个应用系统就可以共享一套缓存数据了,它的特点是共享缓存服务和可集群部署,为缓存系统提供了高可用的运行环境,以及缓存共享的程序运行机制。

本地缓存VS分布式缓存

本地缓存:是应用系统中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持的场景下使用本地缓存较合适;但是,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法共享缓存数据,各应用或集群的各节点都需要维护自己的单独缓存。很显然,这是对内存是一种浪费。

分布式缓存:与应用分离的缓存组件或服务,分布式缓存系统是一个独立的缓存服务,与本地应用隔离,这使得多个应用系统之间可直接的共享缓存数据。目前分布式缓存系统已经成为微服务架构的重要组成部分,活跃在成千上万的应用服务中。但是,目前还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。