前言

之前整理了Mysql的相关内容,这次准备整理一下Redis的相关内容,以下是本次内容的XMind:
image.png

IO线程模型

Redis在6.0之前都是Reactor单线程模型底层基于Epoll实现多路复用机制,如果客户端请求是Accept连接请求则将请求分发给Acceptor连接器处理,如果是执行命令则分发给Handler处理。大致如下图所示:
image.png

6.0版本之后Redis支持多线程,这里要注意的是读写IO采用的是多线程而执行命令还是采用的单线程。这么做的目的在于第一Redis的瓶颈在于网络IO、第二执行命令依旧采用单线程不需要考虑多线程带来的线程安全问题。
image.png

通过比较整个流程可以得出Redis将请求分配给其他IO线程待其他线程对命令读取并解析完成在丢回主线程执行命令,主线程执行命令完成后再将结果写进Buffer,待其他IO线程将执行结果从Buffer读到结果在写回Socket。整个流程上看是Redis针对网络IO的吞吐量变大了。

数据类型

除了常见的5大数据类型String(字符串)、Hash(哈希)、List(列表)、Set(无序集合)、Zset(有序集合)外,还有Geo(地理位置)、Bitmap(布隆过滤器)、Herloglog。

事务

Redis事务我们可能用比较少,原因可能有以下几点:

  1. Redis事务不支持回滚;
  2. 如果是为了保证命令执行的原子性,可以选择使用LUA脚本

Redis通过Multi开启一个事务,当你执行Command并不会马上执行而是丢到队列中,仅有通过Exec指令才能使之前丢掉队列中的Command按照顺序执行。Discard指令则是刷新队列并退出事务、Watch指令则是对一个key或者多个key进行监视,如果其他事务对监视的key进行改动,则当前事务中止,就是乐观锁那玩意。举个例子就是Watch key,执行事务发现key已被修改过再执行Exec时会返回NULL表示当前事务执行失败。

持久化

RDB

在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。

1
2
3
4
// 60 秒内有至少有 1000 个键被改动
save 60 1000

// bgsave是redis后台默认保存rdb文件的指令,它采用COW(Copy On Write)的机制,说白了就是主进程Fork子进程异步执行指令,不像Save指令会阻塞其他Redis指令。如果主进程对数据进行了修改,那么被修改的数据将会被复制一份生成一份副本,子进程将会把这份数据写进RDB文件。

AOF

AOF是将修改的每一条指令都写进文件appendonly.aof。每当Redis重启时都能靠AOF重建缓存。

1
2
3
4
5
6
7
8
9
set name zhangchen
// aof文件中存储形式
// *3
// $3
// set
// $4
// name
// $9
// zhangchen

fsync刷盘机制
  1. appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
  2. appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
  3. appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
AOF重写机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
incr age
incr age
incr age

// 会被重写成
// *3
// $3
// set
// $3
// age
// $1
// 3



// 配置参数
# auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
# auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写

俩者比较

RDB AOF
启动优先级
体积
恢复速度
数据安全性

混合持久化

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。其结构如下图所示:
image.png

经典问题

像缓存穿透、缓存雪崩、缓存击穿算是比较常见的问题了,也有了比较大众的解决方案,这里我就直接摘抄了阿里公众号《这些年背过的面试题——Redis篇》里面关于这3个问题的解答。

缓存雪崩

指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案
  • Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃;
  • 本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死;
  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生;
  • 逻辑上永不过期给每一个缓存数据增加相应的缓存标记,缓存标记失效则更新数据缓存;
  • 多级缓存,失效时通过二级更新一级,由第三方插件更新二级缓存;

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案
  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒。这样可以防止攻击用户反复用同一个id暴力攻击;
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。(宁可错杀一千不可放过一人)

缓存击穿

这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案
  • 设置热点数据永远不过期,异步线程处理。
  • 加写回操作加互斥锁,查询失败默认值快速返回。
  • 缓存预热

Redis线程阻塞

  1. BigKey
  2. Swap分区(硬盘上分出来的空间,目的是内存不足时保证系统能正常运行,但是性能会下架)
  3. Fork子进程(执行Save指令)
  4. AOF刷盘
  5. 慢查询
  6. Redis 输入缓冲区可能导致的阻塞
  7. Redis 输入缓冲区可能导致的阻塞

6、7接触的比较少,有兴趣的同学可以自行学习一下。

Redisson

Redisson是Java项目中比较常用的包了吧,这次主要聊的是其实现分布式锁的Watch Dog机制,源码就不看了(大量运用Lua脚本)网上很多关于这个源码的解析。其实这里就是一个锁续期的机制(Watch Dog),将Key的过期时间设置为30s,WatchDog每隔(30/3)10s去给锁重新设置过期时间,这样就算是代码异常Key也会正常30s到期而杜绝了死锁的可能。