Redis学习笔记

学习视频:狂神说Java 学习时间:2021年2月2日

基础知识

Redis默认有16个数据库,默认使用第0个数据库。默认端口为6379;

1
2
select 3 #切换数据库
dbsize #数据库大小
1
2
flushdb #清空当前数据库的信息
flushall #清空所有数据库中的信息

Redis是单线程的

Redis是基于内存操作的,CPU不是其性能瓶颈。Redis的瓶颈是机器的内存和网络带宽,所以使用单线程。

Redis是C语言写的。性能完全不比memcache差。

下载安装

window下载安装

image-20211215194017491

Linux下载安装

基本命令

1
2
3
4
5
6
7
8
keys * # 列出所有的键
set name hongyi # 设置键值对
get name # 获取键为name的值
exists name # 判断是否存在键name
move name 1 # 从当前数据库移除键name
expire name 10 # 设置键name十秒后过期
ttl name # 查看键name的剩余存活时间
type name # 查看键name的值的类型

五大基本数据类型

1.String

基本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
append key "hello" # 向键key的值后追加字符串,如果当前key不存在则新建,相当于set
strlen key # 获取字符串的长度
#####################################################
incr key # key的值加1
decr key # key的值减1
incrby key 10 # key的值加10
decrby key 10
#####################################################
getrange key 0 3 # 取子串 [0,3]
getrange key 0 -1 # 得到整个字符串
setrange key 1 hello # 替换指定位置开始的字符串
#####################################################
#setex (set with expire) 设置过期时间
#setnx (set if not exsit) 不存在则设置 在分布式锁中常常使用
setex key 10 "hello" # 设置key-hello的过期时间为10s
setnx key "hello
#####################################################
mset k1 v1 k2 v2 k3 v3 # 一次性设置键值对
mget k1 k2 k3 # 一次性获取值
msetnx # 原子性操作
msetex
#####################################################
set user:1 {name:hongyi,age:3} # 设置一个user:1对象key,值为json字符串来保存一个对象
mset user:1:name hongyi user:1:age 12 # 同上
mget user:1:name user:1:age
#####################################################
getset db redis # 先设置再查出显示 若没有则新建,若有则覆盖

使用场景

value除了字符串,数字也可以。使用场景有计数器,多单位的统计数量 uid:234123:follow 322

2.List

3.Set

基本指令

set集合中无序不可重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#####################################################
sadd myset "hello" # 向myset集合中添加值hello
smembers myset # 查看集合中的所有值
sismember myset hello # 查询是否存在值hello
#####################################################
scard myset # 获取set集合的值的个数
srem myset "hello" # 移除元素
#####################################################
srandmember myset 2 # 随机获取集合中的两个元素,没有2则默认抽取1个
#####################################################
spop myset # 随机移除元素
#####################################################
sdiff myset1 myset2 # 求差集
sinter myset1 myset2 # 求交集
sunion myset1 myset2 # 求并集

使用场景

共同关注

4.Hash

key-Map集合,逻辑形式:key- 或 key-map

基本指令

1
2
3
4
5
6
7
8
9
10
#####################################################
hset myhash f1 hongyi # myhash为key,value为键值对f1-hongyi
hget myhash f1 # 获取myhash的值f1对应的值
hmset myhash f1 v1 f2 v2 # 为myhash设置多个键值对
hgetall myhash # 获取myhash存储的所有键值对
#####################################################
hdel myhash f1 # 删除指定的键值对
hlen myhash # 获取myhash里的键值对个数
#####################################################
hmset user:1 name hongyi age 12 # 对象的存储

使用场景

hash用于经常变动的数据。更加适用于对象的存储。

5.Zset

有序集合,在set的基础上增加了一个排序

三大特殊数据类型

geospatial

hyperloglog

bitmaps

Redis事务

概念

Redis事务本质:一组命令的集合。一个事务中的所有命令都会被序列化,会按照顺序执行。一次性,顺序性,排他性。

Redis单条命令是保证原子性的,而事务是不保证原子性的。

Redis事务没有隔离级别的概念。

所有的命令在事务中并没有被执行。只有发起事务执行命令的时候才会被执行。

Redis事务的三个阶段:

  • 开启事务 multi
  • 命令入队 …
  • 执行事务 exec

锁:Redis能够实现乐观锁。

基本指令

正常执行事务

1
2
3
4
5
6
7
multi # 开启事务
# 命令入队
set k1 v1 # 返回Queued
set k1 v2
get k1
set k3 v3
exec # 执行事务 会按照顺序输出

放弃事务

1
2
3
4
multi
set k1 v1
set k2 v2
discard # 放弃事务 以上所有指令都不会被执行

编译型异常

事务中所有的命令都不会执行

1
2
3
4
5
6
multi
set k1 v1
set k2 v2
getset k3 # 此条语句有错
set k4 v4
exec # 报错 所有指令都不会被执行

运行时异常

除异常指令,其它指令都可以正常执行(没有原子性),异常指令抛出异常

1
2
3
4
5
6
set k1 "v1"
multi
incr k1 # 字符串不能自增
set k2 v3
get k2
exec # 正常输出,除了第三行指令

监控

悲观锁

  • 无论什么时候都加锁

乐观锁

  • 只在更新数据的时候,判断在此期间是否有人修改过这个数据
  • 获取version
  • 更新的时候比较version

监控测试

1
2
3
4
5
6
7
set money 100
set out 0
watch money # 监视money 可作为乐观锁
multi
decrby money 20
incrby out 20
exec # 事务正常结束

以上是正常情况。

线程1

1
2
3
4
5
6
7
8
9
10
set money 100
set out 0
watch money # 监视money
multi
decrby money 20
incrby out 20
exec # 等待线程二执行完毕后再执行该行指令
#返回nil 执行失败
unwatch # 放弃监视,解锁 再次监视,并执行以上操作即可
...

线程2

1
2
get money
set money 1000

Jredis

通过Jedis操作Redis

什么是Jedis

Jedis是Redis官方推荐的Java连接开发工具,是使用Java操作Redis的中间件,相当于用Java操作数据库的JDBC

1.导入依赖

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

2.编码测试

(1)连接测试

1
2
3
4
5
6
7
8
9
10
public class TestPing {
public static void main(String[] args) {
// new Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
// 所有的命令就是之前的命令
System.out.println(jedis.ping());
//关闭连接
jedis.close();
}
}

(2)部分指令测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestPing {
public static void main(String[] args) {
// 1.new Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
// 所有的命令就是之前的命令
System.out.println(jedis.ping());
System.out.println(jedis.flushDB());
System.out.println(jedis.set("k1", "v1"));
System.out.println(jedis.get("k1"));
//关闭连接
jedis.close();
}
}

(3)事务控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestPing {
public static void main(String[] args) {
// 1.new Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
// 连接数据库
jedis.ping();

JSONObject jsonObject = new JSONObject();
jsonObject.put("name","hongyi");
jsonObject.put("age","32");
// 事务控制
Transaction multi = jedis.multi();
String s = jsonObject.toJSONString();
try {
multi.set("user1",s);
multi.set("user2",s);
multi.exec();
} catch (Exception e) {
// 放弃事务
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
//关闭连接
jedis.close();
}
}
}

SpringBoot整合Redis

1.maven依赖

image-20211215194101140

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.4.3</version>
</dependency>

2.说明

SpringBoot 2.x之后,原来使用的Jedis被替换为了Lettuce

Jedis:采用的直连,多个线程操作的话是不安全的, BIO

Lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的问题,NIO

3.RedisTemplate源码

SpringBoot所有的配置类,都有一个自动配置类 RedisAutoConfiguration.class

自动配置类又会绑定一个properties配置文件 RedisProperties.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
//我们可以自定义一个redisTemplate来替换这个默认的
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的redisTemplate类没有序列化,而Redis对象需要序列化
//两个泛型都是<Object, Object>,后面使用需要强制转换为<String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
//由于String类型常用,所以单独提出来了一个Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

4.整合测试

(1)导入依赖

(2)配置连接

1
2
3
# 配置Redis
spring.redis.host=localhost
spring.redis.port=6379

(3)测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;

@Test
void contextLoads() {
//opsForValue 操作字符串 类似于String
//opsForList 操作List 类似于List ...
redisTemplate.opsForValue().set("k1","v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
//获取连接 很少用
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
}

}

5.自定义RedisTemplate

(1)json依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.codehaus.jackson/jackson-mapper-asl -->
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>

(2)测试

1
2
3
4
5
6
7
public void test() throws IOException {
User user = new User("呵呵", 12);
//将User对象序列化成json字符串
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}

直接传递对象会报错,利用jdk方式序列化后可正常传输

1
2
3
4
5
//将其序列化
public class User implements Serializable {
private String name;
private int age;
}

(3)自定义RedisTemplate类(略)

Redis持久化

redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失,所以Redis提供了持久化功能。

RDB(Redis Database)

什么是RDB

image-20211215194118514

在指定的时间间隔内将内存的数据集快照写入磁盘,也就是snapshot快照,它恢复时是将快照文件直接读入内存里。

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化的文件。整个过程中,父(主)进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是特别敏感,那么RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。Redis默认的就是采用RDB方式(redis.conf),一般情况下不需要修改这个配置。

在生产环境有时会将这个dump.rdb文件进行备份。

==redis保存的持久化文件是dump.rdb==

image-20211215194130117

redis.conf里设置save规则:

1
save 60 5 # 如果60s内操作了5次以上操作,则会生成dump.rdb文件

RDB触发机制

1.save的规则满足的情况下

2.执行flushall命令

3.退出redis时

备份就会自动生成dump.rdb文件

几乎Redis默认的rdb配置就够用了。

优缺点

1.优点

(1)适合大规模的数据恢复

(2)对数据的完整性要求不高

2.缺点

(1)需要一定的时间间隔进行操作。如果Redis意外宕机了,那么最后一次的数据就会丢失,例如在59s时宕机,则这0~59秒间产生的数据都会丢失。

(2)fork子进程的时候会占用一定的内存空间

AOF(Append Only File)

什么是AOF

AOF将我们的所有命令都记录下来。

image-20211215194151429

以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件,不许修改文件,Redis重启后,会根据日志文件的内容将写指令从前到后依次执行一次完成数据的恢复工作。默认是不开启的,需手动开启(改为yes即可)。save规则是每秒生成(每秒都在记录写操作)。

1
appendonly no

==AOF保存的是appendonly.aof文件==

如果aof文件被破坏了,这时候redis将无法启动,需要修复aof文件。redis提供了工具redis-check-aof --fix appendonly.aof 进行修复。

触发机制

重启Redis。

重写规则

aof默认就是文件的无限追加,文件会越来越大。

1
2
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

如果aof文件大于64mb,则fork一个新进程将我们的文件进行重写。

优缺点

1
2
3
appendfsync always #每次修改都会同步,消耗性能
appendfsync everysec #每秒执行一次同步
appendfsync no #不执行同步,这个时候操作系统自己同步数据,速度最快

1.优点

(1)每一次修改都同步,文件完整性会更好。

(2)每秒同步一次,可能会丢失一秒的数据。

(3)从不同步则效率最高

2.缺点

(1)相对于数据文件来说,aof远大于rdb,且修复的速度小于rdb

(2)aof运行效率也比rdb慢

Redis发布订阅

Redis发布订阅(pub and sub)是一种==消息通信模式==:发送者发送消息,订阅者接受消息。

Redis客户端可以订阅任意数量的频道。

image-20211215194211975

指令

image-20211215194229754

发布订阅测试

订阅频道

1
2
3
4
5
6
redis 127.0.0.1:6379> SUBSCRIBE runoobChat #订阅一个频道,没有则创建

Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1

发布消息

1
2
3
4
5
6
7
8
9
10
11
12
redis 127.0.0.1:6379> PUBLISH runoobChat "Redis PUBLISH test" #发布消息到指定频道
(integer) 1
redis 127.0.0.1:6379> PUBLISH runoobChat "Learn redis by runoob.com"
(integer) 1
# 订阅者的客户端会显示如下消息
1) "message"
2) "runoobChat"
3) "Redis PUBLISH test"

1) "message"
2) "runoobChat"
3) "Learn redis by runoob.com"

使用场景

1.实时消息系统

2.订阅/关注系统

3.实时聊天

稍微复杂的场景会使用消息中间件MQ

Redis主从复制

概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。主节点以写为主,从节点以读为主。

默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。最低配置一主二从。

作用

1.数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

2.故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3.负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4.读写分离:可以用于实现读写分离,主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量;

5.高可用基石(集群):除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

环境配置

1.只配置从库,不用配置主库。查看库的信息

1
2
3
4
info replication # 查看当前库的信息
#replication
role:master # 角色
connected_slaves:0 # 从库的数量

2.从机配置

1
SLAVEOF 127.0.0.1 6379 #以127.0.0.1:6379的主机为主库

image-20211215194243160

真实的配置应在配置文件中进行,这样才是永久的。采用指令的方式是暂时的配置,如果从机断开再重启,则又会变回主机,丢失以前所属主机的信息。

一些细节

1.主机可以写,从机只能读。从机一旦连接上主机,主机的所有信息都能被从机自动保存。

image-20211215194252276

2.主机断开连接,从机依旧连接到主机的。

3.层层链路:上一个M链接下一个S。如果主机断开了连接,可以使用SLAVEOF no one让自己成为主机。

如果主机修复了,只能手动配置主从关系。自动化->哨兵模式

哨兵模式

概述

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

image-20211215194304034

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

image-20211215194319452

哨兵测试

1.对sentinel.conf进行核心配置

1
2
#sentinel monitor 被监控的名称(自己设置) 监控主机 端口 1
sentinel monitor myredis 127.0.0.1 6379 1

最后面的1代表主机宕机,让从机投票,看让谁接替成为主机,票数最多的成为主机

2.启动哨兵

1
redis-sentinel kconfig/sentinel.conf

优点

1.哨兵集群基于主从复制模式,所有的主从配置的优点,它都具有。

2.系统可用性更好

3.哨兵模式是主从模式的升级,是其自动的实现,更加健壮

缺点

1.在线扩容非常麻烦

2.哨兵模式的配置复杂

Redis缓存穿透和雪崩

服务的高可用问题

缓存穿透

1.概念

用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,查询失败。当用户很多的时候,缓存都没有命中,于是都去请求持久层数据库。这会给持久层数据库造成很大压力,这时候相当于出现了缓存穿透。

2.解决方案

(1)布隆过滤器 Bloom Filter

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先校验,不符合则丢弃,从而避免了对底层存储系统的压力。

使用场景

原本有10亿个号码,现在又来了10万个号码,要快速准确判断这10万个号码是否在10亿个号码库中?

解决办法一:将10亿个号码存入数据库中,进行数据库查询,准确性有了,但是速度会比较慢。

解决办法二:将10亿号码放入内存中,比如Redis缓存中,这里我们算一下占用内存大小:10亿*8字节=8GB,通过内存查询,准确性和速度都有了,但是大约8gb的内存空间,挺浪费内存空间的。

接触过爬虫的,应该有这么一个需求,需要爬虫的网站千千万万,对于一个新的网站url,我们如何判断这个url我们是否已经爬过了?解决办法还是上面的两种,很显然,都不太好。

同理还有垃圾邮箱的过滤。

那么对于类似这种,大数据量集合,如何准确快速的判断某个数据是否在大数据量集合中,并且不占用内存,布隆过滤器应运而生了。

原理

核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k。

image-20211215194332680

以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。

添加元素

将要添加的元素给k个哈希函数;得到对应于位数组上的k个位置;将这k个位置设为1。

查询元素

将要查询的元素给k个哈希函数;得到对应于位数组上的k个位置;如果k个位置有一个为0,则肯定不在集合中;如果k个位置全部为1,则可能在集合中

优缺点

优点:优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。

缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。

实现

Redis 实现布隆过滤器的底层就是通过 bitmap 这种数据结构,至于如何实现,这里就不重复造轮子了,介绍业界比较好用的一个客户端工具Redisson。Redisson 是用于在 Java 程序中操作 Redis 的库,利用Redisson 我们可以在程序中轻松地使用 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
25
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonBloomFilter {

public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.14.104:6379");
config.useSingleServer().setPassword("123");
//构造Redisson
RedissonClient redisson = Redisson.create(config);

RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
//将号码10086插入到布隆过滤器中
bloomFilter.add("10086");

//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("123456"));//false
System.out.println(bloomFilter.contains("10086"));//true
}
}
(2)缓存空对象

当存储层不命中时,即使返回的空对象也将其存储起来,同时会设置一个过期时间,之后访问这个数据将会从缓存中获取,保护了后端数据源。

存在的问题:

1.如果空值能够被缓存起来,这就意味着缓存需要更多的空间来存储更多的键。

2.即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿

1.概念

缓存击穿是一个key非常热点,在不停地扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

这类数据一般时热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库压力瞬间过大。

2.解决方案

(1)设置热点数据永不过期
(2)加互斥锁

使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁上,对其的考验很大。

缓存雪崩

1.概念

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

2.解决方案

(1)redis高可用:搭建redis服务集群

(2)限流降级:在缓存失效后,通过加锁或队列来控制读数据库写缓存的线程数量。

(3)数据预热:在正式部署之前,把可能的数据预先访问一遍,这样部分可能大量访问的数据会加载到缓存中。在即将发生并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。