Redis总结

Redis总结

什么是Redis 有啥用?

  • 本质上是一个Key-Value类型的内存数据库,常用来做数据的缓存。Redis是单线程的,多线程通过多路复用+单线程的方式实现,本质还是单线程
  • 优点是读写度的快(10w/s读 8w/s写),与其他关系型数据库不同的是,Redis支持多种数据结构,Redis单个value最大限制是512M
  • 缺点是数据库容量受限于物理内存,不能海量存储数据

Redis的原子性

  1. 原子操作是指不会被线程调度机制打断的操作,Redis的原子性的得益于Redis的单线程。
  2. Redis中,只要在一条指令中完成的操作都可以认为具有原子性。
  3. 指令之间不具有原子性,可能发生中断。

Redis支持哪些数据类型,并简述特点?

  1. String

    • Redis最基础的数据类型。一个key对应一个value。

    • String是二进制安全的,意思是string可以包含任何数据。比如图片等

    • 查询复杂度O(1)

  2. list

    • 简单的字符串列表,可以从左边或者右边添加删除元素。

    • 有序、可以重复

    • 底层是由双向链表实现的

      image-20211223203633499
    • 可以用来做简单的消息队列

    • ZipList

      • 在列表元素较少的时候使用连续的内存存储,超过某个值之后再用链表操作。因为存储大量短小的数据用链表的话,会造成内存浪费。

      • 底层通过quicklist来实现

        image-20211223212558077

    img img
  3. Set

    • string的无序集合

    • 无序、不可重复

    • 用来全局去重

    • 它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

      img img
  4. ZSet

    1. string的有序集合, 输入时有一个sort值,会根据sort的值来排序

    2. 集合的成员是唯一的,但是sort值可以是重复

      img
  5. hash

    1. hash的键还是键,值是一个键值对

      img
  6. Bitmaps

    1. bitmaps本质上不是一种新的数据结构, 实际上它就是字符串(key-value)。

    2. Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

    3. 对位进行操作。大大减少了数据在内存的存储空间。

    4. 可以用来做只有两种状态的数据的存储。

      image-20211223213057957

  7. hyperloglog

    1. 统计不重复元素的个数

redis是单线程的吗?为什么执行速度这么快?

redis是单线程的。执行速度快原因:

  • redis基于内存存储
  • redis是单线程的,没有多线程的上下文切换
  • redis使用了多路IO复用的线程模型,一个线程监控多个IO流
  • redis是轻量级的内存数据库,对外部依赖较少

redis的持久化方式有哪些?

RDB

RDB是在指定时间间隔内,将内存中的数据(快照)转存到磁盘中的策略。恢复时,将磁盘中的数据读取到内存中。保存文件为dump.rdb

有两种触发的方式:save和bgsave。

  • save指令会阻塞Redis服务,当转存大量数据时不方便。
  • bgsave指令会先创建子进程,将数据复制到子进程中。由子进程进行数据的存储。阻塞只会发生在创建子进程过程中。一般时间很短
  • 系统默认是执行bgsave的方式

优点:

  • RDB是紧凑的二进制压缩文件。保存了某个快照的全部数据。
  • 恢复速度较快。适用于备份,全量复制,容灾等场景

缺点:

  • 在最后一次保存之后,发生意外容易造成数据丢失。
  • 无法实时持久化
执行过程

img

AOF

AOF的策略是每次将命令追加写入日志中, 当需要恢复的时候就把日志中的AOF重新执行。是目前主流的Redis持久化方式。官网建议两个都开

同步策略:

  • always 每次写入缓存区都同步到AOF文件中。 限制了Redis的高并发
  • no 写入缓存后不同步,由操作系统负责同步。 增大了每次的同步量,且同步周期不可控
  • everysec由专门的线程每秒钟同步一次。

优点:

  • 写入性能高,文件不易损坏。

  • AOF文件体积变得过大时,自动的在后台对AOF进行重写。新文件仅保存构成当前数据集的最小指令集。(就是去掉一些对数据集没操作的指令)

    创建新AOF文件的时候,老的AOF文件还是照常写入。即使重写过程中发生停机,老的AOF文件也不会丢失。而一旦新AOF文件创建完毕,再交换新老日志文件即可。系统默认的重写阈值是64M。(在重写过程中新增加的指令添加到新日志文件中会造成Redis阻塞)

  • AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

缺点:

  • AOF文件通常比RDB数据快照文件大
  • 恢复大数据集的时候较慢
执行过程
img

Redis的主从复制

主从复制是一种将Redis的读写分离在不同服务器上的策略。分为主服务器和从服务器。主服务器负责写,从服务器负责读。将写好的数据复制到从服务器上。

image-20211224102903251

作用:

  • 数据热备份
  • 故障恢复:主节点出现问题时,可以由从节点提供服务。
  • 负载均衡

常用策略

一主二从

主机断开连接后,从机状态保持不变;

主机恢复连接后,从机自动重连主机

从机断开连接后,无法继续获取主机信息

从机恢复连接后,身份转变为主机,无法获取原主机信息

薪火相传模式

前一个结点是后一个节点的主节点,而后一个节点又是前一个节点的从节点。

可以有效减轻master的写压力,去中心化降低风险。

风险是一旦某个slave宕机,后面的slave都没法备份

img
反客为主

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。

哨兵模式

img

每个哨兵以每秒钟一次的频率向它所知的主机,从机以及其他哨兵实例发送一个PING命令 。如果未接受则视为主机下线。提前设置至少多少个哨兵检测下线的数量N,当超过N个哨兵检测到主机下线就标为客观下线,开启自动故障迁移。

主观下线就是单个哨兵检测到主机下线,客观下线就是多个哨兵检测到主机下线。

自动故障迁移(Automatic failover)

  • 从失效的主机的从机中选一个升级为新主机,并让其他从机复制新主机。

  • 当客户端连旧主机时,返回新主机的地址。

    image-20211224111115472(偏移量是指获得原主机数据最全的,每个redis实例启动后都会随机生成一个40位的runi)

复制原理

  1. 从机启动成功连接到主机后会发送一个sync命令

  2. 主机r接到sync命令后,收集所有用于修改数据集的命令。 在修改数据集指令执行完毕后。主机将整个数据文件发送到从机。完成一次全量复制

  3. 全量复制:接受全部数据,加载到内存中

    增量复制:主机将收集到的新的修改数据集的命令发送给从机,完成数据同步。

    只要是重新连接master,自动执行一次全量复制

    image-20211224111818210

Redis集群相关的

Redis中的事务的理解

Redis事务本质是命令的集合。通过将命令存放到串行化的队列中。事务执行过程中,会按照顺序执行队列中的命令,其他指令不会插入到事务的执行顺序中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令

  • 单独的隔离操作

    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

  • 没有隔离级别的概念

    批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。

  • 不保证原子性

    事务中如果有一条命令执行失败,其他正确的命令仍然会被执行,没有回滚。

    如果队列中指令编译错误,则全部指令都不会被执行。

    Redis的单个指令是原子性的,但事务没有原子性

与锁结合

作为WATCH命令的参数的键会受到Redis的监控,Redis能够检测到它们的变化。在执行EXEC命令之前,如果Redis检测到至少有一个键被修改了,那么整个事务便会中止运行,然后EXEC命令会返回一个Null值,提醒用户事务运行失败。

Redis中锁的机制

乐观锁

就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

悲观锁

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。****传统的关系型数据库里边就用到了很多这种锁机制****,比如****行锁****,****表锁****等,****读锁****,****写锁****等,都是在做操作之前先上锁。

分布式锁(待补充)

目的:保证一个方法在高并发情况下只被一个线程执行

用途:

(1)允许多个客户端操作共享资源
这种情况下,对共享资源的操作一定是幂等性操作,无论你操作多少次都不会出现不同结果。在这里使用锁,无外乎就是为了避免重复操作共享资源从而提高效率。
(2)只允许一个客户端操作共享资源
这种情况下,对共享资源的操作一般是非幂等性操作。在这种情况下,如果出现多个客户端操作共享资源,就可能意味着数据不一致,数据丢失。
image-20211224142442616

实现分布式锁

redis使用setnx lock加锁,使用del lock释放锁就可以了。但是如果还没释放锁,服务中途就挂了,这样锁得不到释放造成死锁。可以加上超时时间,等服务启了在释放锁。

由于Redis指令间没有原子性,setnx和expire两个指令之间可能出现问题,redis在2.8以后加入set扩展命令,使得这两个命令能一起执行。

可能出现的问题:

加锁或者释放锁的期间,业务1逻辑在锁限制时间内未执行完,锁被自动释放,业务2重新持有锁 执行过程中业务1执行完毕,释放锁会直接释放业2锁。

这时候需要lua脚本执行。确保原子性。

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性。

Redis中可能出现的问题,怎么解决?

缓存击穿

缓存击穿是某个热度很高的Key被大量访问,当这个Key失效的瞬间。持续高并发访问就直接请求数据库。

是因为某个Key失效引起,大量访问数据库,导致缓存破了个洞。叫做缓存击穿

解决方案
  • 预设热门数据

  • 动态延长热门数据的key过期时间

  • 当访问没有命中时,先将该key上锁。如果上锁成功,访问数据库并回设缓存,最后删除锁。如果上锁失败,说明有线程正在访问数据库,当前线程睡眠一段时间再请求缓存。

缓存穿透

是因为访问了不存在的数据,使得缓存失效,叫做缓存穿透)

用户想要查询一个数据,缓存没有命中,于是向数据库查询,发现也没有,于是本次查询失败。当大量用户缓存没有命中,都去请求数据库,给数据库造成巨大压力。就是缓存穿透。

​ 与缓存击穿的区别是,缓存击穿是某个热度很高的Key被大量访问,当这个Key失效的瞬间。持续高并发访问就直接请求数据库。

image-20211224144632359
解决方案
  • 对空值缓存

    ​ 简单粗暴,如果查询DB返回的数据为空,我们仍然把这个空值放到Redis缓存中,只是将它的过期时间设置的很短,另外为了避免不必要的内存消耗,可以定期清理空值的key。

  • 设置白名单

    使用bitmaps保存可访问的名单。名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

  • 布隆过滤器

    布隆过滤器是一个二进制向量,每一位存放的是0或1,初始时默认为0,长下面这样:555a5d8728b507ef00bad93b83f8312

    当一个元素加入集合时,通过 K 个 Hash 函数将这个元素映射成 k 个值 :K1、K2、K3…,把向量中下标为K1、K2、K3…的位置置为1 。

    对每个可能出现的数据都映射到这个bitmaps中。当访问数据时,先比较要访问的数据的映射值在bitmaps中是不是全为1,如果不是则不可能出现在数据库中,直接拒绝访问。

    布隆过滤器也可能存在一定误判率,但可以拒绝大部分恶意访问。

  • 规范Key命名

    规范key的命名,并且统一缓存查询的入口,在入口处对key的命名格式进行检测,过滤掉不规范key的访问,这样可以过滤掉大部分的恶意攻击。

缓存雪崩

大量key在相同的时间过期,导致大量请求访问数据库。

雪崩与击穿的区别在于:雪崩是指大量key同时过期,击穿是指某个高热度的Key失效。

解决方案
  • 多级缓存架构

    nginx缓存 + redis缓存 +其他缓存(ehcache等)

  • 将热点数据均匀分布在不同搞得缓存数据库中

  • 缓存时间分散

    缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

Redis中过期键删除策略?

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。

    该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。

    该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。

    通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis 如何做内存优化?

内存淘汰策略

全局键空间选择性移除
  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
  • olatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

Redis分区是什么?为什么要这么做?

Redis分区是在一个服务器部署多个Redis实例,并把他们当作不同的服务器来使用。

目的是让redis管理更大的内容存,也可以提高Redis 的计算能力

实现方案

  • 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。

  • 代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy

  • 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

缺点:

  • 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
  • 同时操作多个key,则不能使用Redis事务.
    分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
  • 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

Redis内存回收

LRU

将最久未使用的一页淘汰

image-20211224154103129

引用计数算法

就是对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,同时将该对象引用的其他对象的计数器进行减一操作。

算法的优点

使用引用计数器,内存回收可以穿插在程序的运行中,在程序运行中,当发现某一对象的引用计数器为0时,可以立即对该对象所占用的内存空间进行回收,这种方式可以避免FULL GC时带来的程序暂停,如果读过Redis 1.0的源码,可以发现Redis中就是在引用计数器为0时,对内存进行了回收

算法的劣势

采用引用计数器进行垃圾回收,最大的缺点就是不能解决循环引用的问题,例如一个父对象持有一个子对象的引用,子对象也持有父对象的引用,这种情况下,父子对象将一直存在于JVM的堆中,无法进行回收,代码示例如下所示(引用计数器无法对a与b对象进行回收):

Redis与Memcached的区别

两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

image-20211224153255815
  1. memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
  2. redis的速度比memcached快很多
  3. redis可以持久化其数据

如何保证缓存与数据库双写时的数据一致性?

image-20211224154522936

  1. 先更新数据库,后更新缓存

    这种场景一般是没有人使用的,主要原因是在更新缓存那一步,为什么呢?因为有的业务需求缓存中存在的值并不是直接从数据库中查出来的,有的是需要经过一系列计算来的缓存值,那么这时候后你要更新缓存的话其实代价是很高的。如果此时有大量的对数据库进行写数据的请求,但是读请求并不多,那么此时如果每次写请求都更新一下缓存,那么性能损耗是非常大的。

    举个例子比如在数据库中有一个值为 1 的值,此时我们有 10 个请求对其每次加一的操作,但是这期间并没有读操作进来,如果用了先更新数据库的办法,那么此时就会有十个请求对缓存进行更新,会有大量的冷数据产生,如果我们不更新缓存而是删除缓存,那么在有读请求来的时候那么就会只更新缓存一次

  2. 先更新数据库,后删除缓存

    和第一种情况是一样的

  3. 先更新缓存,后更新数据库

    preview

    此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

    1. 请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作
    2. 此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中
    3. 但是此时请求 A 并没有更新成功,或者事务还未提交

    那么这时候就会产生数据库和 Redis 数据不一致的问题。如何解决呢?其实最简单的解决办法就是延时双删的策略。

    preview

    但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

    img

    此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

    1. 请求 A 更新操作,删除了 Redis
    2. 请求主库进行更新操作,主库与从库进行同步数据的操作
    3. 请 B 查询操作,发现 Redis 中没有数据
    4. 去从库中拿去数据
    5. 此时同步数据还未完成,拿到的数据是旧数据

    此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

    img
  4. 先删除缓存,后更新数据库

    问题:这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

    e68cfb09b595be56db3ef36099c83e9

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  1. 请求 A 先对数据库进行更新操作
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将Redis 的 key 作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

0dc9b6465dbed350beb3f6ffa5c55f9

总结

每种方案各有利弊,比如在第二种先删除缓存,后更新数据库这个方案我们最后讨论了要更新 Redis 的时候强制走主库查询就能解决问题,那么这样的操作会对业务代码进行大量的侵入,但是不需要增加的系统,不需要增加整体的服务的复杂度。最后一种方案我们最后讨论了利用订阅 binlog 日志进行搭建独立系统操作 Redis,这样的缺点其实就是增加了系统复杂度。其实每一次的选择都需要我们对于我们的业务进行评估来选择,没有一种技术是对于所有业务都通用的。没有最好的,只有最适合我们的。

Redis面试题

实就是增加了系统复杂度。其实每一次的选择都需要我们对于我们的业务进行评估来选择,没有一种技术是对于所有业务都通用的。没有最好的,只有最适合我们的。

Redis面试题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值