Redis 相关问题

文章目录

  • 前言
  • 一、Redis基础
    • 1、Redis的优点
    • 2、Redis这么快的原因
    • 3、Redis底层的数据结构
      • redisObject
      • 简单的动态字符串(Simple Dynamic String,SDS)
      • 链表
      • 字典
      • 跳跃表
      • 压缩列表
    • 4、Redis常用的五种数据结构
    • 5、Redis高级数据结构
    • 6、Redis做异步队列
  • 二、Redis和数据库双写一致性
  • 三、Redis持久化
    • 1、Redis的持久化机制
  • 四、Redis的淘汰和删除策略
    • 1、Redis淘汰策略
    • 2、Redis删除策略
    • 3、热点数据和冷数据
  • 五、Redis线程模型
  • 六、Redis缓存异常
    • 1、缓存雪崩
    • 2、缓存穿透
    • 3、缓存预热
    • 4、缓存更新
    • 5、缓存击穿
    • 6、缓存降级
  • 七、Redis事务
    • 1、Redis事务相关命令
  • 八、Redis分布式问题
    • 1、Redis的分布式锁
    • 2、Redis的并发竞争key问题
    • 3、RedLock
  • 九、Redis集群
    • 1、哨兵模式
    • 2、主从复制
  • 总结


前言

总结一下Redis的相关知识点。


一、Redis基础

Redis是一个数据库,不过与传统数据库不同的是Redis的数据库是存在内存中,所以读写速度非常快,因此 Redis被广泛应用于缓存方向。除此之外,Redis也经常用来做分布式锁,Redis提供了多种数据类型来支持不同的业务场景。除此之外,Redis 支持事务持久化、LUA脚本、LRU驱动事件、多种集群方案。主要应用于缓存、分布式锁、排行榜(zset)、计数(incrby)、消息队列(stream)、地理位置(geo)、访客统计(hyperloglog)等。

1、Redis的优点

  • 高性能
    操作缓存就是直接操作内存,所以速度相当快。当用于第一次访问数据从硬盘中读取,之后存入内存,下次访问从内存读取就会变快很多,如果数据更新,同步改变缓存中的数据即可。
  • 高并发
    直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

2、Redis这么快的原因

1、Redis是纯内存的数据库
2、Redis是单线程的数据库,利用队列将并发访问转换成串行访问,有效避免了频繁的上下文切换
3、Redis采用多路I/O复用技术,“多路”指多个网络连接;"复用"指复用一个线程;多路复用IO技术可以让单线程高效的处理多个连接请求。

Redis使用单线程的原因:
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)

3、Redis底层的数据结构

其底层的数据结构包括:简单的动态字符串 SDS、链表、字典、跳表、整数集合、压缩列表、对象。压缩列表是一种为了节约内存而开发的且经过特殊编码之后的连续内存块顺序型数据结构

redisObject

redisObject是Redis类型系统的核心,数据库中的每个键、值,以及Redis本身处理的参数,都表示为这种数据类型。

Redis 对象
typedef struct redisObject {
    类型,记录了对象所保存的值的类型
    unsigned type:4
    对齐位
    unsigned notused:2;
    编码方式,记录了对象所保存的值的编码
    unsigned encoding:4;
    LRU 时间(相对于 server.lruclock)
    unsigned lru:22;
    引用计数
    int refcount;
    指向对象的值,指向实际保存值的数据结构, 这个数据结构由type属性和encoding属性决定
    void *ptr;
} robj;

简单的动态字符串(Simple Dynamic String,SDS)

Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。其实SDS等同于C语言中的char * ,但它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结束,因此它必然有个长度字段。

struct sdshdr {
    记录buf数组中已使用字节的数量
    等于sds所保存字符串的长度
    int len;
    
    记录buf数组中未使用字节的数量
    int free;
    
    字节数组,用于保存字符串
    char buf[];
}
C语言字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据 可以保存文本数据或者二进制数据
可以使用所有的库中的函数 可以使用一部分库中的函数

优点:

  1. 获取字符串长度的复杂度为O(1)。
  2. 杜绝缓冲区溢出。
  3. 减少修改字符串长度时所需要的内存重分配次数。
  4. 二进制安全。
  5. 兼容部分C字符串函数。

链表

当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表建的底层实现。

typedef struct listNode {
    前置节点
    struct listNode *prev;
    后置节点
    struct listNode *next;
    节点的值
    void *value;
} listNode;

链表的特点:

  1. 链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。
  2. 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
  3. 每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
  4. 因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
  5. 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值

List的数据结构

typedef struct list {
    表头节点
    listNode *head;
    
    表尾节点
    listNode *tail;
    
    链表所包含的节点数量
    unsigned long len;
    
    节点值复制函数
    void *(*dup)(void *ptr);
    
    节点值释放函数
    void (*free)(void *ptr);
    
    节点值对比函数
    int*match)(void *ptr, void *key);
} list;

字典

字典的底层是哈希表,类似 C++中的 map ,也就是键值对。
hash的数据结构

typedef struct dictht {
    哈希表数组
    dictEntry **table;
    哈希表大小
    unsigned long size;
    哈希表大小掩码,用于计算索引值,总是等于size-1
    unsigned long sizemark;
    该哈希表已有节点的数量
    unsigned long used;
} dichht;

哈希算法
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash算法(非加密型哈希函数)。这种算法的优点在于即使输入的键是规律的,算法仍能给出一个个很好的随机分布性,并且算法的计算速度非常快。

哈希冲突的解决方式
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

特性

  1. 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
  2. Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
  3. Redis使用MurmurHash2算法来计算键的哈希值。
  4. 哈希表使用链地址法来解决键冲突。

HashMap在进行扩容时,会调用resize()函数,重新计算HashMap所需的新容量,然后重新定义一个新容器,将原数组数据进行Hash,放入新的容器中。这个过程就是rehash。Redis的rehash是渐进式的。

跳跃表

Redis 相关问题_第1张图片
如图上面的普通的单链表,查找元素的时间复杂度为O(N)。下面的就是跳跃表,查找的步骤是从头节点的顶层开始,查到第一个大于指定元素的节点时,退回上一节点,在下一层继续查找,查找效率会相对提高。

为了避免插入操作的时间复杂度是O(N),skiplist每层的数量不会严格按照(两层节点数量:一层节点数量)2:1的比例,而是对每个要插入的元素随机一个层数。随机层数的计算过程如下:

  1. 每个节点都有第一层
  2. 那么它有第二层的概率是p,有第三层的概率是p*p
  3. 不能超过最大层数

一般来说,层的数量越多,访问其他节点的速度越快。
zskiplistNode

typedef struct zskiplistNode {
    后退指针
    struct zskiplistNode *backward;
    
    分值 权重
    double score;
    
    成员对象
    robj *obj;struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } leval[];
} zskiplistNode;

zskipList

typedef struct zskiplist {
    表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    表中节点的数量
    unsigned long length;
    表中层数最大的节点的层数
    int leval;
} zskiplist;

特性:

  1. 跳跃表是有序集合的底层实现之一
  2. Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点
  3. 每个跳跃表节点的层高都是1至32之间的随机数
  4. 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
  5. 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
  6. 跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现简单。

压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一,是为了节省内存造的列表结构。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

4、Redis常用的五种数据结构

Redis常用的五种数据结构包括:字符串(String)、列表(List)、有序集合(Sorted Set)、集合(Set)、哈希表(Hash)。下图展示了 redisObject 、Redis 所有数据类型、以及 Redis 所有编码方式(底层实现)三者之间的关系:
Redis 相关问题_第2张图片
String
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。

Hash
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等。

List
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,实现分页查询,这个很棒的一个功 能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

Set
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。

当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:sinterstore key1 key2 key3将交集存在key1内。

Sorted Set
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。

5、Redis高级数据结构

HyperLogLog: 通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数据。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计(访客统计)。

Geo: Redis 3.2版本的新特性。可以将用户给定的地理位置信息储存起来,并对这些信息进行操作。获取2个位置的距离,根据给定地地理位置坐标获取指定范围内的地址位置集合。

BitMap: 位图。

Stream: 主要用于消息队列,类似于 Kafka,可以认为是 pub/sub 的进阶版。提供了消息的持久化和主从复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

6、Redis做异步队列

一般使用list结构作为队列,rpush生产消息,lpop消费消息。

缺点:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等

问题:能不能生产一次消费多次呢?
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

二、Redis和数据库双写一致性

系统不是严格要求缓存+数据库必须一致性的话,最好将读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。数据库与Redis双写一致性

缓存+数据库读写的模式,就是 预留缓存模式

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 (读取)
  • 更新的时候,采用延迟双删策略
    先淘汰缓存
    再写数据库
    休眠一会,再次淘汰缓存(可能没删除成功)
  • 消息队列
    启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
    Redis 相关问题_第3张图片

MySQL binlog 增量发布订阅消费+消息队列+增量数据更新到Redis

  1. 读请求走Redis:热数据基本都在Redis
  2. 写请求走MySQL: 增删改都操作MySQL
  3. 更新Redis数据:MySQ的数据操作binlog,来更新到Redis

三、Redis持久化

由于Redis是一种内存型数据库,即服务器在运行时,系统为其分配了一部分内存存储数据,一旦服务器挂了或宕机了,那么数据库里面的数据将会丢失,为了使服务器即使突然关机也能保存数据,必须通过持久化的方式将数据从内存保存到磁盘中。持久化就是把内存的数据写到磁盘中,防止服务器宕机了,导致内存数据丢失。

1、Redis的持久化机制

Redis提供两种持久化机制,分别是RDB和AOF。Redis服务器默认开启RDB,关闭AOF;

RDB持久化
RDB(Redis DataBase):快照。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb(二进制文件)。通过配置文件中的save参数来定义快照的周期。

优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快。与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。

缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。

AOF持久化
AOF(Append Only File):将Redis执行的每次写命令记录到单独的日志文件(文本协议数据)中,当重启Redis会重新从持久化的日志文件中恢复数据。与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。

持久化策略选择

  1. 如果Redis中的数据完全丢弃也没有关系,那么无论是单机,还是主从架构,都可以不进行任何持久化。
  2. 在单机环境下,如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF。
  3. 但在多数情况下,我们都会配置主从环境,slave的存在既可以实现数据的热备(数据库运行时备份,实时的),也可以进行读写分离分担Redis读请求,以及在master宕掉后继续提供服务。

四、Redis的淘汰和删除策略

1、Redis淘汰策略

当Redis的内存(maxmemory参数配置)已满时,它会根据淘汰策略(maxmemory-policy参数配置)进行相应的操作。

  • 不删除策略(no-eviction
    no-eviction:不删除策略。Redis默认策略。达到最大内存限制时,若需要更多内存,直接返回错误信息。

  • 最近最少使用策略(lru)
    allkeys-lru:所有key通用;优先删除最近最少使用的key
    volatile-lru:只限于设置了 expire 过期时间的部分;优先删除最近最少使用的key

  • 随机策略(random)
    allkeys-random:所有key通用;随机删除一部分key。
    volatile-random:只限于设置 expire 的部分;随机删除一部分key。

  • 剩余时间短策略(ttl)
    volatile-ttl:只限于设置 expire 的部分;优先删除剩余时间(Time to live)短的key。

  • 最不经常使用策略(lfu)
    volatile-lfu:只限于设置 expire 的部分;优先删除最不经常使用的key。
    allkeys-lfu:所有key通用;优先删除最不经常使用的key。

volatile-:从已过期时间的数据集中淘汰key。
allkeys-:所有key。

2、Redis删除策略

Redis是 key-value 数据库,可以设置Redis缓存的key的过期时间。Redis的过期删除策略就是指当Redis中的key过期了,Redis是如何进行处理的。Redis默认是惰性+定期。

  • 定时删除:在设置key的过期时间的同时,Redis会创建一个定时器,当key达到过期时间时,立即删除该键。
  • 惰性删除:放任键过期不管,只有当从键空间获取键时,才检查获取的键是否过期,若过期删除该键,不返回任何东西;若没过期,就返回值。
  • 定期删除:每隔一段时间(默认100ms),程序就对数据库进行一次检查,删除过期键。

3、热点数据和冷数据

热数据就是访问次数较多的数据,冷数据就是访问很少或者从不访问的数据。

需要注意的是只有热点数据,缓存才有价值;对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

五、Redis线程模型

Redis基于 Reactor 模式开发了自己网络事件处理器,它由四部分组成,分别是套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。它采用 IO 多路复用机制同时监听多个socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
Redis 相关问题_第4张图片
使用 I/O 多路复用程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 I/O 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

一句话总结就是:“I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。

注: I/O多路复用 参考我之前写的文中,C++ 网络编程(第四篇)

六、Redis缓存异常

1、缓存雪崩

定义: 同一时间内大量键过期(失效),后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

如何解决缓存雪崩

  1. 给缓存数据的过期时间设置随机值,防止同一时间大量数据过期。
  2. 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,若标记失效,则更新缓存数据。
  3. 并发量不大时,可以使用加锁排队。

解决思路

  • 事发前:实现Redis的高可用(主从架构+Sentinel哨兵 或者 Redis集群),发现机器宕机尽快补上,选择合适的内存淘汰策略,尽量避免Redis挂掉。
  • 事发中:设置本地缓存(ehcache)+限流(hystrix),通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。尽量避免MySQL崩掉,保证服务能正常工作。
  • 事发后:利用 Redis 持久化机制保存的数据尽快恢复缓存。

2、缓存穿透

定义: 一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法

  • 采用布隆过滤器,将所有可能存在的数据哈希到一个bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免对底层存储系统的查询压力。

布隆过滤器是引入了k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

  • 缓存空对象
    当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;如果一个查询返回的数据为空(不管是数据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

      这种方法会存在两个问题:
      1. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
    
      2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
    
缓存空对象 布隆过滤器
适用场景 数据命中不高;据频繁变化且实时性较高 数据命中不高 ;数据相对固定即实时性较低
维护成本 代码维护简单 ;需要较多的缓存空间 ;数据会出现不一致的现象 代码维护较复杂 ;缓存空间要少一些

3、缓存预热

定义: 缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据!

解决思路

  1. 直接写个缓存刷新页面,系统上线时手动将缓存数据加载;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

4、缓存更新

除了缓存服务器自带的缓存失效(淘汰)策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存删除策略,常见的策略有两种:

(1)定期去清理过期的缓存;定期删除和惰性删除 (Redis默认的删除策略)
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

缓存更新的设计模式有四种(以及上面的双写一致性):

  • Cache aside:查询:先查缓存,缓存没有就查数据库,然后加载至缓存内;更新:先更新数据库,然后让缓存失效;或者先失效缓存然后更新数据库;

  • Read through:在查询操作中更新缓存,即当缓存失效时,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载;

  • Write through:在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库;

  • Write behind caching:俗称write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库;

5、缓存击穿

定义: 指缓存中没有但数据库中有的数据,用户请求并发读缓存数据没有读到,导致所有请求都落在数据库,造成过大压力。比如大并发集中对某个 key 进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

解决思路:

  1. 设置热点数据永不过期。
  2. 利用互斥锁:在缓存失效的时,先获取锁,得到锁后再去请求数据库。没有得到锁,则休眠一段时间在重试。(一般不用)

6、缓存降级

定义: 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是(核心服务的功能无法完全保障)有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。再比如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址。

七、Redis事务

事务间相互独立:事务中的所有命令都会序列化,按顺序执行。事务在执行过程中,不会被其他客户端请求的命令中断。事务中的命令要么都执行,要么都不执行。

1、Redis事务相关命令

Redis事务功能是通过 MULTI、EXEC、DISCARD、WATCH 命令实现的。通过MULTI开启事务,然后将请求的命令入队,最后通过EXEC命令依次执行队列中所有的命令。

Redis会将一个事务所有的命令序列化,然后按顺序执行。

1. Redis不支持回滚。Redis在事务失败时不进行回滚,而是继续执行剩下的命令。
2. 若在一个事务中的命令出现错误,那么所有命令都不会执行。
3. 若在一个事务中出现运行错误,那么正确的命令会被执行。

WATCH命令: 是一个乐观锁,可以为Redis提供CAS操作。可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不执行,监控一直持续到EXEC命令。

MULTI命令: 用于开启事务。MULTI执行后,Client可以继续向服务器发送任意多条命令,这些命令会存放到一个队列中,当EXEC命令调用后,所有队列中的命令才会被执行。

EXEC命令: 执行所有事务块的命令,可以理解为提交事务。按命令的执行顺序,返回事务中所有命令的返回值。当操作被打断时,返回空值(nil)。

DISCARD命令: 用于清空事务队列,并放弃执行事务。Client从事务状态中退出。

UNWATCH命令: 用于取消WATCH命令对所有key的监控。

注: 事务执行过程中,若服务器收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中。

Redis事务的特性

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)

八、Redis分布式问题

1、Redis的分布式锁

Redis是单进程模式,队列技术将并发访问变为串行访问,且多个客户端对Redis的连接并不存在竞争关系,Redis可以使用setnx命令实现分布式锁。

获取锁时调用setnx(setnx若设置值成功,返回1;设置失败,返回0)。锁的value值会随机生成一个UUID,在释放锁时,会通过UUID进行判断是否为对应的锁,若是该锁,则释放该锁;可以使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁。使用delete删除锁。

setnx key value:只有在 key 不存在时,才将key设置为value值。
Redis 相关问题_第5张图片

2、Redis的并发竞争key问题

多个系统同时对一个key进行操作,最后执行的顺序和我们期望的顺序不同,导致结果不同。

如何解决并发竞争问题

可以通过Redis或Zookeeper实现分布式锁。

  • Redis实现分布式锁:通过Redis中setnx命令可以实现分布式锁。当获取锁时,调用setnx加锁。锁的value值会随机生成一个UUID。在释放锁时,通过UUID进行判断是否为对应的锁,若是则释放锁。使用expire命令为锁添加一个超时时间,若超过该时间则自动释放锁。
  • Zookeeper实现分布式锁:通过Zookeeper临时有序节点可以实现分布式锁。每个Client对某个方法加锁时,在Zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。通过判断有序节点中,序号是否为最小来获取锁;当释放锁时,只需要删除瞬时有序节点。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

3、RedLock

Redis 官方提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。

特性

  • 安全性:互斥访问。只会有一个Client能拿到锁资源。
  • 容错性:只要大部分Redis节点可以存活,就可以正常提供服务。
  • 避免死锁:最终Client都可以拿到锁,不会出现死锁的情况。即使原本锁住某资源的Client crash了或者出现了网络分区

设置锁的方法(和setnx差不多)

SET key_name my_random_value NX PX 30000                  
# NX 表示if not exist 就设置并返回True,否则不设置并返回False   
# PX 表示过期时间用毫秒级, 30000 表示这些毫秒时间后此key过期

这篇文章提出了一个有趣的问题,针对 RedLock 失效的问题,大家看一看Redlock(redis分布式锁)原理分析

九、Redis集群

1、哨兵模式

哨兵(Sentinel) 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。

Redis 相关问题_第6张图片

哨兵的作用:

  1. 监控redis主、从数据库是否正常运行
  2. 主数据库出现故障自动将从数据库转换为主数据库。

哨兵的核心知识

  1. 哨兵至少需要 3 个实例,来保证自己的健壮性。
  2. 哨兵 + redis 主从的部署架构,是不保证数据零丢失的只能保证 redis 集群的高可用性
  3. 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
  4. 配置哨兵监控一个系统时,只需要配置其监控主数据库即可,哨兵会自动发现所有复制该主数据库的从数据库。

2、主从复制

当从数据库启动时,会向主数据库发送 sync 命令,主数据库接收到 sync 后开始在后台保存快照rdb,在保存快照期间收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从数据库。复制初始化结束。之后,主数据库每收到1个命令就同步发送给从数据库。 当出现断开重连后,2.8之后的版本会将断线期间的命令传给从数据库。增量复制。

主从复制是乐观复制,当客户端发送写执行给主数据库,主数据库执行完立即将结果返回客户端,并异步的把命令发送给从数据库,从而不影响性能。


总结

参考文章:
数据库和redis双写一致性
Redis面经汇总
菜鸟教程:Redis 列表(List)
Redis系列 | 缓存穿透、击穿、雪崩、预热、更新、降级
Redlock(redis分布式锁)原理分析

你可能感兴趣的:(数据库,redis,缓存,数据库)