【Redis】redis小知识随手记

234 阅读13分钟

生产环境大key的正确删除姿势

当生产环境去删除一个大key的时候,可能会造成线上阻塞,这是一个非常危险的操作。

解决方法

  1. 低峰期删除可以有效降低损失
  2. hash key使用scan分批删除,zset和set每次获取一部分进行删除,list直接使用pop进行清空
  3. redis4.0支持unlink异步删除,不阻塞主线程。

缓存穿透、击穿、雪崩如何解决

缓存穿透

缓存穿透指的是对非法数据(如id = -1)的获取,缓存内不存在,数据库内也不存在,造成两次请求都没获取到数据而对服务端造成的压力。

image.png

解决方法

  1. 前端校验直接拦截非法请求

  2. 后端校验验证参数的合法性

  3. hash拦截 或者 位图拦截 内存中维护数据是否存在的hash表或者位图 如map["商品ID"]=1 ,但是需要为所有数据都开辟空间

  4. 布隆过滤器(维护一个bit数组或向量,使用多次hash函数对bit数组进行标记,若请求参数的布隆过滤校验存在一个bit位未进行标记则不存在)过滤,不能保证百分百正确(bit位会被多次置位),但能拦截绝大部分,hash函数越多性能越差,但准确性越高,bit数组越长越准

缓存击穿

热点数据过期造成的某一时刻请求都打到了db造成db压力

image.png

解决方法

  1. 加锁,可以是单机锁,也可以是分布式锁,也可以是一个信号量,总而言之就是控制并发数量
  2. 热点数据永不过期
  3. 二级缓存,可以是内存缓存,也可以是其他数据库缓存

缓存雪崩

大量key同时失效,对db造成的压力,和缓存呢击穿的区别就是,缓存击穿是一个key,大量请求,缓存雪崩是大量key,大量请求。

image.png

解决方法

  1. 根据场景加锁,控制到达db的并发请求数量
  2. 二级缓存,同缓存击穿
  3. 缓存时间随机,减少同时失效的可能
  4. 热点数据永不过期

redis分布式锁

redis分布式锁基于 setNXexpire设置超时时间

key 和 value设计

key根据业务设置,是锁的唯一标志,value 应该为线程的唯一标志,如线程名等,多机场景下可以加上设备号,或者唯一标志线程的id等。

加锁的原子性

setNX key value 设置key value,仅当key不存在时才成功
exprire key seconds 为key设置超时时间

因为可能会存在setNX成功,exprire失败造成了分布式锁永远不会释放因此需要将两个命令合并 redis提供SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]将加锁逻辑变为原子操作

解锁的原子性

解锁方式有两种:一种是到期后自动释放 一种是线程主动释放。
因为存在一种可能,就是线程A先获取到了锁,然后业务执行时间超过了锁有效时间,导致了锁自动释放,而线程B在自动释放之后获取到了锁,此时线程A完成了业务,主动释放了锁,导致线程B的锁释放失败。
为了避免这种情况,需要保证释放锁的线程是持有锁的线程,因此需要value具备身份识别的能力,比如线程id、客户端id、uuid等只有持锁线程能够获取到的信息。
解锁分为两个步骤

  1. get key 获取value 判断是否是持锁线程
  2. del key 删除锁

虚代码如下

String uuid = xxxx;
// 伪代码,具体实现看项目中用的连接工具
// 有的提供的方法名为set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
    // unlock
    if(uuid.equals(redisTool.get('Test')){
        redisTool.del('Test');
    }
}

上述代码存在一个问题就是get和del是两个命令,因此是非原子的,可能导致get时value还是当前线程,del时可能因自动释放锁已经被另一个线程获取。此时需要保证整个解锁过程的原子性,需要使用lua脚本。 代码如下

-- lua删除锁:
-- KEYSARGV分别是以集合方式传入的参数,对应上文的Test和uuid。
-- 如果对应的value等于传入的uuid。
if redis.call('get', KEYS[1]) == ARGV[1] 
    then 
	-- 执行删除操作
        return redis.call('del', KEYS[1]) 
    else 
	-- 不成功,返回0
        return 0 
end

锁续期

对于key超时时间的设置应该基于压测时的任务执行时间的数倍来决定,换句话讲业务时间应当短于锁过期时间,但是若存在特殊情况下业务还未完成的情况下,不应该让锁自然释放

解决方法

image.png

创建一个后台线程(watch dog看门狗),定时检索分布式锁的实效时间,达到一定阈值后对分布式锁的超时时间进行重置。若担心死锁问题,可以设置最大的锁续期次数,如果达到该该次数后仍然未完成业务,则锁失效的时间设置存在问题,或是业务代码存在问题。

redis主从模式

旧版主从复制

旧版的主从复置包含两个步骤

  1. 同步
  • 从服务器 连接到 主服务器时 向主服务器发送 SYNC 命令 请求同步
  • 主服务器 收到命令执行 BGSAVE 命令 生成rdb文件,发送给从服务器,并同时记录这个过程中产生的写命令到缓冲区,完成rdb文件发送后把缓冲区的写命令也发给从服务器
  • 从服务器 收到rdb文件后载入rdb文件初始化至主服务器生成rdb文件时的状态,再将收到的缓冲区写命令也执行完成同步
  1. 命令传播 主从完成同步后,因为redis只有主服务器提供读写服务,因此客户端产生的读写命令只影响主服务器,如key的新增、删除等均会造成主服务器状态的改变,而从服务器则不会改变。因此主服务器会作为从服务器的客户端,向从服务器发送相同的命令来保持主从服务器状态一致

PS: 旧版主从复制的主要缺陷,无论是初次复制还是断线后重连复制,都需要像主服务器发送 SYNC 命令 ,因为需要生成RDB文件非常消耗主服务器资源,而断线重连后主从服务器的大部分状态都是一致的只有部分数据存在不一致,没必要进行全量同步。

新版主从复制

新版主从同步使用 PSYNC 命令 代替 SYNC 命令 具有 完整同步部分同步两种模式,完整同步和旧版主从复制基本相同,都是获取RDB文件和缓冲区命令来实现全量的状态同步,而部分同步则是从记录一个偏移值,来记录状态位置,从偏移值开始获取主服务器复制积压缓冲区的写命令缓冲来完成部分同步

复制积压缓冲区

主服务器维护有个大小固定的复制积压缓冲区,保存一段时间内的写命令,每一个写命令对应一个偏移量,先进先出,从服务器维护的偏移量若不存在于复制积压缓冲区则需要进行完整同步。

PSYN命令

PSYNC <runId> <offset>

image.png

参数

参数:runId 代表主服务器的运行id主服务器可以判断从服务器所处的状态是否还是当前状态,或是否之前复制的是当前服务器。
参数:offset 代表从服务器当前状态保存的偏移值,主服务器根据自身的偏移值和复制积压缓冲区,决定当前应该进行完整同步还是部分同步。

返回结果

PSYNC 命令 的返回值可能有返回两种结果,分别代表需要进行完整同步或是部分同步
完整同步返回 +FULLRESYNC <runID> <offset> 告知从服务器当前的runId和offset,接下来要进行完整同步
部分同步返回 +CONTINNUE 告知从服务器接下来要进行部分同步

redis哨兵模式

哨兵模式是redis的高可用解决方案,由一个或者多个哨兵组成哨兵系统,监视任意多个主服务器以及这些主服务器从属的从服务器当被监视的主服务器被判定为下线后将其下属的从服务器升级为主服务器代替已下线的主服务器继续处理命令。
9AA2F20A-1818-4C16-9522-71AB8EC0835C.png B420EAD4-4412-4D94-B738-EA0A6C145812.png 79337476-91B2-4DFC-98A1-D19F89D9FFF4.png

哨兵

redis哨兵本质是一个特殊模式下的redis实例,与普通实例共用一套代码,使用不同配置执行与普通实例不同的任务,例如哨兵不使用redis的数据库功能,也不需要持久化因此不需要载入rdb文件aof文件恢复状态,不接受普通的客户端命令如setdelhset相比于普通实例命令表的命令少得多, 3692D1AF-9003-4C43-A5F7-C2AAB03BCCFA.png

哨兵状态

在应用了哨兵的专用代码后,会初始化一个sentinelState结构,保存了哨兵功能的相关状态,其他的一般服务器状态依然保存在redisServer结构中。其中最主要的状态是维护了一个dict *masters用来记录所监视的主服务器状态。该字典中维护了主服务器的纪元(用于故障转移)、ip、端口、最后联系时间、运行id、名称、从服务器列表等信息。

两个连接

哨兵监视主服务器需要进行连接,作为主服务器的客户端,一共需要建立两个连接,一个命令连接用于向主服务器发送命令获取信息,另一个是订阅连接用于订阅主服务器的 __sentinel__:hello频道,至于为什么需要两个连接,笔者认为,redis服务器不保存发送的消息,发送时订阅的客户端不在线则会造成消息丢失,redis采用的是单线程事件循环的模式运行,循环获取具有可读可写状态的文件描述符,订阅命令是一个同步命令,需要持续等待服务端返回,直到退订,本质也是监听一个文件描述符,如果命令连接订阅连接使用同一个连接监听同一个文件描述符,则发送命令等待返回时无法获取订阅信息,等待订阅信息的时候无法发送命令。

哨兵感知

每个哨兵都是单独的redis实例,哨兵刚启动时并不知道其他哨兵的存在,也不知道主服务器的从服务器,但是知道自己监视的主服务器。与主服务器建立命令连接订阅连接之后发送命令或接受订阅可获取相关数据。

感知从服务器

哨兵每隔10秒向主服务器发送INFO命令,可以获取到主服务器自身的ip、端口、运行id等信息,以及正在复制主服务器的从服务器的ip、端口、复制偏移量等信息,根据获取的信息更新哨兵服务器内的相关数据接口更新主服务器信息和保存从服务器信息包括建立对应的数据结构和维护其与主服务器结构的关系,并且为从服务也建立订阅连接命令连接,同样每隔10秒向从服务器发送INFO命令获取更新从服务信息。

感知其他哨兵

哨兵每隔2秒会向所有监视的主服务器、从服务器发送PUBLISH命令__sentinel__:hello频道,其内容包含自身的ip、端口、运行id、配置纪元等信息以及被监视的服务器(向哪台推送就是哪台)的名称、ip、端口、纪元等信息。然后所有订阅了该频道的哨兵都能收到上述消息,包括发送者自身,发送者根据自身的运行id过滤自己发送的消息,其他服务器则通过该消息为发送者创建或更新对应的哨兵数据结构,以及更新监视的主服务器的对应的数据结构,并且在主服务器对应的数据结构内记录发送者哨兵信息。然后再与新感知到的其他哨兵服务器建立命令连接

实例下线

主观下线

在默认情况拿下哨兵会每隔1秒对所有建立命令连接的服务器(包括主服务器、从服务器、其他哨兵)发送PING命令,有效回复有三种分别是 +PONG-MASTERDOWN-LOADING,除了上述三种以外的都是无效回复,根据配置文件决定多久后依然没收到有效回复则哨兵会认为该实例主观下线,更改对应的数据结构中的Flags标志。

客观下线

所谓客观下线就是认为某节点下线的哨兵达到一定数量后,整个集群对该节点状态的认知为下线。哨兵会向其他哨兵发送SENTINEL is-master-down-by-addy 接受命令的哨兵会告知发送命令的哨兵该节点在他那边的状态以及用来进行哨兵选举的参数。

哨兵选举

当一个节点被认定为客观下线后,需要对其进行故障转移,选出他的一个从节点作为新的主节点,发送该结点为客观下线的节点会像其他结点发送SENTINEL is-master-down-by-addy带着运行id和纪元要求其他哨兵将自己设置成局部领头哨兵,接收到命令的哨兵若没有认定过其他哨兵为局部领头哨兵,即第一次收到请求,则会同意该请求,在返回值里告知他所认定的领头哨兵的运行id和纪元,超过半数哨兵认定为局部领头哨兵则成为领头哨兵,因此只会有一个领头哨兵,若一轮选举无法选出一个领头哨兵,则会在一段时间后重新进行选举,直到选出领头哨兵。

故障转移

故障转移由领头哨兵牵头,主要完成三个动作。 一,从客观下线的主服务器的从服务器列表里选出一个从服务器作为新的主服务器,选择的标准包括是否下线、最近与领头哨兵的通信、与原主服务器的通信,满足所有条件则以复制偏移量优先,若相同则取运行id小的。 二,让其他从服务器复制新的主服务器,领头哨兵向其他从服务器发送SLAVEOF命令让其他从服务器复制新的主服务器。 三,将下线的原主服务器设置为从服务器,待他再次上线后向他发送SLAVEOF命令让其复制新的主服务器。