redis

本文详细介绍了 Redis 与 Memcached 的区别,包括单线程模型、内存管理和数据结构支持。重点讲解了 Redis 的持久化机制,包括 RDB 和 AOF 两种方式,以及它们的优缺点。此外,讨论了 Redis 的数据结构如 String、Hash、List、Set 和 Sorted Set 的应用,并分析了 Redis 在高并发场景下的性能优势及单线程设计的原因。还涉及了分布式锁的实现、Redis 遇到的并发竞争问题及解决方案,并讨论了缓存雪崩、缓存穿透和缓存并发等问题及其预防策略。
摘要由CSDN通过智能技术生成

我这人平时懒,但是面试不知有多少次问我redis的问题了,今天我得好好总结下redis。

redis和memcached的区别

为了简化以下m代表memcached。
1、redis是单线程。memcached是多线程(主线程监听,work子线程工作)
2、m存储在物理内存中,redis有自己的VM机制,当数据超量时使用swap影响计算机服务性能
3、memcached单条最大1M,redis最大512M
4、m存储K,V形式数据,redis支持多种数据结构
5、m的数据全部在内存,服务器重启缓存就没了,redis的数据部分在硬盘上,可以进行持久化

redis分布式锁

由于分布式中是多个进程、多个线程、分布在不同的机器上,所以才会有分布式锁的问题。
redis分布式锁采用开源框架Redission;加锁和释放锁的代码:

Rlock lock = Redisson.getLock(“myLock”);
lock.lock();
lock.unlock();

redis支持单例、哨兵、cluster、master-cluster、redis-master-slave等各种部署架构。

redis持久化

redis持久化流程:
在这里插入图片描述
以上是理想条件下数据正常的保存流程,1,2,3需要redis服务器进行操作,4,5两步是操作系统帮我们完成的持久化操作。

redis持久化方式:

推荐博客:
redis两种持久化机制个人认为比较好的文章

1、RDB默认的持久化方式

通过快照snapShotting完成。
触发方式:
1、save方式会产生阻塞
在这里插入图片描述
2、bgsave方式,在后台异步进行快照操作
在这里插入图片描述
bgsave持久化时redis服务发生故障,会丢失持久化时的数据。
在这里插入图片描述
两种方式总结:

1、save方式是同步的,不会产生子进程,会阻塞;bgsave方式是异步的,会产生子进程,消耗额外内存,但不会阻塞。
2、save适合停机备份或低谷时段进行,比较快
3、bgsave适合线上备份

3、自动触发

在redis.conf配置文件有如下配置:
- 表示每隔900s即15分钟内至少有一个键被更新就持久化一次
save 900 1  
- 表示每隔300s即5分钟内至少有10个键被更新就持久化一次
save 300 10
-表示每隔60s即1分钟至少有10000个键被更新就持久化一次
save 60   10000

-这些条件是或的关系

-快照文件的路径
dir ./
-rdb快照文件的名称
dbfilename

总结:
1、redis是全量备份,文件比较紧密
2、子进程在进行快照持久化时,主进程的数据变更不会持久化到快照中
3、当redis重启时,就从硬盘快照文件rdb中恢复数据,但是redis异常退出时会丢失最后一次快照后的数据。
4、恢复数据块

2、AOF持久化

采用追加的方式持久化。
持久化原理:
在这里插入图片描述

文件重写原理:
当aof文件比较大时,就通过命令bgrewriteaof压缩持久化文件,将内存中的数据以命令的方式保存到临时文件中,同时fork出一条新进程将文件重写。

1、在重写期间redis客户端可以继续处理客户端命令
2、fork子进程带有父进程的内存数据拷贝副本,在不适用锁的情况下也可以保证数据的安全
3、fork子进程重写完数据后向父进程发送信号,父进程收到信号后,调用信号处理函数,把AOF重写缓冲区里的数据写入AOF重写文件,把新的AOF文件替换掉老的AOF文件。
4、整个过程在父进程调用信号处理函数时有阻塞。
在这里插入图片描述

aof持久化方式的触发机制:
1、**always:**服务器每写入一个命令,就调用一次fdatasync,将缓冲区里的命令写入到硬盘,在这种模式下,服务器出现故障,也不会丢失任何已经成功执行的命令。
2、everysec(默认):服务器每隔1秒就调用一次fdatasync,将缓冲区里的命令写入到硬盘,在这种模式下,服务器出现故障,最多丢失一秒的数据
3、No:服务器不主动掉fdatasync,由操作系统决定何时将缓冲区里的命令写入到硬盘,在这种模式下,服务器出现故障,丢失的数据是不可控的

- 默认情况下没有开启AOF持久化,通过修改配置文件redis.conf中的参数开启。
appendonly true

开启aof持久化,当有数据更改时就持久化到aof文件中,

appendfsync  always|everysec|no 持久化触发方式

持久化文件路径也是dir配置的路径,

文件名设置:appendfilename appendonly.aof

AOF数据恢复是通过创建一个不带网络的伪客户端执行aof命令,来完成数据恢复的。

两种持久化方式比较:
RDB:
优点: 1、全量备份,不同时间的数据备份可以做到多版本恢复
2、紧凑的单一文件,适合网络传输和灾难恢复
3、恢复大数据集比AOF快
缺点:1、会丢失最近写入或修改未持久化的数据
2、fork过程耗时,造成毫秒级不能响应客户端请求

AOF:
优点:1、写入机制好,做多丢失1秒的数据
2、重写机制好
3、如果误操作了flushAll,只要aof未被重写,停止服务,移除aof文件flushall命令,重启redis,可以将数据恢复到flushall之前的数据
缺点:1、相同数据集,aof文件体积比rdb文件大
2、恢复速度比rdb慢

redis的数据结构

String 字符串

就是简单的Key-Value类型

Hash 字典

存储的一般是对象信息。
适用于并发操作,省去了String类型的序列化和反序列化过程

List 列表

双端链表实现list,可以轻松实现最新消息排行功能,当做消息队列,push和pop操作,还提供查询、删除某一段元素的API。
微博里的关注列表,粉丝列表

Set 集合

一堆不重复值的组合。
提供了求交集、并集、差集的操作

Sorted Set 有序集合

和Set相比,有个权重参数score,使得集合中的元素按score进行排序,比如全班同学考试,value是学号,score是分数,实现按分数排序。按权重大小取数据,比如普通消息score为1,重要消息score为2,按score倒序获取工作任务。

redis其他应用场景

1、消息发布-订阅
对某一个key值进行消息发布-订阅,当一个key值改变了,所有订阅它的客户端都会收到消息。
2、事务
redis提供了对key进行watch机制,如果watch值被修改了,这个事务就会发现并拒绝执行。

redis高并发和快速的原因

1、基于内存操作
2、单线程省去了上下文切换、资源竞争
3、采用多路(多个socket)复用(一个线程)技术,可以处理并发连接。非阻塞IO内部epoll,采用了epoll+自己实现的简单事件框架。epoll中的读、写、关闭、连接都转化为了事件,然后利用epoll的多路复用特性,绝不在io上浪费时间。
4、数据结构上采用hash读取速度快;数据存储上使用压缩表;跳表使用有序的数据结构加快读取速度。

跳表:
在这里插入图片描述
压缩表:
列表键和hash键枚举比较少,值比较少,为减少内存存储选择压缩表。有连锁更新问题。
在这里插入图片描述

为什么redis是单线程的

redis是基于内存的,cpu不是瓶颈,redis的瓶颈是内存或者带宽,既然单线程容易实现,cpu不是瓶颈,就顺理成章的采用单线程方案。

如果CPU成为了瓶颈,就起多个redis进程,但是要分清哪些key在哪些redis进程上。

多线程主要是提高CPU的利用率

多线程是随着CPU多核应用火起来的

  • 调用sleep()时释放CPU
  • 网络编程中的阻塞accept()等待客户端连接,阻塞recv()等待下游回包都不占用cpu资源。
  • 单核CPU设置能否提高并发性能?
    大多数情况下可以提高并发性能。
    如果一个线程一直占用资源,此时增加线程没效果,比如如下代码:
    while(true){i++}
    多线程编码让代码更加清晰:比如I/O线程收发包,worker线程进行任务处理,Timeout线程进行超时检测
    在这里插入图片描述
    上图是典型的工作线程处理过程,从开始处理start到结束处理end,该任务的处理共有7个步骤:

1、从工作队列里拿出任务,进行一些本地初始化计算,例如http协议分析、参数解析、参数校验等
2、访问cache拿一些数据
3、拿到cache里的数据后,再进行一些本地计算,这些计算和业务逻辑相关
4、通过RPC调用下游service再拿一些数据,或者让下游service去处理一些相关的任务
5、 RPC调用结束后,再进行一些本地计算,怎么计算和业务逻辑相关
6、访问DB进行一些数据操作
7、 操作完数据库之后做一些收尾工作,同样这些收尾工作也是本地计算,和业务逻辑相关

分析整个处理的时间轴,会发现:
其中,1、3、5、7步骤中,线程执行本地业务逻辑计算时需要占用CPU,而2、4、6步骤中访问cache,service,DB过程线程处于等待结果的状态,不需要占用CPU。进一步分解等待这部分:

  1. 请求在网络传输到下游的cache,service,db
  2. 下游cache,service,db进行任务处理
  3. cache,service,db将报文传输到工作线程

N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)=N*(x+y)/x,能让CPU的利用率最大化。

redis并发竞争锁的问题

redis本身是单线程,不存在并发问题,但是利用jedis等客户端对Redis进行并发访问时会出现问题。
解决方案:分布式锁+时间戳
分布式锁setnx:
客户端使用下列命令进行获取:

线程锁:只在同一JVM中有效,因为线程锁的实现根本上是依靠线程之间共享内存来实现的。 比如synchronized是共享对象头,显示锁lock是共享某个变量state。

进程是系统进行资源分配和调度的基本单位,系统中正在运行的一个应用程序,进程是线程的容器
在这里插入图片描述
线程是操作系统能够进行运算调度的最小单位,一个进程可以并发执行多个线程,每条线程并行执行不同的任务
在这里插入图片描述
进程之间通过TCP/IP进行交互的
线程之间交互:有一大块内存,只要大家的指针是同一个就可以看到各自的内存

进程锁:是为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有相互独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized线程锁实现进程锁。

分布式锁:当多个进程不在同一操作系统中,用分布式锁控制多个进程对资源的访问。

redis实现分布式锁

分布式锁的实现原理:
setnx方式

在这里插入图片描述
加锁时的LRU脚本:
在这里插入图片描述

那么,这段lua脚本是什么意思呢?
这里KEYS[1]代表的是你加锁的那个key,比如说:RLock lock = redisson.getLock(“myLock”);这里你自己设置了加锁的那个锁key就是“myLock”。

ARGV[1]代表的就是锁key的默认生存时间,默认30秒。ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1

给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。如何加锁呢?很简单,用下面的命令:hset myLock
在这里插入图片描述
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。好了,到此为止,ok,加锁完成了。

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。

上述分布式锁的缺点:

就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。

常用的命令:

1、setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。

2、get(key):获得key对应的value值,若不存在则返回nil。

3、getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。

4、 expire(key, seconds):设置key-value的有效期为seconds秒。

加锁代码示例:

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

解锁的代码示例:

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
 
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
时间戳方式

要求key的操作需要顺序执行,所以需要保存一个时间戳判断set顺序。

系统A key 1 {ValueA 7:00}
系统B key 1 { ValueB 7:05}

假设系统B先抢到锁,将key1设置为{ValueB 7:05}。接下来系统A抢到锁,发现自己的key1的时间戳早于缓存中的时间戳(7:00<7:05),那就不做set操作了。

使用消息队列

把并行访问改为串行访问

redis做缓存会出现的问题

缓存雪崩

数据未加载到缓存中,或者缓存大面积失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。

预防缓存雪崩:

1、缓存的高可用,防止大面积故障,例如:Redis Sentinel(哨兵模式)和Redis Cluster(集群模式)都实现了高可用
2、缓存降级
对源服务访问进行限流、资源隔离(熔断)、降级。

当访问量剧增、服务出现问题仍然需要保证服务还是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级,这里会涉及到运维的配合。

降级的最终目的是保证核心服务可用,即使是有损的。

在进行降级之前要对系统进行梳理,比如:哪些业务是核心(必须保证),哪些业务可以容许暂时不提供服务(利用静态页面替换)等,以及配合服务器核心指标,来后设置整体预案,比如:

(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

缓存穿透

缓存穿透是指查询一个一不存在的数据。例如:从缓存redis没有命中,需要从mysql数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决思路:

如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。

可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。

缓存并发

缓存并发问题,上面的分布式锁,消息队列是应对措施

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统中。
这样用户发起请求时不用先请求数据库再存储到缓存系统中了。

缓存预热的方法:
1、直接写个缓存刷新页面,上线后操作下
2、数据量不大的情况下,项目启动的时候自动加载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值