关于redis的描述、数据结构、持久化、事务学习笔记
前言
本文围绕面试问题、redis学习记录。
本文是个人的笔记,会有遗漏或含糊的地方。
描述下redis
redis是一款非关系型数据库,它是以key-value的形式存在数据,因为它的数据在内存中所以它的读写速度极高。
当然它支持持久化,将数据以二进制形式或者以命令的形式持久化到磁盘。
然后它是单线程的具有原子性,支持lua脚本,包含多种数据类型。
我们除了用redis进行数据的缓存,还能利用它实现分布式锁、订阅、队列
redis数据类型
类型名 | 结构 | 描述 |
---|---|---|
String | int或sds(简单动态字符串) | 字符串 |
List | 双向链表+压缩列表 (3.2版本后由quicklist实现) |
字符串列表 |
Hash | 压缩列表(7.0后换listpack)或哈希表 | 键值对集合 |
Set | 哈希表或整数集合 | 无序并唯一集合 |
ZSet | 压缩列表(7.0后换listpack)或跳表 | 有序并唯一集合 |
BitMap | string,可看作bit数组 | 位图 |
redis持久化
持久化分为两种:AOF(保存写操作命令)、RDB(保存二进制数据)。第一个简单来说就是在写的时候对写的命令进行持久化,第二个就是在指定的间隔时间后对内存中的数据进行保存(但是二进制数据,并不是存操作命令)。
AOF持久化
在客户端发送一个写的操作到redis当中,redis会先把数据写到内存,写完后再对此次的写操作进行存储(保证了写操作是正常的并不是无效的)。
redis启动手会指向AOF的文件里的命令来达到恢复数据的效果。
存储的格式是什么样的呢?
例如我写入的数据命令是:set name liuscraft
那么先写到缓存,发现正常写入,就记录此次写操作的日志,内容如下:
*3 # 三个值
$3 # 第一个值的长度
set # 第一个值的内容
$4 # 第二个值的长度
name # 第二个值的内容
$9 # 第三个值的长度
liuscraft # 第三个值的内容
AOF三种策略
fasync(int fd):该函数功能是确保文件fd所有已修改的内容已经正确同步到硬盘上,该调用会阻塞等待直到设备报告IO完成。
- always(同步写回):就是在每次进行写操作后日志写入AOF缓冲区后都会立马进行 fasync()
该策略,性能差,但是它可靠性高,最大程度保证了数据不丢失。 - everysec(每秒写回): 在每次写操作后日志写入AOF缓冲区但不立马进行fasync,而是每个一秒进行fasync()
该策略,性能适中,但在宕机后,redis的1秒内写操作的数据会丢失。 - no(由操作系统控制):在每次写操作后日志写入AOF缓冲区,但是不进行fasync的调用,而且由系统内核自行调用。
该策略,性能好,但在宕机后,会丢失很多的数据,因为fasync操作是由系统控制。
AOF重写(对AOF持久化文件进行压缩)
因为AOF持久化存储的是写命令,然后我们如何进行该文件进行压缩?
其实就是将AOF文件里只保存当前key的最新的一次命令即可。
例如:
set name liuscraft
这是一个上一次执行的,它存在aof中。
set name xiaoming
这是一个最后一次执行的(当前内存中的数据),但也在aof中。
我们会发现其实aof中貌似对这个name的数据重复了!那我们对他们进行去重嘛。
这时我们就可以用到:AOF重写机制。
AOF重写机制就是:当AOF的文件大小达到我限定的阈值后就会进行AOF重写,但是该重写是在后台进行的,不会影响主进程。
AOF后台重写,其实就是创建了一个子进程,然后进行一个AOF重写,因为AOF重写它相当于是新建一个AOF文件,然后把内存中的数据生成命令然后写入到新的AOF文件中,那么这就是一个耗时操作,所以需要在后台进行这个重写操作避免了主进程的阻塞。
在AOF后台进行重写时,其实主进程还是能够正常写命令的(客户端存取数据的操作),因为在AOF进行后台重写时它是在子进程完成的,创建的子进程会把父进程的页表数据复制一份到子进程,但是父进程和子进程的页表都是指向的一个物理内存,但是主进程进行写操作时系统会对物理内存进行一个复制,从而保证了主进程不会影响子进程,子进程不会影响父进程。
但是需要注意的是:如果在进行AOF重写的时候主进程修改了key-value的修改,它的数据会存储在AOF重写缓冲区里,当AOF重写的子进程完成后会把缓冲区里的内容也写到新AOF文件当中,这样就保证了在AOF重写时新的数据写入导致了主进程于子进程的数据不一致问题。
但是需要注意的是,用fork函数(C语言的函数)创建子进程时,它会把父进程的页表复制一份到子进程,这个操作是阻塞的,然后如果在父进程有新的数据写入时会复制一份新的物理内存,这也是一个阻塞操作(如果遇到的是BigKey就会造成长时间的阻塞)。
重写后的AOF文件其实就是把旧命令都去掉了,然后只保留最新的一次。
AOF有个问题就是在启动后恢复数据的速度慢。
RDB持久化(redis默认持久化策略)
RDB(快照持久化)简单来说就是对内存数据直接存储到本地,不存储额外的内容(指令那种)。
RDB触发就是设置一个时间间隔,进行一次内存数据的持久化,这个操作它也是会阻塞的所以也是跟上面AOF重写差不多用到了子进程,区别就是它存储的是直接存储内存数据不用写成命令,然后它不一样的是没有跟AOF一样在子进程进行持久化时,主进程有新的写入操作时会进行缓存等待子进程持久化完后一并写入,它是写入的数据只在内存存储,只有等待下次持久化时才能持久化到磁盘。
RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
RDB触发方式
- RDB手动触发:
执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞 - RDB自带触发,修改配置文件内容(redis.conf文件):
下面是默认配置:可以根据需求更改该配置。
它配置了 redis 服务器在什么情况下自动触发 bgsave 异步 RDB 备份文件生成。save 900 1 # 900秒内执行一次set操作 则持久化1次 save 300 10 # 300秒内执行10次set操作,则持久化1次 save 60 10000 # 60秒内执行10000次set操作,则持久化1次
RDB虽然它是一次性对内存进行一次存储(快照),但是如果频率高会对redis性能造成影响,但如果频率低的情况如果服务器宕机则会导致redis丢失大量数据,它不像AOF那种秒级的存储丢失数据量少。
AOF于RDB混合
为了解决它们之间的问题,我们可以混合使用这两种持久化方式。
这样既可以解决启动redis数据恢复的快的问题,又能解决数据大量丢失的问题。
在进行RDB时,如果发生了写操作,它会放在AOF缓冲区,等RDB持久化完成后会把AOF缓冲区的内容追加到RDB数据后面,然后在redis启动时能够保证不会大量数据丢失。
在redis启动后它恢复数据会先恢复RDB数据在恢复AOF的数据,整个持久化数据分为前半部分(RDB)和后半部分(AOF增量的数据)。
缓存穿透、缓存击穿、缓存雪崩
缓存穿透
就是绕过了缓存、直接走数据库,这种情况一般是查询的未知数,都是些恶意请求。
解决方方法:
- redis缓存null,然后直接返回null,但是无法避免恶意请求的key带来更多的key缓存null,提升了对redis的维护成本,而且它与布隆过滤器比起来响应速度不快。
- 加入布隆过滤器,这个它是一种通过存储元素的哈希值和位数数组中的位置,可通过该比过滤器快速匹配可能存在于集合中,它有失精度(虽然可以调节),它跟redis一样,并不能完全防住,但是在速度和内存占用上肯定是少了很多。
但通过上面两个方式我们可以选用布隆过滤器外加配合限流、黑名单这种安全措施来防止恶意用户通过随机key来进行恶意请求的操作。
缓存击穿
指在缓存过期了在重写被缓存之前,有大量请求进来都无法从缓存得到数据则全部转向数据库,从而导致数据库压力剧增、请求大量阻塞最终导致直接挂掉。
解决方法:
- 在特定的时间段不让它过期,过期时间设置长点。如果是预知的情况下可在大量请求来之前的时间点先把数据缓存好然后设置过期时间要到大流量时间段过去,要么就不设置过期。
- 还可以利用分布式锁解决该问题,当请求进来时发现缓存没有数据那么就加入分布式锁 ,然后开始从数据库中获取数据并且更新缓存中最后释放锁(考虑到可能加锁后服务出现了问题导致死锁,可给分布式锁设置过期时间),然后其它请求也没找到缓存时如果发现无法获得分布式锁可直接返回服务繁忙(服务降级)
缓存雪崩
指在同一时间点大量缓存过期或被删,导致所有请求全部打进了数据库,从而导致数据库压力剧增、请求大量阻塞最终导致挂掉。
解决方法:
- 缓存过期时间设置的均匀分散,不要集中在一个点,我们可以在过期时间后面加上有个小范围的随机数从而达到时间不集中在一个点。
- 当然我们还可以利用其它策略比如:分布式锁、不设置过期时间等。
redis事务
redis事务它保证了隔离性。redis的事务它是一次性执行一组命令,它没有回滚操作。
如何使用事务
开启事务: multi
该命令执行后会返回OK,只有在命令的参数不对或者之后的命令错误不存在则会中止当前事务并清除命令队列,但是在执行过程中执行命令不会做任何操作(注意事务不允许有子事务)
执行事务: exec
该命令会将该事务里的命令队列,一次执行。(也就是说开启事务后执行的命令是在执行该命令后才会执行)
中止事务,清空命令队列: DISCARD
命令用于取消事务,放弃执行事务块内的所有命令。(总是返回OK)
监听key: watch
如果监听的key在事务之外被修改,则不回执行该事务命令队列(若执行exec 则返回nil)
取消监听Key: unwatch
取消监听key,与watch
搭配
在执行事务里的命令过程中,如果其中一条命令失败,不会影响其它命令的继续执行,也就是说它不保证原子性(不回进行回滚)
multi+exec命令示例
> MULTI
OK
(TX)> INCR foo
QUEUED
(TX)> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
discard命令示例
> MULTI
OK
(TX)> PING
QUEUED
(TX)> SET name "liuscraft"
QUEUED
> DISCARD
OK
watch命令示例
# 监听 "name"
> WATCH name
OK
# 在开启事务前修改 name
> SET name "liuscraft"
OK
# 事务
> MULTI
OK
(TX)> SET name "xiaoming"
QUEUED
# 结果为 nil,未执行任何命令
(TX)> EXEC
(nil)
# 验证是否未执行 SET name "xiaoming"
> GET name
"liuscraft"