Redis系统学习

Redis 使用场景及常见面试题

缓存穿透及应对措施

缓存穿透: 查询一个缓存中不存在的数据,并且数据库中也查询不到,从而无法写入缓存,导致每次查询都直接请求到数据库,给数据库造成巨大压力,这种情况大概率遭到了攻击。

解决方案一: 对不存在的key缓存一个空数据,缺点是会导致内存不断增大。

解决方案二: 使用布隆过滤器,在查询缓存之前先去布隆过滤器中查询一遍。

关于布隆过滤器,查看这篇文章,讲解的很细。 布隆(Bloom Filter)过滤器——全面讲解,建议收藏

缓存击穿及应对措施

缓存击穿: 对于一个设置了过期时间的热点key,恰好在某个时间点过期,且大量并发请求同时访问这个key,于是所有请求直接访问数据库,可能会瞬间把DB压垮。

解决方案1: 互斥锁。当缓存失效时,不立即访问db,先使用redis的setnx设置一个互斥锁,当操作成功返回时再进行db操作并写入缓存,否则重试get缓存的方法。

解决方案2: 设置当前key逻辑过期。 大致思路:

  1. 在设置key的时候,设置一个过期时间字段一起存入缓存中,不给当前key设置过期时间。
  2. 当查询的时候,从redis中取出数据后判断添加的过期时间字段是否过期。
  3. 如果过期,则启动新的线程进行数据同步,当前线程正常返回数据,但这个数据可能不是最新的。

两种方案各有利弊。如果选择数据的强一致性,建议使用互斥锁的方案,性能上可能不高,因为锁需要等待,也有可能产生死锁的问题。

如果优先考虑高可用性,性能比较高,建议选择给key添加逻辑过期时间,但是数据同步做不到强一致性。

缓存雪崩及应对措施

缓存雪崩: 在同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  1. 给不同的key设置随机过期时间
  2. 利用redis集群提高服务的可用性(哨兵和集群模式)
  3. 给缓存业务添加降级限流策略(降级可作为系统的保底策略,适用于穿透、击穿和雪崩)
  4. 给业务添加多级缓存 (Guava和Caffeine)

如何对数据库的数据和缓存的数据进行同步?(读写一致性)

主要分为延时一致性和强一致性的同步。

延时一致性的业务场景: 比如发表文章、发布商品的场景,不需要实时性很高,像这些场景的业务就可以采用延时一致的解决方案。

强一致性的业务场景: 比如抢券、秒杀的场景,实时性要求非常高,需要知道是否有券或者商品剩余,这些实时性非常高的场景可以使用强一致性的同步方案。

允许延时一致的业务采用的异步通知

  1. 使用MQ中间件,更新数据后,再更新缓存
  2. 使用Canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存

强一致性的业务使用Redisson的读写锁进行同步

  1. 共享锁: 获取读锁ReadLock,加锁之后,其他线程可以共享读操作
  2. 排他锁: 也叫独占锁WriteLock,加锁之后,阻塞其他线程读写操作。 通过使用排他锁,就可以保证在写数据的时候不会让其他线程读取数据,避免了脏数据。需要注意的是,获取读写锁的读方法和写方法需要使用同一把锁。

延时双删
如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定(数据库主节点同步到从节点的时间不确定多久),在延时的过程中可能出现脏数据(如果延时的时间小于数据同步的时间,那么读取的数据可能是旧数据),并不能保证强一致性。

Redis的持久化

Redis中有两种持久化方式:RDB和AOF。
RDB(Redis Database Backup file)是一个快照文件,它是把Redis内存中存储的数据保存到磁盘上,方便从RDB的快照文件中恢复数据。

1
2
3
4
5
6
7
8
redis-cli
save # 由redis主进程执行rdb,会阻塞所有命令
bgsave # 开启子进程执行rdb,避免主进程受影响

# x秒内,如果至少有y个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000

AOF(Append Only File)是追加文件,当redis操作写命令的时候,都会存储在这个文件中。当需要恢复数据时,重新执行该文件的命令即可恢复数据。

修改redis.config 文件来修改配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# AOF 默认是关闭的,默认是no,开启需要设置为yes
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

# 每执行一次写命令,立即记录到aof文件
appendfsync always

# 写命令执行完先放入aof缓冲区,每隔一秒将缓冲区的数据写到aof文件,是默认方案
appendfsync everysec
# 写命令执行完先放入aof缓冲区,由操作系统决定何时将缓冲区内容写入磁盘
appendfsync no
配置项刷盘时机优点缺点
always同步刷盘可靠性高,几乎不丢数据性能影响大
everysec每秒刷盘性能适中最多丢失1秒数据
no操作系统控制性能最好可靠性较差,可能丢失大量数据

bgrewriteaof命令对aof文件执行重写,用最少的命令达到相同效果

1
2
3
4
5
# AOF 文件比上次文件增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100

# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
RDBAOF
持久化方式定时对整个内存做快照记录每次执行的命令
数据完整性不完整,两次备份之间会丢失相对完整,取决于刷盘策略
文件大小会有压缩,文件体积小记录命令,文件体积很大
宕机恢复速度很快
数据恢复优先级低,因为数据完整性不如AOF高,因为数据完整性更高
系统资源占用高,大量CPU和内存消耗低,主要是IO磁盘资源,但AOF重写时会占用大量CPU和内存
使用场景可以容忍数分钟的数据丢失,追求更快的启动速度对数据安全性要求较高

这两种存储方式,哪种恢复的更快呢
RDB是二进制文件,在保存的时候体积也是比较小的,它恢复的较快,但是它有可能会丢失数据,通常也会使用AOF恢复数据,虽然AOF的恢复速度慢,但是它丢失的数据风险较小,在AOF文件中可以设置刷盘策略,使用较多的是每秒批量写入一次命令。

Redis的数据过期策略

惰性删除: key的过期时间到期后,不会自动删除,而是当再次查询时,先检查key是否过期,如果过期则删除,否则直接返回该key

定期清理: 每隔一段时间,对一些key进行检查,删除过期的key。定期清理有两种模式:

  1. slow模式是定时任务,执行频率默认为10hz,每次不超过25ms,通过修改redis.conf的hz选项调整次数。
  2. fast模式执行频率不固定,每次事件循环会尝试执行,但每次间隔不低于2ms,每次耗时不超过1ms。 Redis的过期删除策略,是惰性删除和定期删除两种策略配合使用。

Redis的数据淘汰策略

数据的淘汰策略: 当Redis的内存不够用时,再向redis中添加新的key时,那么redis就会按照某种规则将内存中的数据删除掉,这种规则称为内存的淘汰策略。

  1. noeviction: 不淘汰任何key,但是内存满时不允许写入任何数据,直接报错,这是默认策略。
  2. volatile-ttl: 对设置了ttl(Time to live 存活时间)的key,比较key的剩余ttl值,ttl越小越先被淘汰。
  3. allkeys-random: 全体key随机进行淘汰
  4. volatile-random: 对设置了ttl的key,随机进行淘汰
  5. allkeys-lru: 对全体key,按照lru算法淘汰
  6. volatile-lru: 对设置了ttl的key按照lru算法淘汰
  7. allkeys-lfu: 对所有key按照lfu算法淘汰
  8. volatile-lfu: 对设置了ttl的key,按照lfu算法淘汰
1
2
3
4
5
# redis.conf中设置
maxmemory-policy allkeys-lru

# redis实例动态设置
CONFIG SET maxmemory-policy allkeys-lru

LRU(Least Recently Used)算法 :最近最少使用。用当前时间减去最后一次访问时间,这个值越大淘汰优先级越高。
LFU(Least Frequently Used)算法 : 最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

使用建议:

  1. 优先使用allkeys-lru淘汰策略。充分利用LRU算法的优势,把最常访问的数据保留在缓存中,如果业务有明显的冷热数据区分,建议使用。
  2. 如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用随机淘汰策略allkeys-random。
  3. 如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不要设置过期时间,那么这些数据就会一直不被删除,会淘汰其他设置过期时间的数据。
  4. 如果业务中有短时高频访问的数据,建议使用allkeys-lfu或volatile-lfu策略。

问:数据库中有1000w数据,Redis只能缓存20w数据,如何保证redis中的数据都是热点数据?
答:使用allkeys-lru(最近最少访问的数据优先淘汰)淘汰策略,留下来的都是经常访问的热点数据。

问:Redis的内存使用完了会发生什么?
答:主要看设置的数据淘汰策略是什么,如果是默认的noevction,内存满后继续添加key会报错。其他的策略都会淘汰某些key后,继续写入。

分布式锁

分布式锁一般是多个进程同步数据时加的锁,而平时代码中的写的lock,是同一个进程下多个线程同步时加的锁。

通常情况下,使用分布式锁的场景有:集群情况下的定时任务、抢券、幂等性场景

Redis分布式锁主要利用Redis的setnx命令,setnx是SET if not exist的简写。

1
2
3
4
5
# 获取锁,NX是互斥EX是设置超时,EX必须设置,否则可能因为业务超时或服务宕机等原因而无法释放锁。添加超时时间后,到期会自动释放锁
SET lock value NX EX 10

# 释放锁,删除即可
DEL key

问:Redis分布式锁如何合理地控制锁的有效时长?
答:1.根据业务执行时间预估(不靠谱);2.给锁续期

Redisson分布式锁的看门狗(Watch Dog)机制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public void redisLock() {
    // 获取锁(可重入锁)
    RLock lock=redissonClient.getLock("lockKey");

    // 尝试获取锁,参数含义分别是获取锁的最大等待时间(期间会重试),锁自动释放时间(默认30),时间单位
    // boolean isLock=lock.tryLock(10,30,TimeUnit.SECONDS);

    // 加锁、设置过期时间等操作都是通过lua脚本完成
    boolean isLock=lock.tryLock(10,TimeUnit.SECONDS);
    if (isLock) {
       try{
         System.out.println("获取锁成功");
       }finally{
        // 释放锁
        lock.unlock();
       }
    }
}

lua学习教程 https://www.runoob.com/lua/lua-tutorial.html

redisson的分布式锁是可重入的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void add1(){
    RLock lock=redissonClient.getLock("locker");
    boolean isLcok=lock.tryLock();
    // 执行业务
    add2();

    // 释放锁
    lock.unlock();
}

public void add2(){
    RLock lock=redissonClient.getLock("locker");
    // 如果是同一个线程,则可以获取锁,否则互斥
    boolean isLcok=lock.tryLock();
    // 执行业务

    // 释放锁
    lock.unlock();
}

通过哈希结构区分锁的不同线程访问记录。field保存线程唯一标识,value保存重入次数。

redisson分布式锁的主从一致性

在哨兵模式等主从同步模式中,如果主节点Master没有成功同步数据到从节点Slave时,有一个请求线程获取了一个锁,与此同时,Master主节点宕机了,然后会从一个从节点Salve中选举一个主节点,又一个请求线程获取了同一个锁。这种场景就会导致多个线程获取同一把锁,失去了锁的意义,可能或导致脏数据。

RedLock(红锁): 不能只在一个redis实例上创建锁,而应该在多个(n/2+1)实例上创建锁,(n/2+1)表示至少一半的实例。

官方不建议使用红锁解决主从不一致问题。因为实现复杂,且高并发下性能差,运维繁琐。

那如何解决这一问题呢? Redis是AP思想(高可用),应该使用CP思想的Zookeeper。

分布式锁FAQ

问:Redis分布式锁如何实现?
答:在redis中提供了一个命令setnx(set if not exists),由于redis是单线程的,用来命令之后,只能有一个客户端对某个key设置值,在没有过期或者删除key时,其他客户端是不能设置这个key的。

问:如何控制redis分布式锁的有效时长呢?
答:redis的setnx指令不好控制这个问题,可以采用redisson框架实现。在redisson中可以手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的业务还没执行完毕时,redisson中引入了看门狗机制,就是说每隔一段时间就检查当前业务是否持有锁,如果持有锁就增加锁的持有时间,当业务执行完成后释放锁就可以了。还有一个好处是,在高并发场景下,如果客户1获取了锁,客户2来了后并不会马上拒绝,它会不断尝试获取锁,如果客户1释放锁之后,客户2会马上持有锁,性能也得到了提升。

问:redisson的分布式锁是可以重入的吗?
答:是可重入的。这样做是为了避免死锁的发生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是就会计数器加1,如果释放锁就会减1。在存储数据的时候采用的是hash结构,大key可以按照业务进行定制,小key是线程的唯一标识,value是当前线程的重入次数。

问: redisson的分布式锁能解决主从一致性的问题吗?
答: 不能。比如,当线程1加锁成功后,master节点数据会异步复制到从节点slave,此时当前持有redis锁的master节点宕机,slave节点被提升为新的master节点,之前的master节点变成slave节点,假如现在又来了一个线程2,两个线程持有同一把锁,执行业务可能导致脏数据问题。其实Redis采用的是高并发思想(AP),可以考虑使用强一致性思想的Zookeeper(CP)。

Redis集群方案

主从复制

单节点的Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。一般是一主多从,主节点负责写,从节点负责读。主节点将数据同步到从节点。
全量同步原理:

  1. 从节点请求主节点同步数据(replicationId,offset)
  2. 主节点判断是否是第一次请求,是第一次请求就与从节点同步版本信息(replicationId和offset)
  3. 主节点执行bgsave,生成rdb文件后,发送给从节点执行
  4. 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件repl_baklog)
  5. 把生成的命令日志文件发送到从节点执行,从而完成全量数据同步

增量同步原理:

  1. 从节点从主节点请求同步数据,主节点判断不是第一次请求,不是第一次请求就获取从节点的offset值
  2. 主节点从命令日志文件中获取offset值之后的数据,发送到从节点进行数据同步

哨兵模式

Redis提供了哨兵Sentinel机制来实现主从集群的自动故障恢复。
哨兵的作用:

  1. 监控:Sentinel会不断检查Master和Slave是否按预期工作;
  2. 自动故障恢复:如果master故障,Sentinel会自动将一个slave提升为master,当故障实例恢复后以新的master为主;
  3. 通知:Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis客户端。

这样,通过哨兵模式就可以实现redis的高并发高可用。

服务状态监控 Sentinel基于心跳机制监测服务状态,每隔1s向集群的实例发送ping命令:

  • 主观下线: 如果某个Sentinel节点发现某个实例在规定时间内没有响应,则认为该实例主观下线;
  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过sentinel实例数量的一半

哨兵选主规则:

  1. 首先判断主节点与从节点的断开时间长短,如果超过指定值就排除该从节点;
  2. 然后判断从节点的slave-priority的值,越小优先级越高;
  3. 如果slave-priority的值一样,则判断slave节点的offset值,越大说明从主节点同步的数据越多,优先级也就越高;
  4. 最后时判断slave节点的运行id大小,越小优先级越高

Redis集群(哨兵模式)脑裂问题 集群脑裂redis的主节点、从节点和哨兵集群Sentinel处于不同的网络分区,使得sentinel没有能够检测到主节点的心跳,所以就通过选举的方式提升一个从节点作为主节点,这样就存在了两个主节点master,就像大脑裂开了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降级为从节点,这时再从新的master同步数据,就会导致数据丢失。

解决办法: Redis中有两个配置参数:

  1. min-replicas-to-write 1 表示最少的slave节点为1个
  2. min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒

达不到这两个要求的就拒绝请求,可以避免大量数据丢失。

一般规模的应用使用一主一从+哨兵就可以了,单节点不超过10GB内存,如果Redis内存不足则可以给不同的服务分配独立的Redis主从节点。

分片集群

Licensed under CC BY-NC-SA 4.0
页面浏览量Loading
如果觉得我的博客能帮助到你,欢迎点击右侧的赞助进行投喂。如有技术咨询,也可以加本人好友。