Redis的数据类型


  • 字符串类型:其值可以是字符串、数字、二进制。是动态字符串。一般用于简单的k-v缓存。
  • hash类型:类似java中的hashmap,值只能存储字符串。用于存储结构化数据,比如对象。
  • list有序列表类型:是一个双向链表,存储有序的字符串。常用于存储一些列表。
  • set无序集合:类似java中的hashset,不允许集合中有重复元素。用于对数据进行快速去重,以及求交集、并集和差集。
  • set有序集合:排序的set,写入的时候通过score分数排序。



Redis高性能的原因


  • Redis是基于内存的操作。
  • Redis用c语言编写,基于基础数据结构并做了大量优化。
  • 使用单线程。避免了因为线程切换带来的资源消耗(Redis6.0以后,在数据读取和协议解析上改用了多线程,但是执行命令依然采用单线程。是因为Redis的性能瓶颈不在cpu上,而在网络io上。采用多线程是为了提高io的读写效率)。
  • Redis使用了io多路复用机制。



Redis的过期策略


定期删除

Redis定期从设置了过期时间的key中,随机抽取一些来检查,如果已经过了过期时间,则清除该key。

惰性删除

每次访问Redis中的key时,检查该key的过期时间,如果过了过期时间,则清除该key。

实际上,Redis是定期删除+惰性删除同时进行的。但是即使这样,也有很多key无法被清除。所以还需要配合Redis的内存淘汰机制。

内存淘汰机制

  • volatile-lru:在设置了过期时间的key中,淘汰最近最少使用的key。
  • volatile-random:在设置了过期时间的key中,随机淘汰某个key。
  • allkeys-lru:在所有key中,淘汰最近最少使用的key。
  • allkeys-random:在所有key中,随机淘汰某个key。



Redis的持久化策略


RDB

RDB保存的是当前Redis中所有数据的快照。它以二进制文件的形式保存。每隔一段时间,进行一次周期性持久化。它在持久化的时候,Redis会fork一个子进程去进行持久化,对Redis的读写服务影响很小。

RDB的执行周期间隔较长,如果Redis在这个过程中宕机,就会丢失这期间的数据。

AOF

AOF一般每隔1秒通过一个后台线程执行一次fsync操作。它以文本文件的形式保存,并以append-only模式写入写命令。当AOF文件过大的时候,会对其中的命令进行压缩。当Redis宕机的时候,可以在Redis重启的时候,回放AOF日志的写命令进行恢复,最多只会丢失最近1秒的数据。

Redis可以同时开启两种策略,使用AOF来保存数据不丢失,使用RDB来做快速恢复。



Redis的高可用


主从架构

即一主多从。主节点负责写,然后把数据复制到从节点,从节点负责读。

主从同步

  • 当slave启动的时候,slave发送一个psync命令给master。
  • master收到命令后,执行bgsave,生成rdb全量文件。同时把新收到的写命令缓存写到内存中。
  • 完成后,master把这个rdb文件发送给slave。slave先把文件写入磁盘,再从磁盘加载到内存。之后master再把内存中缓存的写命令发送到slave,slave再同步这些数据。之后master持续将写命令异步复制给slave。

哨兵模式

简介

主从模式当master宕机的时候,必须手动切换,因为它没有故障转移机制。而哨兵模式具备自动故障转移,集群监控,消息通知。

一般来说常见的方案是,一主两从+三哨兵节点。如果master节点的机器宕机,则剩余的两个哨兵能选举出一个节点进行故障转移。

哨兵在与master建立连接之后,会向其创建两个异步网络连接。

  • 命令连接:用于向master发送命令,并接收回复。
  • 订阅连接:用于订阅master服务的sentinel:hello频道。

哨兵模式会启动三个定时任务:

  • 每个哨兵节点每隔1s向所有的master、slave、其他哨兵发送ping命令,进行心跳检测。
  • 每个哨兵节点每隔10s向master和slave发送info命令,通过回复来分析master和salve的信息,并发现最新的集群信息。
  • 每个哨兵每隔2s向所有被监控的master和slave发送询问命令publish sentinel:hello。用于和其他哨兵进行信息交换(拓展:哨兵之间是通过Redis的发布/订阅实现的,每个哨兵往监控的master和slave的sentinel:hello这个channel中发送消息,也从中读取并消费消息,去发现其他哨兵节点)。

在第一个定时任务中,当哨兵节点向master发送一个ping命令,并且超过了配置的响应时间。则会被认为主观下线(SDOWN)。接着其他的哨兵节点也会发送ping命令,如果超过了quorum的哨兵都认为master宕机,那就是客观下线(ODOWN)。

当master节点宕机后,哨兵节点会从master的所有slave节点中,选择一个作为新的master。并让其他的slave节点从新的master赋值数据。选举slave时会考虑salve的优先级(slave priority越低,优先级就越高)、复制的偏移量(slave复制的数据越多,offset越靠后,优先级越高)。
切换时,还需要得到majority的哨兵授权。如果quorum < majority,那达到majority数量的哨兵授权即可。如果quorum >= majority,则需达到quorum数量的哨兵授权。

主备切换的问题

在master和slave节点切换的时候。可能会导致数据导致。有以下情况:

  • 异步复制导致的数据丢失。即数据还没有复制到slave节点,master就宕机了。
  • 脑裂问题。即master脱离了正常的网络,但master仍然在运行。这时哨兵可能会认为master宕机,然后升级某个slave节点为master。这样集群就会有两个master。而此时数据仍然向旧的master写入,等该master变为slave后,数据会被清除,并从新的master复制。这样就会丢失这部分写入的数据。

解决数据丢失的问题,可以对其配置

  • min-slaves-to-write 1
  • min-slaves-max-lag 10 即至少一个salve的数据复制的延迟不超过10s,一旦所有slave延时都超过10s后,master就不再接受请求。

这样,一旦slave复制数据的时间过长,master就会拒绝写请求。并且如果master出现了脑裂,该配置也能保证如果不能给指定数量的salve发送数据,也直接拒绝写请求。



缓存穿透、缓存击穿、缓存雪崩


缓存穿透

即访问了一个Redis中不存在的key,请求直接到db,数据库中也不存在。请求量大的情况下,造成db压力过大。

其解决方法有:

  • 设置一个空值到缓存中,并设置一个较短的过期时间。这样保证请求落在Redis层。
  • 使用布隆过滤器。它把存入的数据通过hash函数映射为位数组上的某个点,并把其值置为1。当有请求的时候,如果布隆过滤器上其值为0,说明该数据不存在,则直接返回。

缓存击穿

即某个热点key,在过期时间突然失效,造成大量请求直接到db,造成db压力过大。

其解决方法有:

  • 若缓存数据更新频率不高,可以当查询不到缓存的时候,加分布式锁,去库中查询到数据后并更新缓存,之后释放锁,其余线程才能请求缓存。
  • 在查询的同时去更新过期时间,或者利用定时任务去更新这些key的过期时间。

缓存雪崩

即大量热点key同时失效(比如宕机、同时过期),所有请求都到了db,造成db压力过大。

其解决方法有:

  • 设置Redis高可用方案。比如主从+哨兵。
  • 采用本地缓存,并配置限流和降级,避免服务器宕机。
  • 热点key设置不同的过期时间,避免同时过期。
  • 开启Redis持久化,宕机时能快速恢复。



缓存与数据库的一致性


数据不一致的问题,可以在更新该数据的时候,先删除缓存,然后更新数据库,更新完后再次删除缓存。

这是因为,当一个请求去更新数据库时,如果更新失败,由于缓存已经删除,则其余请求重新从数据库中读取,并更新到缓存。避免读取到的缓存是新的数据,而数据库中还是旧的数据。
而再次删除缓存,是因为如果更新数据时,有大量的读请求在读取缓存,此时会把缓存更新为旧的时候,当数据更新完成后,数据和缓存就不一致了。此时再次删除缓存,使之后的读请求需要从数据库中重新读取,同时更新缓存为新数据。

双删策略对于大部分并发没这么高的场景,基本能满足需要。但是对于某些系统中要强一致性的场景。可以使用串行队列的方式,即读取的时候,如果缓存中不存在,则该读请求放入队列中,等待前面的更新请求完成后,更新了缓存,后面的读请求才能执行。



分布式锁


分布式锁一般需要考虑:

  • 互斥。
  • 不能死锁(可重入)。
  • 是否阻塞、是否公平、性能(根据业务考虑)。

分布式锁可以用Redis来实现。

单Redis实现分布式锁

Redis使用set NX来实现。同时设置过期时间,避免死锁。

这样,当多个线程同时获取锁时,只有执行set命令成功的才返回成功,说明加锁成功。否则说明加锁失败,一直循环等待,当超过获取锁的超时时间,则获取锁失败。

但是,这样也存在单点问题、可重入、非公平锁的问题。

Redisson实现分布式锁

Redisson的原理是,当线程去获取锁时,执行lua脚本(拓展:lua脚本可以把多个命令以原子性地方式执行,因为lua在执行的时候,不会有其他脚本和命令同时执行,类似于mutli/exec的作用),如果锁不存在,则设置值和过期时间,获取锁成功。如果锁存在,且锁的是当前线程,则说明是重入锁,也获取锁成功。否则锁已存在,获取锁失败,并在超时时间内一直循环尝试获取锁。

Redisson在Redis中存储的数据为hash类型,在它的k-v键值中,保存了线程id和重入次数。当同一线程再次获取锁时,无需等待获取锁,当发现线程id一致时,即可直接获取锁,并更新重入次数。

如果线程执行任务的时间超过了加锁的时间,可以开启Redisson的watchdog配置,它能每隔10s检查一次,如果锁还存在,就重新设置过期时间。但是在设置了加锁时间后,该配置会失效。

释放锁时,也通过lua脚本判断。如果锁已经不存在,则发布锁释放消息,释放锁成功。如果释放锁的线程id和锁的线程id不一致,则解锁失败,抛出异常。如果重入数大于0,则减1并刷新过期时间,当重入数小于等于0时,发布释放锁的消息,释放锁成功。

Redisson分布式锁存在的问题是,在主从架构中,当线程获取到锁时,还没有复制到slave节点,这时如果宕机,哨兵节点选举slave成为新的master,则其他的线程仍成获取到锁,这会导致脏数据的产生。

Redlock算法

待整理。

拓展:Zookeeper实现分布式锁

Zookeeper分布式锁的原理是,当一个线程要在zk上获取锁时,它会在zk上创建一个临时顺序节点(可以想象为一个id为1的节点)。如果之后再有线程来获取锁,则会在上一次临时顺序节点之后,再创建一个顺序节点(并且id序号+1,即id为2)。这时我们判断当前线程创建的节点,是不是临时顺序节点中的第一个节点,如果是,则获取到锁。如果不是,比如当前线程创建的节点是id为2的节点,则会在该节点的上一个节点上加一个监听器。当锁被释放时,上一个节点被删除,则线程收到通知,便可获取到锁。

用这种方式,当客户端获取到锁但突然宕机后,那么这个临时节点就会自动删除,其实线程便可获取锁。并且,当线程获取锁时,只要临时顺序节点的第一个节点是该线程创建的,则可直接获取到锁,实现了可重入。另外,因为临时顺序节点是有序的,所以实现了公平锁。且zk是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务,解决了单点问题。

Zookeeper分布式锁的问题是,性能上没有Redis分布式锁的性能高,因为每次需要创建和删除节点。