zoukankan      html  css  js  c++  java
  • Redis最佳实践及核心原理

    Redis最佳实践以及原理剖析

    背景

    最近开始总结redis相关知识,虽然网上也有挺多资料的,自己也看过不少,但毕竟是别人的,只看还是太空洞了。于是就自己总结一番,计划有两部分,本篇关于redis基本原理以及常见适用场景,后面会有剖析源码。

    Redis作为一款十年前(2010)诞生的no-sql数据库,讲道理还是比较年轻的。因为是基于内存的,所以速度还是相当快的。常用于对一些访问频率比较高的数据做缓存,或者利用redis数据结构特点处理一些特定业务场景。

    常见数据类型

    redis提供了5大基本数据类型,以及额外三种扩展数据类型。

    注:redis是no-sql数据库,所有数据类型都至少有一个key

    首先是五大基本数据类型,基本类型应该全部都要求熟悉。

    • String类型
    image-20211125162740127

    此类型是使用最多,且最简单的,一个key对应一个value。

    常用命令有以下:

    中括号内代表实际输入的值

    命令 说明
    set [key] [value] 设置指定key的值(如果有则覆盖)
    get [key] 获取指定key的值
    getset [key] 设置指定key的值,并返回旧值
    setnx [key] [value] 只有当指定key不存在时才设置值
    del [key] 删除指定key以及值
    • Hash类型
    image-20211125163927027

    其实就是string类型的套娃,你可以看作是一个key的value由很多个string类型组成。

    • list类型

    跟hash不同的是,它一个key直接对应了多个value,按照插入顺序保存,并且支持索引来操作。

    image-20211125164154183
    • set类型

    与list唯一不同的是,它对应的value无法重复,且是无序的。可以利用它来做一些集合相关的操作。

    命令参考:set操作命令

    • zset类型

    其实我更愿意理解它为hash的变种,只不过它的子key只能为数字,且value不能重复,zset可以自动根据你的子key数字来排序。它的功能与set类似。

    一些额外数据类型

    • geo(了解)

    主要用于存储地理位置信息,并对存储的信息进行操作

    • stream(了解)

    主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

    简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。

    而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

    • bitmap(重点)

    通过位来存储,表示某个数字是否存在与其中。

    听起来可能有点一脸懵逼,其实就是通过1或0来表示某个值的状态,它们占用只有一个bit。

    比如:你想表示1、2、4、6四个值的状态,那么就可以这样存:

    image-20211125170401506

    由于bit占用空间非常小,(8bit = 1byte ,1024byte = 1kb),理论上表示100亿个值的状态也占用差不多1g的内存。

    一个面试必问的经典问题:redis为什么那么快?

    1. 基于内存

    虽然redis进行查询修改的操作都是单线程,当架不住人家都是在内存里面操作,所以它天生就可以很快。

    1. 采用了多路复用的io模型

    io多路复用具体我会在后面的源码篇将。简单说下大概原理:

    我们知道redis的读写操作都是基于内存的,所以它的瓶颈并不在磁盘io(内存也可以看作是一种磁盘)这里,而它本身也没有什么计算复杂的逻辑,所以肯定也不会在cpu这里。

    最后只剩下网络请求了,由于网络请求相比于内存操作慢很多,所以redis最大的性能瓶颈其实是在建立连接和返回响应这里,导致可能redis在很长时间里会处于等待网络请求中。

    其实这里就跟cpu到缓存到内存再到磁盘的原理差不多,缓存和内存的出现就是因为磁盘、内存的速度远远跟不上cpu的速度,所以缓存就成了它们之间的中间,以至于不会让cpu一直等待内存或者磁盘的操作结束,从而让cpu效率更高。

    多路复用机制有着异曲同工之妙,它其实也是在内存中有一个队列,专门存放客户端的请求,并且redis不会一直来轮询这些请求那一个到达了,而是基于select/epoll提供的时间回调机制,只有队列里有请求真正到达后,就立马执行。这样一来,在高并发下,队列里就会有源源不断的请求存放,redis就会一直处于运行状态,不会傻傻的等到请求真正到达后才去执行。

    redis的持久化

    redis主要包括rdbaof两种持久化方式,它们各有优劣,也谈不上谁好谁坏,只有最合适。

    RDB持久化

    rdb就是通过配置的某种策略,将所有数据保存到磁盘文件,这个文件是那一刻的全量数据,并且是以二进制形式保存,理论上恢复时效率最高。

    redis还提供了两种进行rdb快照持久化的操作。

    1. save命令

    我们在任何时刻都以直接输入save命令来进行rdb持久化,不过此刻会阻塞后面所有的客户端请求。

    1. bgsave命令

    redis会fork一个子线程来进行rdb持久化,这里借助了操作系统提供的写时复制操作,大概就是:当此时主线程进行的是读操作,那么子线程就直接写入到文件,如果主线程进行写操作,那么子线程会将那一块数据复制一份,再写入文件。

    注意:子线程进行写入文件时,是可以共享读取主线程内存数据的,内存数据它是一块一块读,然后写入,如果读到某一块,发现主线程在进行写操作,它就会直接复制一份,然后再慢慢写入文件,可能你会觉得复制一份有点多余,但是如果不复制,你的数据就是乱的(可能会被主线程不停修改),因为进行文件io操作远比内存慢。可能你还会觉得,在主线程修改完后,会回过头来再将当时被子线程复制的数据也修改吗?答案是不会,因为没必要,我们只是备份某一刻的数据,如果再修改,那就没完没了了(因为这个数据可能会一直变,你也一直改吗?不就成为实时备份了)

    AOF持久化

    有点类似数据库的redolog,aof模式下的持久化存到文件里面的其实就是每一个命令,我们可以设置策略来进行触发,主要有三种模式:

    1. appendfsync always: s每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
    2. appendfsync everysec: 每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
    3. appendfsync no: 从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

    额外:混合持久化方式(since4.0)

    此模式是基于aof的

    由于aof是保存的全量操作,在恢复的时候会很慢。混合方式就是利用了rdb的迅速恢复。它会在进行持久化时,首先将那一刻进行rdb全量持久化,然后将后续操作以aof形式追加,这样一来既兼顾了恢复速度,又兼顾了数据完整性。

    redis的常见应用场景

    各种缓存服务

    适合查询比较频繁且不易变动的数据

    关于缓存服务的一些问题

    1. 缓存击穿

      指缓存失效,导致大量请求直接打入数据中。

      解决方案:利用锁实现缓存过期后只有一个线程请求数据库并写入到缓存,其他未获取到锁的线程可以尝试自旋或者是直接休眠x毫秒(x毫秒取决于具体业务查询时间)然后再尝试获取缓存。

      ps:自选还是休眠效率高实际取决于并发竞争程度,可根据实际场景测试。理论上并发没那么高可以选择自旋,并发较高选择休眠。

    2. 缓存雪崩(缓存击穿的特例)

      缓存雪崩指同一时间大量缓存同时过期,导致大量数据查询直接进入数据库,从而使数据不堪重负宕机。

      解决方案:设置缓存时间不过期或者将缓存时间设置为随机过期时间

    3. 缓存穿透

      指查询缓存和数据库都没有的数据,属于非正常请求。

      解决方案:限制ip短时间内查询接口的频率;将数据库不存在的数据也添加到缓存

    缓存更新策略

    常见的缓存更新策略有以下:

    1. 先更新数据库,再删除缓存

      这是目前最常见也是最简单的方式,通常来说没啥问题,但还是有小概率发生数据不一致的情况:线程a查询缓存没有,就查询数据库,再准备更新缓存时,线程b已经修改了数据并成功更新了缓存,这时线程a就更新了旧数据的缓存。

      ps:其实对于一般项目来说可以说几乎不会发生这种情况,因为发生的概率还是很低的,需要同时满足:1. 读取缓存为空 2. 同时有修改的操作 3. 读操作比修改操作更慢且修改操作的写入缓存早于读操作写入缓存

    2. 先更新数据库,再更新缓存

      其实理论上这种方式才是正常操作,但是业内几乎没人这么做。主要有两点原因:1是你更新了缓存也不一定会被访问到,删除了相当于起到懒加载的效果,同时也一定程度节省了内存开销 2是有些数据结构在redis修改的代价要高,为考虑性能,直接删除更好

    3. 缓存代理

      其实就是把缓存当作主要数据库,直接对缓存进行修改查询操作,然后同步或异步更新到数据库

    理论上,只要使用到缓存,基本是不可能保证缓存数据与数据库绝对的一致性的,不过我门可以通过一定策略来减轻数据不一致效果。

    比如:延迟双删

    简单来说就是更新数据库前删除缓存,更新数据库后延迟一定时间再删除缓存,这样只能说是能降低一定几率出现数据不一致的情况,但是因为多了一步延迟删除的操作,高并发下对吞吐量有一定影响。

    毫秒级响应判断亿级用户签到情况

    首先来看一个业务场景:系统中日活越有1亿用户,我们需要统计这些用户每天的签到情况。

    这时候我们就可以运用bitmap这一数据结构来实现。

    将日期作为key,然后将用户id存入bitmap。判断某一天某个用户签到与否,只需根据日期key来判断用户id value是否存在就行。

    简单抽奖系统

    我们可以利用redis集合的sRandMember命令(随机返回一个集合中的元素)或者sPop命令(随机删除并返回删除的元素),例如,我们可以设置1到100数字到集合中,然后定义小于等于10为中奖,再利用上述命令判断取出的元素是否为中奖。

    进阶:高可用架构

    主从架构

    image-20211222112017603

    虽然redis单机并发性能也很高,当时当我们的业务发展规模起来后,单机性能可能也达不到,或者如果redis突然宕机,我们也束手无策。

    所以,主从模式就诞生了。他的模式大体为上图所示。

    核心是:主库负责写操作和同步从库,而从库负责读操作

    做到了读写分离,性能提升。即使从库突然宕机,我们也还有其他从库。

    主库和从库是怎么样同步数据的?

    1. 全量同步

      一般发生在初始阶段,由从库发送同步命令到主库,主库通过rdb方式向从库同步数据。此时主库依然可以正常接受请求,并将命令缓存到replication buffer中,当rdb同步完成再将命令发送给从库同步。

      一般来说,只要第一次主从连接后,就会进行一次全量同步数据,基本是只要后续没有出现断开连接的现象, 后续都是通过同步命令的方式进行同步数据。

      replication buffer本质就是一个记录写命令的缓存区,理论上在主库上会有多个,取决于与之相连的从库数量;不仅如此,每有一个与之相连的客户端,都会存在一个replication buffer

    2. 增量同步

      通常来说,在主库与从库失去连接后,重连后一般会重新进行全量同步,不过在redis2.8开始,支持了增量同步,可以在同步过程中断开重连后接着同步,无需重新全量同步。

      这里需要注意【增量同步】只针对已经完整进行过一次【全量同步】的情况下才会触发。如果正在进行【全量同步】,断开连接重新连接后,依然需要重新进行【全量同步】

      增量同步实现原理:

      redis有一个repl_backlog_buffer缓冲区,是一个环形结构,可以保存写请求命令。用偏移量来定位主库和从库已经执行的命令的位置。

      img

      如图,刚开始主库和从库一般都是一起的,在从库断开连接后,主库偏移量可能在不断增加,等下次从库连接,就可以只同步从库与主库偏移量之间的命令了。

      不过很明显这里有一个问题,如果从库断开连接时间比较长,因为是一个环,可能就会覆盖之前的记录。

      所以需要我们综合评估设置环的大小来尽可能避免这种情况。

    对于repl_backlog_buffer的理解。本质其实跟replication buffer一样,都是缓存写入命令的,它会同时和replication buffer一起生成,不过它在整个主库中只有一份。它的结构相当于一个环,并且记录着主库已经执行命令的偏移量。值得一提的是,从库同步主库命令的偏移量由从库自己记录。

    关于repl_backlog_bufferreplication buffer的区别可见下图:

    img

    主从复制风暴问题

    看起来主从复制模式很完美,但当我们的从节点很多时,主节点需要向每一个从节点同步数据,这在高并发下会极大的影响性能,就有违我们的初衷了。

    不过,由于redis是支持同时是主库和从库的,我们可以使用主从联级模式来分担主库同步的压力。

    img

    哨兵架构

    可能你会发现,上述我们讨论的架构仿佛在默认主库就一定稳定的情况下,倘若我们主库宕机了呢,好像就变得群龙无首了。

    所谓有需求就有解决策略,所以,我们的哨兵模式就诞生了。它是专门解决主从架构中主库宕机的情况。

    哨兵架构整体流程如何?

    简单来说,哨兵的工作就是监控选主通知

    1. 监控

      哨兵负责定期向主库发送心跳检测来判断主库时候宕机。通常是同时存在多个哨兵,来防止因网络误差情况下的“误判”。只要有多数哨兵都认为主库挂机,所有才会真的认为它挂机。

    2. 选主

      当主库挂机后,哨兵集群就开始重新选主了,它会有一个打分机制,分为三次打分来综合评分。在打分前,我们会有一个海选,初筛掉一批明显不合格的从库。比如,那些已经宕机的从库,会被剔除掉;还有一些可能当时在线,但是之前已经掉线许多次的从库,我们有理由相信它接下来可能还会继续掉线。

      海选完了,我们就会对剩下的从库进行打分了。

      • 第一次打分。

        我们可以给每个从库设置slave-priority(越小优先级越高),然后会根据优先级程度进行打分。

      • 第二次打分。

        根据每个从库的数据同步情况来打分。在主从同步过程中,每个从库都会维护一个偏移量来表示与主库同步的命令进度(上文有详细介绍),偏移量是递增的,谁大就代表谁数据越接近主库。

      • 第三次打分。

        每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。为什么呢?因为从库的id也是递增的,越小就代表越早连接,也就是存活时间最长,也就最稳定。

      1. 通知

        主库选出后,哨兵将通知各个从库和已经连接的客户端。

  • 相关阅读:
    正则表达式的三种模式【贪婪、勉强、侵占】的分析
    php实用的文件上传类
    php简单实用的验证码生成类
    phpstorm不安装apache就可以本地测试PHP
    Ajax技术——带进度条的文件上传
    Mybatis 多表查询及查询结果映射
    关于textarea包在div的自适应问题
    Luogu P3200 [HNOI2009]有趣的数列
    群&置换群&burnside
    卡特兰树
  • 原文地址:https://www.cnblogs.com/lovelylm/p/15600984.html
Copyright © 2011-2022 走看看