当前位置: 首页 > news >正文

黑马redis

Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理

Redisson分布式锁实现15问

文章目录

  • 主线程和IO线程是如何协作的
  • Unix网络编程中的五种IO模型
  • Linux世界一切皆文件
  • 零拷贝
    • 传统的文件传输有多糟糕?
    • 总结
  • 生产上限制keys *、flushdb、flushall等危险命令
    • keys * 遍历查询100W数据花费时长
    • 配置禁用这些命令
  • BigKey案例
  • 缓存更新策略
    • Redis内存不足的缓存淘汰策略
    • 先删缓存再操作数据库
      • 理想情况
      • 多线程竟态条件下
      • 多线程竟态条件下
    • 先操作数据库再删除缓存【胜出】
      • 理想情况
    • 总结
  • 项目实践【黑马点评】
    • 目标
    • 缓存一致性
    • 缓存穿透
      • 缓存穿透解决方案调研
      • 实战解决商铺信息缓存穿透
      • 总结
    • 缓存雪崩
    • 缓存击穿
      • 缓存击穿解决方案调研
      • 实战解决缓存击穿
        • 互斥锁(setnx)
  • 优惠券秒杀-单机锁
    • 全局唯一ID
      • 自增ID存在的问题
      • 分布式ID的实现
    • 实战优惠券秒杀
      • 总结
  • 优惠券秒杀-分布式锁
    • 自定义的分布式锁
    • 将单机 synchronized 替换为自定义分布式锁
    • 分布式锁误删问题🍖
      • 问题原因分析
      • 代码实现
    • 判断锁标识和释放锁非原子性🥩
    • 存在的问题
      • 锁不可重入
      • 不可重试
      • 超时释放
      • 主从一致性
  • Redis集群方案
    • 主从复制—全量同步、增量同步
      • 全量同步
      • 增量同步
      • 面试题
    • 哨兵模式
      • 服务状态监控
      • redis集群(哨兵模式)脑裂
      • 面试题
    • 分片集群
      • 分片集群结构
      • 分片集群结构——数据读写
      • 存在的问题
      • 面试题1
      • 面试题2
  • Big Key
    • 大key的影响
    • 大key的查找
    • 删除大key注意事项
    • 大key的处理
    • 分拆方案
      • 一、单个简单的key存储的value很大
      • 二、value中存储过多的元素
      • 方案一:使用时间戳作为附加属性
      • 方案二:通过在 `key` 拼接上基于时间分拆
      • 代码解释
        • 方案一代码解释
        • 方案二代码解释
  • redis集群分片为什么最大槽数是16384个?

主线程和IO线程是如何协作的

  • 阶段一:服务端和客户端建立Socket连接,并分配处理线程
    首先,主线程负责接收建立连接请求,当有客户端请求和实例建立Socket连接时,主线程会创建和客户端的连接,并把 Socket放入全局等待队列中。紧接着,主线程通过轮询方法把Socket连接分配给IO线程

  • 阶段二:IO线程读取并解析请求
    主线程一旦把Socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求读取和解析。因为有多个IO线程在并行处理,所以,这个过程很快就可以完成。

  • 阶段三:主线程执行请求操作
    等到IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作
    在这里插入图片描述

  • 阶段四:IO线程回写Socket和主线程清空全局队列
    当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待IO线程,把这些结果回写到Socket中,并返回给客户端。和IO线程读取和解析请求一样,IO线程回写Socket时,也是有多个线程在并发执行,所以回写Socket的速度也很快。等到IO线程回写Socket完毕,主线程会清空全局队列,等待客户端的后续请求。
    在这里插入图片描述

Unix网络编程中的五种IO模型

Blocking IO - 阻塞IO

NoneBlocking IO - 非阻塞IO

IO multiplexing - IO多路复用 ★★★

signal driven IO - 信号驱动IO(偏C)

asynchronous IO - 异步IO(偏C)

Linux世界一切皆文件

文件描述符、简称FD,句柄

FileDescriptor:
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统

I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的

零拷贝

在这里插入图片描述

可以看到,整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。
简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用CPU 来搬运的话,肯定忙不过来。
计算机科学家们发现了事情的严重性后,于是就发明了 DMA技术,也就是直接内存访问(Direct Memory Access)技术。
什么是 DMA技术?简单理解就是,在进行I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样CPU 就可以去处理别的事务。

那使用 DMA 控制器进行数据传输的过程究竟是什么样的呢?下面我们来具体看看。
在这里插入图片描述

具体过程:

  • 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
  • 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
  • DMA 进一步将 I/O 请求发送给磁盘;
  • 磁盘收到 DMA的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA发起中断信号,告知自己缓冲区已满;
  • DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用CPU,CPU 可以执行其他任务;
  • 当 DMA 读取了足够多的数据,就会发送中断信号给CPU;
  • CPU 收到 DMA的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回;

可以看到,整个数据传输的过程,CPU不再参与数据搬运的工作,而是全程由 DMA 完成,但是CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。

早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。

传统的文件传输有多糟糕?

如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。

代码通常如下,一般会需要两个系统调用:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

代码很简单,虽然就两行代码,但是这里面发生了不少的事情。
在这里插入图片描述
首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。

其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:

  1. 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。

  2. 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。

  3. 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。

  4. 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。

我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。

这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。

所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。

总结

早期 I/O 操作,内存与磁盘的数据传输的工作都是由 CPU 完成的,而此时 CPU 不能执行其他任务,会特别浪费 CPU 资源。

于是,为了解决这一问题,DMA 技术就出现了,每个 I/O 设备都有自己的 DMA 控制器,通过这个 DMA 控制器,CPU 只需要告诉 DMA 控制器,我们要传输什么数据,从哪里来,到哪里去,就可以放心离开了。后续的实际数据传输工作,都会由 DMA 控制器来完成,CPU 不需要参与数据传输的工作。

传统 IO 的工作方式,从硬盘读取数据,然后再通过网卡向外发送,我们需要进行 4 上下文切换,和 4 次数据拷贝,其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间,这个是由 DMA 完成,另外 2 次则发生在内核态和用户态之间,这个数据搬移工作是由 CPU 完成的。

为了提高文件传输的性能,于是就出现了零拷贝技术,它通过一次系统调用(sendfile 方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数。另外,拷贝数据都是发生在内核中的,天然就降低了数据拷贝的次数。

Kafka 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能。

零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。

需要注意的是,零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。

另外,当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,并且大文件的缓存命中率不高,这时就需要使用「异步 IO + 直接 IO 」的方式。

在 Nginx 里,可以通过配置,设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。

生产上限制keys *、flushdb、flushall等危险命令

keys * 遍历查询100W数据花费时长

在这里插入图片描述

配置禁用这些命令

redis.conf 在 SECURITY 这一项中

rename-command keys ""
rename-command flushdb ""
rename-command FLUSHALL ""

BigKey案例

多大算Big
参考《阿里云Redis开发规范》

在这里插入图片描述

缓存更新策略

在这里插入图片描述

Redis内存不足的缓存淘汰策略

  • noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  • volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  • allkeys-random:加入键的时候如果过限,从所有key随机删除
  • volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  • volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键 allkeys-lfu:从所有键中驱逐使用频率最少的键
    在这里插入图片描述
    在这里插入图片描述

先删缓存再操作数据库

理想情况

在这里插入图片描述

多线程竟态条件下

在这里插入图片描述

多线程竟态条件下

好巧不巧,缓存失效了,此时线程2要采用先更新数据库再删除缓存的策略,但由于更新数据库没有线程1查询数据库快,所以查到的还是未更新前的旧值10;
线程2更新完毕之后删除了redis缓存,线程1获取时间片后又将10写回了缓存,导致数据库缓存不一致的情况
在这里插入图片描述

先操作数据库再删除缓存【胜出】

理想情况

在这里插入图片描述
在这里插入图片描述

总结

给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案

我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。
也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以数据落库DB为准

项目实践【黑马点评】

目标

在这里插入图片描述

缓存一致性

com.sddp.service.impl.ShopServiceImpl#update
事务保证原子性,如果在微服务系统中,这两步不在一个方法当中,甚至不在一个服务当中,那么就需要mq消息通知删除缓存的服务,可以借助TCC来保证分布式事务的原子性
在这里插入图片描述

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。如果被恶意用户利用,对服务器会造成负载,严重会导致服务不可用
常见的解决方案有两种:

com.sddp.service.impl.ShopServiceImpl#queryById

在这里插入代码片

缓存穿透解决方案调研

在这里插入图片描述

实战解决商铺信息缓存穿透

如果提交的商铺id本身就是瞎写的,查询数据库之后必然没有数据,那此时,redis则将此id存在redis并赋值为null,下次在查询此id时直接走redis返回null即可
在这里插入图片描述

总结

在这里插入图片描述

缓存雪崩

TTL随机数分散降低机率
Redis宕机:利用集群提高服务的可用性
快速失败、拒绝服务

在这里插入图片描述

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂下图第 2 步比较耗时,导致多线程访问的时候短时间为写入缓存,期间的流量都打到DB上了)的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
在这里插入图片描述

缓存击穿解决方案调研

互斥锁:CP(强一致)
逻辑过期:AP(高可用)
在这里插入图片描述
在这里插入图片描述

实战解决缓存击穿

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁 来锁住它。

其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。

后面的线程进来发现已经有缓存了,就直接走缓存


/*** @auther zzyy* @create 2021-05-01 14:58*/
@Service
@Slf4j
public class UserService {public static final String CACHE_KEY_USER = "user:";@Resourceprivate UserMapper userMapper;@Resourceprivate RedisTemplate redisTemplate;/*** 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行* @param id* @return*/public User findUserById(Integer id){User user = null;String key = CACHE_KEY_USER+id;//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysqluser = (User) redisTemplate.opsForValue().get(key);if(user == null){//2 redis里面无,继续查询mysqluser = userMapper.selectByPrimaryKey(id);if(user == null){//3.1 redis+mysql 都无数据//你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redisreturn user;}else{//3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率redisTemplate.opsForValue().set(key,user);}}return user;}/*** 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。* @param id* @return*/public User findUserById2(Integer id){User user = null;String key = CACHE_KEY_USER+id;//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,// 第1次查询redis,加锁前user = (User) redisTemplate.opsForValue().get(key);if(user == null) {//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysqlsynchronized (UserService.class){//第2次查询redis,加锁后user = (User) redisTemplate.opsForValue().get(key)

http://www.mrgr.cn/news/79563.html

相关文章:

  • 如何将CSDN的文章保存为PDF?
  • 软考中级-软件设计师通过心路经验分享
  • vscode通过ssh连接远程服务器(实习心得)
  • [Collection与数据结构] 位图与布隆过滤器
  • Vue.createApp的对象参数
  • 【Python】【Conda 】Conda 与 venv 虚拟环境优缺点全解:如何做出明智选择
  • frida(objection)中x.ts到x.py封装路径
  • 复现论文:PromptTA: Prompt-driven Text Adapter for Source-freeDomain Generalization
  • ubuntu防火墙设置(四)——iptables语法与防火墙基础配置
  • 树莓派4B使用opencv读取摄像头配置指南
  • 【计网笔记】网络参考模型
  • MongoDB-BSON 协议与类型
  • 【数据库】关系代数和SQL语句
  • [C++]C风格数组之指针数组、数组指针、指向数组的指针、指向数组第一个元素的地址的指针的异同和联系
  • Redis(一)
  • openjdk17 jvm加载class文件,解析字段和方法,C++源码展示
  • CUDA编程 | 5.3减少全局内存访问
  • HCIA-Access V2.5_2_2网络通信基础_TCP/IP模型结构
  • linux 系统常用指令
  • react hooks讲解--通俗易懂版
  • log4j漏洞复现--vulhub
  • 基于Pyhton的人脸识别(Python 3.12+face_recognition库)
  • 自然三次样条插值推导笔记
  • Linux:动静态库
  • 图神经网络学习笔记-点云数据处理(专题七)
  • Qt 2D绘图之五:图形视图框架的结构、坐标系统和框架间的事件处理与传播