redis学习

130 阅读22分钟

一 下载安装包

redis官网: redis.io/

在官网下载的是linux版本的,所以我们需要将redis放到虚拟机里面去解压使用(这里就以centos为例讲解)

二 配置文件

1.安装依赖库(因为redis是使用c语言编写的,所以需要安装gcc来编译执行)

yum install -y gcc tcl

make MALLOC=libc

2.上传安装包并解压
2.1 解压

tar -zxvf redis-6.2.6.tar.gz

切记,软件一定要在/usr/local目录下解压(约定大于配置)

2.2 编译

进入redis目录,执行编译命令

cd redis-7.0.11/

make && make install

2.3 查看

cd /usr/local/bin

image.png

因为这里就是centos环境变量所在地,所以就相当于已经配置好了环境变量

3. 配置redis.conf

image.png

bind 0.0.0.0(允许访问的地址,默认是127.0.0.1,默认会导致只能在本地访问,改为0.0.0.0则可以在任意ip访问)

daemonize yes(守护进程)

requirepass 123456(密码,设置后访问redis必须输入密码)

protected-mode yes(保护模式)

port 6379(redis监听的端口,默认使用6379)

dir .(工作目录,默认是当前目录,日志,持久化等文件都会保存在这个目录)

databases 16(数据库数量,设置为1,代表只使用一个库,默认有16个库,编号为0~15)

maxmemory 512mb(设置redis能够使用的最大内存)

logfile "redis.log"(日志文件,默认为空,不记录日志,那么控制台就会输出日志.如果日志文件是绝对路径,那么输出位置就是绝对路径,如果日志文件是相对路基,那么输出位置就是工作目录dir.)

三 启动redis服务

1.启动redis

redis-server /usr/local/redis/redis.conf

2.验证启动是否成功

ps -ef | grep redis

3. 进入客户端

redis-cli -a 123456 -p 6379 -h 127.0.0.1

-a: 是输入密码

-p: 是端口(因为redis默认6379,所以如果在redis.conf文件配置的也是6379,那么这一步可以省略)

-h: ip地址,因为是本机访问,可以省略

4. 退出客户端
4.1 kill -9 进程号
4.2 在进入客户端的情况下可以使用shutdown命令来退出

四 客户端访问redis

1. 图形化桌面客户端(RedisStudio)
2. linux命令行客户端
2.1 redis常用命令

keys *: 查看所有的key

keys name: 查看key为name的值

del key: 删除key

del key1 key2 key3: 批量删除

exist key: 查看key是否存在

expire key s: 给key设置一个过期时间,时间为s秒

ttl key: 查看key是否过期,如果过期返回-2,如果没有设置返回-1

2.2 String类型命令

set name tom ex 100: 设置key为name,value为tom,过期时间为100秒

mset name tom age 100 gender female: 相比于sex这个可以设置多个值

setex name 100 tom: 设置key为name,value为tom,过期时间为100秒

setnx name tom: 如果key存在返回0,Key不存在返回1

get name: 获取name的值

mget name age gender: 批量获取多个值

incr age: 将age的值+1

decr age: 将age的值-1

redis对象缓存

set user:1 '{"id":"1","name":"tom","age","10"}'

2.3 hash数据类型命令

key field value这是hash的一个结构

hset com:cctv:user name tom 设置key为com:cctv:user,field为name,value为tom

hmset com:cctv:user name tom age 10 批量设置

hget com:cctv:user name 用来获取name的值

hmget com:cctv:user name age 批量获取值

hgetall com:cctv:user 获取所有值

hdel com:cctv:user 删除值

hlen com:cctv:user 用来返回里面有多少个field

hincrby com:cctv:user age 20 给age的值+20

2.4 list数据类型命令

lpush id 1 2 3: 从左边依次将值存入

rpush id 4 5 6: 从右边依次将值存入

lpop id: 将最左边的一个值弹出

rpop id: 将最右边的一个值弹出

lrange id start end: 切片功能(注意索引从0开始)

blpop key timeout: 从key左边弹出一个值,如果没有值就会阻塞timeout秒

brpop key timeout: 从key右边弹出一个值,如果没有就会阻塞timeout秒

2.5 set数据类型命令

sadd habit basketball football tennis golf: 创建一个key为habit,value为basketball football tennis golf的set

smembers habit: 查看key为haibt的值

srem habit football: 删除footbal

scard habit: 统计habit有多少个值

sismember habit str: 查看habit里面是否有str这个值

sinter s1 s2: 集合的交,公共部分

sunion s1 s2: 集合的并

sdiff s1 s2: 集合的差,s1-s1和s2的公共部分

2.6 zset数据类型命令

zadd emp:1 60 tom 70 jery 90 lucy:

zscore emp:1 tom 查看tom的值

zcard emp:1 统计emp:1下面有多少个元素

zincrby emp:1 20 tom 给tom的值+20

zrange emp:1 start end withscores 从低到高排序,找出下标为start到end的值

zrevrange emp:1 start end withscores 从高到低排序

3. java编程(springboot整合redis)
3.1 引入依赖

image.png

<!--redis依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 添加对应的配置

image.png

host: redis服务所在的ip地址

port: redis服务的端口号

password: redis服务的密码

3.3 注入redisTemplate

image.png

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置连接⼯⼚
        template.setConnectionFactory(connectionFactory);
        // 创建JSON序列化⼯具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置Key的序列化
        template.setKeySerializer(jsonRedisSerializer);
        template.setHashKeySerializer(jsonRedisSerializer);
        // 设置Value的序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 返回
        return template;
    }
}
3.4 测试

image.png

这个redisTemplate需要自己通过@Autowired注解注入

redis数据序列化器

RedisTemplate可以接受任何Object作为值写入Redis,只不过写入之前会把Object序列化为字节形式,默认采用JDK序列化器,但是由于JDK序列化器不好用,这里给大家介绍两个其他的

  1. 自定义RedisTemplate序列化方式,这里采用了JSON序列化来代替JDK序列化方式

image.png

  1. stringRedisTemplate,使用时自动注入即可,不需要配置

image.png

需要注意的是,stringRedisTemplate序列器的键值对都必须是string类型

这里来介绍一下stringRedisTemplate和json的整合使用

image.png

redisTemplate.opsForValue()//操作字符串

redisTemplate.opsForHash()//操作hash

redisTemplate.opsForList()//操作list

redisTemplate.opsForSet()//操作set

redisTemplate.opsForZSet()//操作有序set

这是一些api,如果以后需要用到可以看看

image.png

image.png

image.png

image.png

五 redis持久化问题

redis持久化有两种方案,一种是RDB持久化,另外一种是AOF持久化

1. RDB持久化

RDB(Redis Data Back up file)简单来说就是把内存中的所有数据都记录到磁盘中,当Redis实例故障重启后,从磁盘读取快照文件,恢复数据.快照文件称为RDB文件,默认是保存在当前运行目录.对RDB文件可以在redis.conf文件中设置:

// 是否压缩,建议不开启,压缩会消耗cpu

rdbcompression yes

//RDB文件名称

dbfilename dump.rdb

// 指定生成rdb文件的路径,默认相对路径受到启动redis的操作路径的影响

dir ./

2. RDB执行时间

RDB在以下四种情况下会执行:

执行save命令

执行bgsave命令

Redis停机时

触发RDB条件时

2.1 执行save命令

save命令导致主进程执行RDB,这个过程中其他所有命令都会被阻塞,数据迁移时可以用到这个命令.

image.png

2.2 执行bgsave命令

bgsave命令开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响

image.png

2.3 Redis停机

当我们的Redis停机的时候,会实现RDB持久化

2.4 触发RDB条件

可以在redis.conf中找到对应的redis策略

redis7的默认策略: 3600 1 300 100 60 10000

意思是如果3600秒内如果至少有1个key被修改,则执行bgsave,300秒内如果有100个key被修改,则执行bgsave,60秒内如果有10000个key被修改则执行bgsave

2.5 对比save和bgsave
命令savebgsave
IO类型同步异步
知否阻塞redis其他命令否(在生成的子进程执行调用fork函数hi会有短暂阻塞)
优点不会额外消耗内存不阻塞客户端命令
缺点阻塞客户端命令需要fork子进程,消耗内存
3. AOF持久化

AOF(append only file)持久化是将修改的每一条指令记录进文件append.aof中(先写入os cache,默认每隔一秒fsync到磁盘),Redis处理的每一个命令都会记录在AOF文件,可以看做命令日志文件.AOF配置(AOF默认是关闭的,需要在redis.conf中配置):

// 是否开启AOF功能,默认是no

appendonly yes

// AOF文件名称

appendfilename "appendnonly.aof"

// AOF文件目录

appenddirname "appendonlydir"

这里需要注意的是,在redis7及其以上的版本,appendonly.aof不会以单独文件爱你出现,而会在appendonlydir文件夹中.

appendonly.aof.1.base: rdb作为基本文件

appendonly.aof.1.incr.aof: 作为增量文件的aof

4. AOF执行时间

AOF执行时间可以通过redis.conf文件来配置

// 每执行一次命令,立即记录到AOF文件

appendsync always

// 写执行命令完后先放入AOF缓冲区,然后每隔1秒将缓冲区数据写入AOF文件

appendsync everysec

// 写命令执行先放入AOF缓冲区,由操作系统来决定何时将内容写入磁盘

appendsync no

4.1 三种策略对比
配置项刷盘时机优点缺点
always同步刷盘可靠性高,几乎不丢失数据性能影响大
everysec每秒刷盘性能适中最多丢失1秒数据
no操作系统决定性能最好可靠性差,可能丢失较多数据
5. RDB和AOF对比
命令RDBAOF
启动恢复优先级
体积
恢复速度
数据安全性容易丢失数据根据策略决定

从表中我们可以看出,RDB和AOF各有所长,那么是否有一种方法可以兼顾两者的优点呢?答案是肯定的,我们可以通过开启混合持久化.

混合持久化在redis.conf中配置: aof-usr-rdb-preamble yes

如果要开启混合持久化就必须先开启aof持久化,混合持久化可以在Redis重启的时候先加载RDB的内容,因为其体积小,速度会快一点,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件,因此效率会大大提高,同时数据完整性也得到了保障.

六 Redis主从架构

单节点的redis并发能力是有上限的,要进一步提高redis并发能力,需要搭建主从架构,实现读写分离.

image.png

我下面在一台虚拟机里面来搭建一个主从架构

IPPORT角色
192.168.197.1316379master
192.168.197.1316380slave
192.168.197.1316381slave

注意这个ip地址是我自己的虚拟机的地址

1. 创建工作目录

mkdir -p /usr/local/redis/data/6379

mkdir -p /usr/local/redis/data/6380

mkdir -p /usr/local/redis/data/6381

2. 配置文件

复制redis.conf到6379,6380,6381文件夹下面:

cp redis.conf /usr/local/redis/data/6379/redis_6379.conf

cp redis.conf /usr/local/redis/data/6380/redis_6380.conf

cp redis.conf /usr/local/redis/data/6381/redis_6381.conf

进行修改,具体修改如下:

公共部分

bind 0.0.0.0

daemonize yes

requirepass 123456

protected-mode yes

// 如果是6379就配置6379,如果是6380就配置6380,灵活转变一下

port 6379

maxmemory 512mb

databases 16

// 如果是6379就配置6379,如果是6380就配置6380,灵活转换

dir /usr/local/redis/data/6379

// 把pid进程号写入pidfile配置文件

pidfile /var/run/redis_6379.pid

masterauth 123456

rdbcompression yes

dbfilename dump.rdb

appendonly yes

appendfilename "appendonly.aof"

appenddirname "appendonlydir"

appendsync everysec

auto-aof-rewrite-min-size

auto-aof-rewrite-percentage 100

aof-use-rdb-preamble yes

从节点配置

// ip地址填写的是redis服务的ip地址,因为我会把redis放到本机运行,所以这个ip地址就是我自己虚拟机的ip地址,端口号就是master的端口号

replicaof 192.168.197.131 6379

// 从节点默认只读,不具备写的能力,实现读写分离

replica-read-only yes

3. 启动redis服务

redis-server /usr/local/redis/data/6379/redis_6379.conf

redis-server /usr/local/redis/data/6380/redis_6380.conf

redis-server /usr/local/redis/data/6381/redis_6381.conf

4. 进入主节点,查看情况

redis-cli -a 123456 -p 6379

执行命令 info replication

image.png

七 Redis哨兵

Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复.

1. 哨兵原理

sentinel哨兵是特殊的Redis服务,不提供读写服务,主要用来监控redis实例节点,哨兵架构下的client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端.

image.png

哨兵作用:

监控: Sentinel会不断检查master和slave是否按预期工作

自动故障恢复: 如果master故障,Sentinel会将一个slave提升为master,当故障实例恢复后也以新的master为主

2. 实现哨兵
节点IPPORT
s1192.168.197.13126380
s2192.168.197.13126380
s3192.168.197.13126381
2.1 创建工作目录

mkdir -p /usr/local/redis/data/s1

mkdir -p /usr/local/redis/data/s2

mkdir -p /usr/local/redis/data/s3

2.2 配置文件

将redis里面的sentinel.conf移动到s1、s2、s3工作目录

cp sentinel.conf /usr/local/redis/data/s1/sentinel_26379.conf

cp sentinel.conf /usr/local/redis/data/s2/sentinel_26380.conf

cp sentinel.conf /usr/local/redis/data/s3/sentinel_26381.conf

修改配置如下:

protected-mode no

// 改成对应的端口,灵活改变

port 26379

daemonize no

pidfile "/var/run/redis-sentinel-26379.pid"

logfile ""

// 工作目录不同的需要修改,灵活改变

dir /usr/local/redis/data/s1

// sentinel monitor master-redis-name master-redis-ip master-redis-port quorum

quorum是一个数字,指多少个sentinel认为master失效时,这个master才算真的失效

sentinel monitor mymaster 192.168.197.131 6379 2

// sentinel auth-pass <服务器名称><密码>

sentinel auth-pass mymaster 123456

2.3 启动服务

启动redis集群

redis-server /usr/local/redis/data/6379/redis_6379.conf

redis-server /usr/local//redis/data/6380/redis_6380.conf

redis-server /usr/local/redis/data/6381/redis_6381.conf

启动哨兵实例

redis-sentinel /usr/local/redis/data/s1/sentinel_26379.conf

redis-sentinel /usr/local/redis/data/s2/sentinel_26380.conf

redis-sentinel /usr/local/redis/data/s3/sentinel_26381.conf

2.4 查看sentinel的info信息

redis-cli -a 123456 -p 26379

执行info命令可以查看了

八 SpringBoot整合Redis哨兵

1. 引入依赖

image.png

<!--redis依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 在application中添加配置

image.png

redis:
  database: 0
  timeout: 3000
  password: 123456 # master密码
  sentinel: #哨兵模式
    master: mymaster #主服务器所在集群名称
    nodes:
      - 192.168.197.131:26379
      - 192.168.197.131:26380
      - 192.168.197.131:26381
  lettuce:
    pool:
      max-idle: 50
      min-idle: 10
      max-active: 100
      max-wait: 1000
3. 测试

image.png

@RestController
public class RedisController {
    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/test_sentinel")
    public void testSentinel() throws InterruptedException {
        int i = 1;
        while (true) {
            try {
                stringRedisTemplate.opsForValue().set("age" + i, i + "");
                System.out.println("设置key:" + "age" + i);
                i++;
                Thread.sleep(1000);
            } catch (Exception e) {
                logger.error("错误:", e);
            }
        }
    }
}
注意: 如果一个master挂了之后,在重新选举master期间,集群是不可用的

九 Redis高可用集群

主从和哨兵解决了高可用、高并发读的问题,但是依然没有解决海量数据存储问题,高并发写的问题

image.png

redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性

集群有多个master,每个master保存了不同数据

每个master都可以有多个节点

master之间通过ping监测彼此健康状态

客户端请求可以访问集群任意节点,最终都会被转发到正确节点

接下来讲一下如何搭建高可用集群

IPPORT角色
192.168.197.1318001master
192.168.197.1318002master
192.168.197.1318003master
192.168.197.1318004slave
192.168.197.1318005slave
192.168.197.1318006slave
1. 创建工作目录

mkdir -p /usr/local/redis-cluster

mkdir -p /usr/local/redis-cluster/8001

mkdir -p /usr/local/redis-cluster/8002

mkidr -p /usr/local/redis-cluster/8003

mkdir -p /usr/local/redis-cluster/8004

mkdir -p /usr/local/redis-cluster/8005

mkdir -p /usr/local/redis-cluster/8006

2. 配置文件

将redis.conf配置文件复制到8001-8006各一份,

cp redis.conf /usr/local/redis-cluter/8001/redis_8001.conf

cp redis.conf /usr/local/redis-cluster/8002/redis_8002.conf

cp redis.conf /usr/local/redis-cluster/8003/redis_8003.conf

cp redis.conf /usr/local/redis-cluster/8004/redis_8004.conf

cp redis.conf /usr/local/redis-cluster/8005/redis_8004.conf

cp redis.conf /usr/local/redis-cluster/800f6/redis_8006.conf

具体修改内容如下:

daemonize no

// 注意每个端口不一样

port 8001

// 每一个改成对应的(方便阅读)

pidfile /var/run/redis_8001.pid

// 改成对应端口的工作目录

dir /usr/local/redis-cluster/8001

// 启动集群模式

cluster-enabled yes

// 集群节点信息文件对应即可

cluster-config-file nodes-8001.conf

// 表示某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换

cluster-node-timeout 10000

bind 0.0.0.0

portected-mode yes

appendonly yes

requirepass 123456

masterauth 123456

3. 启动redis实例

redis-server /usr/local/redis-cluster/8001/redis_8001.conf

redis-server /usr/local/redis-cluster/8002/redis_8002.conf

redis-server /usr/local/redis-cluster/8003/redis_8003.conf

redis-server /usr/local/redis-cluster/8004/redis_8004.conf

redis-server /usr/local/redis-cluster/8005/redis_8005.conf

redis-server /usr/local/redis-cluster/8006/redis_8006.conf

4. 创建redis集群

虽然服务启动了,但是目前每一个服务之间还是相对独立的,没有任何关联,我们需要执行命令来创建集群让他们关联

redis-cli -a 123456 --cluster create --cluster-replicas 1 192.168.197.131:8001 192.168.197.131:8002 192.168.197.131:8003 192.168.197.131:8004 192.168.197.131:8005 192.168.197.131:8006

redis-cli --cluster: 代表集群操作命令

create: 代表创建集群

--replicas 1或者--cluster-replicas 1: 指定集群中每个master的副本数个数为1,此时节点总数/(replicas+1)得到的就是master的数量,因此节点列表前n个就是master,其他的就是slave节点,随机分配不同的master

5.验证集群

连接任何一个客户端都可以

redis-cli -a 123456 -p 8001

进行验证

cluster info(查看集群信息)

cluster nodes(查看节点列表)

十 SpringBoot整合redis高可用集群

1. 引入依赖

image.png

<!--redis依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置文件

image.png

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  redis:
    database: 0
    timeout: 3000
    password: 123456
    cluster:
      nodes:
        - 192.168.197.131:8001
        - 192.168.197.131:8002
        - 192.168.197.131:8003
        - 192.168.197.131:8004
        - 192.168.197.131:8005
        - 192.168.197.131:8006
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000
3. 测试

image.png

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

@RestController
public class ClusterController {

    private static final Logger logger = LoggerFactory.getLogger(ClusterController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/test_cluster")
    public void testCluster() throws InterruptedException {
        stringRedisTemplate.opsForValue().set("good", "666");
        System.out.println(stringRedisTemplate.opsForValue().get("good"));
    }
}

十一 Redis集群原理分析

Redis Cluster 会将所有的数据划分为16384个slots(槽位),每个节点负责其中一部分槽位,槽位的信息存储于每个Master节点中.当Redis Cluster客户端来连接集群时,它会得到一份集群的槽位配置信息并将其缓存到客户端本地,这样客户端要查找某个key时,可以直接定位到目标节点.同时客户端的槽位信息可能会存在和服务器不一致情况,还需要纠正机制来校验.

Redis槽位定位算法

Cluster默认会对key使用crc16算法得到一个整数值,然后用这个整数值对16384进行取模来得到具体的槽位

跳转重定位: 当客户端向一个错误的节点发送了指令,该节点会发现指令的key所在槽位不属于自己管理,这是它会向客户端发送一个特殊的跳转指令并携带目标操作的节点地址,告诉客户端去对应的节点获取数据.

1. Redis脑裂问题
1.1 什么是脑裂问题

脑裂是指主从集群中,同时存在两个主节点,它们都可以接受写的请求.而脑裂问题的最大影响就是客户端不知道该往那个master写入数据.

1.2 为什么会有脑裂问题

假设某一个Redis集群中某个主库突然出现了暂时性"失联",而不是真的故障,那么集群便会发起选举进行主从切换,当原来的master从故障中恢复过来了,这样就会出现旧的master和新的master,了脑裂问题

1.3 如何解决脑裂问题

在redis.conf中配置如下参数:

min-replicas-to-write 2

这个代表写入数据最少同步slave数量,这个数量因场景而定.比如集群中有三个节点,1个master和2个slave,那么配置为2就代表写入的数据最少要有2个slave同步这个master才会生效.弊端在于在一定程度撒上会影响集群的可用性.

注意: Redis集群最少三个master节点,并且推荐节点数为奇数的原因:

新的master选举需要大于半数才会选举成功.

十二 Redis性能优化

1. 缓存穿透
1.1 什么是缓存穿透

缓存穿透是指查询一个根本不存在的数据,导致缓存层和存储层都没有命中.缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存层对后端的保护作用.

1.1 如何解决缓存穿透

方案一: 缓存空对象

简单而言就是查询不存在的数据,那么第一次去存储层查询,然后在缓存层给它设置一个key,值是空的,下次来查询可以直接在缓存层查询到,只是查询到是空对象而已.

下列是伪代码:

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储(指数据库)中获取
        String storageValue = storage.get(key);
        // 写入缓存
        cache.set(key, storageValue);
        // 如果存储数据为空, 需要设置一个过期时间(300秒)
        if (storageValue == null) {
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}

方案二: 布隆过滤器

布隆过滤器是一个大型的位数组,布隆过滤器第一步会把需要缓存的表数据的主键用hash算法计算后存储到布隆过滤器数组中,当客户端向布隆过滤器发送请求询问某个主键是否存在,布隆过滤器会根据主键的hash值来看是否存在,如果不存在就不会将该请求转到后端去,进行一个过滤的作用.

2. 缓存击穿
2.1 什么是缓存击穿

由于大批量缓存在同一时间失效导致大量请求同时穿透缓存到达数据库,可能会造成数据库瞬间压力过大而挂掉

2.2 如何解决缓存击穿

在批量缓存的时候将一批数据的缓存时间设置不同即可

如下是伪代码:

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        //设置一个过期时间(300到600之间的一个随机数)
        int expireTime = new Random().nextInt(300)  + 300;
        cache.expire(key, expireTime);
        
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}
3. 缓存雪崩
3.1 什么是缓存雪崩

缓存雪崩是指缓存层宕机或者缓存层由于某种原因不能对外提供服务,导致大量请求发送到存储层,存储层调用量暴增,造成存储层也宕机

3.2 如何解决缓存雪崩
  1. 保存缓存层服务高可用性,比如说Redis Sentinel或者Redis Cluster

  2. 依赖隔离组件为后端限流、熔断、并降级

4. 数据库、缓存双写不一致
4.1 什么是双写不一致

由于用户修改数据库后,缓存由于各种原因没有及时更新,后续用户在查询会直接查询redis缓存,后续用户得到的是旧数据,而此时数据库中的是新数据,这就是双写不一致问题.

4.2 如何解决双写不一致问题
  1. 先写缓存,再写数据库(不推荐)

image.png

如上图,如果有网络延迟,那么会导致数据库的数据是用户A写入的旧数据,而缓存里面的是用户B写入的新数据,导致双写不一致

  1. 先写数据库,再写缓存(不推荐)

image.png

如上图,如果有网路延迟,那么会导致数据库中的数据是用户B写入的新数据,而缓存里面是用户A写入的旧数据

  1. 先删缓存,再写数据库(不推荐)

image.png

请求A写完新值之后删除缓存,但是由于网络延迟问题,数据库没来得及更新,请求B查询缓存发现没有,直接查询数据库,然后将旧值更新到缓存中,请求A延迟结束,数据库更新新值,那么数据库和缓存会双写不一致

  1. 先写数据库,再删缓存(并发量不高,推荐使用)

image.png

请求B查询缓存,发现没有数据,查询数据库,然后发生网络延迟,这是请求A写入新值到数据库,然后删除缓存,然后请求B延迟结束,缓存更新旧值,导致双写不一致.但是在实际情况下更新缓存比数据库要开的多,发生网络延迟的概率会很小,如果在并发量不高可以考虑.

  1. 延时缓存双删(推荐)

image.png

如上图,请求A过来,先把缓存删除了,但是由于网络延迟,数据库没来得及更新,这是请求B过来了,查询缓存发现没有,就查询数据库,请求B就会把数据库的旧值更新到缓存中,请求A延迟结束,将新值写入数据库,然后再将缓存删除,注意第二次并非马上删除缓存,而是等待一定的时间.