vlambda博客
学习文章列表

Redis常见知识点总结

常见数据类型

1.String:二进制安全(就是可以表示任何数据),最大上限512m,也叫sds,本质是个可以动态扩容的byte数组,每次扩容大小为2n+1

2.Hash:kv集合,String的kv映射,适合存储对象

3.Set:无序集合,底层实现是value为null的HashMap,通过计算Hash值快速重排和快速判断成员是否在集合内,适用于需要取交集的场景,比如共同好友之类的

4.sorted Set:有序集合,底层使用hashmap实现,每个元素对应一个score,依靠score排序,排序后的元素放在跳跃表里,即通过hashmap决定存储结构,通过跳跃表保持有序性,适用于用户评分榜(id:socre)

5.List:字符串双向链表,缓冲区的实现基础

6.zipList:当数据较少时可以用zipList代替List和Hash,zipList本质是个数组,结构如下

|zlbyte|zltail|zllen|数据本身|zlend|

  • zlbyte:数据大小 4字节
  • zltail:末尾指针(偏移量) 4字节
  • zllen:数组长度 2字节
  • zlend:结束符 1字节

跳跃表

多层链表,查询的时候可以一次性跳跃多个节点,实现O(logn)的查询速度,本质是用空间换时间

插入节点时使用一个随机数作为插入层数,例如插入7时随机层数为4,则在1到4层都可以找到7这个节点,但是在其他层就看不到了




图片转自知乎,如侵删请后台留言

为什么不用红黑树/avl:

  • 1.并发环境下调整节点的操作容易产生线程安全问题(所谓的支持无锁操作)
  • 2.跳跃表实现更简单
  • 3.不需要旋转节点,插入极快

pub/sub

消息发布和消息订阅,在redis中,你可以对某一个key进行消息发布和订阅,当该key进行发布后,所有订阅它的客户端都会收到对应的消息。

持久化(RDB)

RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。

1.自动触发:通过配置save命令到redis.conf配置文件中,根据save命令后接的参数决定自动触发的时间和频率

2.手动触发

  • 1.save命令:阻塞式持久化,该命令会调用主进程把数据库状态写入RDB命令文件(dump.rdb)中,文件创建完毕之前redis不能处理其他的任何请求和事务
  • 2.bgsave命令:非阻塞持久化,该命令会创建一个子进程(父进程的复制品)专门处理数据库状态的写入,全程只有fork父进程的时候才会有瞬间的阻塞

缺点:需要一定时间内做一次备份,如果redis意外down掉的话,就会丢失最后一次快照后的所有修改(数据有丢失)。对于数据完整性要求很严格的需求

持久化(AOF)

通过将数据库的操作记录写入AOF文件来完成持久化

1.append only file:该命令将操作写入appendonly.aof文件内,os在缓冲区满之后会一次性刷入磁盘(也就是说对于redis来说是写入磁盘,但是os实际上不会每次都写入磁盘),使用主进程fork一个子进程来并发处理,将重写对服务器的影响降低到了最小。

  • no:每30秒同步一次
  • everysex:每秒同步一次
  • always:每接受到一条新命令就同步一次
  • aof缓冲区:子进程创建后如果主进程处理了新的命令,则会将该操作写入aof缓冲区,子进程会将缓冲区的内容也写入aof文件,保证重写的实时性

redis的事件处理模型

1.6.0之前的redis使用io多路复用处理事件,该模型被称为事件处理器,包含了多个socket管理客户端链接,io多路复用程序,文件事件分派器,事件处理器四个模块

2.执行流程:

  • 文件处理器基于io多路复用模型监听多个socket
  • 当socket的读/写/应答/关闭等操作任意之一就绪时,多路复用程序将socket通过事件分派处理器分派给与socket相关连的事件处理器进行处理

3.非阻塞执行流程(redis6.0后支持非阻塞,并发只存在于网络事件io,其他部分还是单线程,不存在线程安全问题):

  • 主线程建立链接请求,获取socket放入全局等待读队列
  • 主线程处理完读事件之后通过RR策略将等待队列平均分给io线程们(socket和线程绑定)
  • 主线程阻塞,等待io线程读取socket完毕
  • io线程组并行对socket解析(只解析不执行),主线程阻塞,解析全部完成后主线程执行所有请求命令,将数据写入缓冲区
  • 主线程阻塞等待socket回写完毕后解除等待队列和线程的绑定,清空等待队列

4.为什么不用多线程

  • redis的性能瓶颈不在cpu而在io和内存
  • 单线程容易编程和维护
  • 死锁和上下文切换会影响性能

5.为什么后来加入多线程

  • 加入的多线程只负责网络io,解决io瓶颈问题

事务指令

1.multi:标记事务块的开始,总是返回ok,redis会将后续命令加入到执行队列中再执行exec

2.watch:用于监听一个或多个key,如果事务exec之前该key被修改则触发事务打断,exec执行结果返回null

3.exec:执行所有被放入执行队列中的事务,执行结束后恢复正常链接状态,使用cas同步,执行成功则返回该队列中所有原子化事务的执行结果组成的数组

4.discard:取消事务块内所有命令的执行,总是返回ok

redis的事务不支持rollback,不满足原子性和持久性

key的过期删除策略

redis使用key+该key的过期时间组成一个过期字典,通过查询该字典判断key是否过期,使用expire设置

1.定期删除:定期检查一部分key是否过期,需要人工设置检查频率,设置的好则能平衡cpu和内存的消耗,设计的不好会导致获取到一个已经过期的key

2.定时删除:创建多个定时器对每个key进行检查,一旦发现过期立刻删除,频繁删除不cpu友好,对内存友好

3.惰性删除:读取key时才检查是否过期,删除频率低对cpu友好,对内存不友好

4.redis默认使用定期删除+惰性删除,平衡cpu和内存,同时惰性删除可以避免获取到一个过期的key

内存淘汰策略

1.volatile-lru:利用LRU算法移除最近使用较少的(only expire)

2.allkeys-lru:利用LRU算法移除最近使用较少的

3.volatile-random:瞎jb删(only expire)

4.allkeys-random:瞎jb删

5.volatile-ttl:优先移除快过期的(only expire)

6.noeviction:默认策略,oom后直接抛错误,别的什么也不做

缓存穿透

指用户不断查询缓存和数据库中不存在的数据,导致每次请求都会到达数据库,从而压跨数据库

解决方法:

  • 业务层校验:对明显有问题的查询直接拦截(比如自增主键小于0)
  • 将空结果短期缓存,下次查询时直接返回缓存
  • 布隆过滤器

缓存击穿

当热点key失效时依然有大量查询请求到达数据库,导致数据库被压跨(比如淘宝双11活动0点结束,在0点0分1秒时key失效,但依然会有大量请求)

解决方案:

  • 设置热点key永不过期
  • 对热点key根据业务场景定期更新
  • 互斥锁:当热点key第一次查询失效时先上锁,使其他请求sleep一段时间,该期间从数据库更新key到缓存然后再释放锁

缓存雪崩

缓存大面积失效,大量查询会直达数据库导致数据库被压跨

解决方案:

  • 设置均匀分布的数据有效期避免数据集体失效(失效时间后加入随机值也可以)
  • 提前预热:对将要到来的大量请求提前走一遍系统,提前缓存
  • 保证redis服务器高可用

redis保证和其他db的一致性

1.读操作优先读redis,失效则访问其他数据库,并把读到的数据回写给redis

2.写操作时先写其他数据库,再写redis(mysql的话可以CRUD服务触发器,CRUD触发后直接写入redis,或者让redis解析binlog,根据binlog执行操作)

3.设定超时时间,超时则删除redis中对应的数据,这样最差的情况下在超时时间外,数据一致性也能得到保证

分布锁

概念:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问,保证只有一台服务器取到锁

分布锁的四个要求

  • 1.互斥性:懒得解释了
  • 2.安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
  • 3.死锁避免:对客户端释放锁的时间应做限制
  • 4.容错:过半节点挂掉时另一半节点仍能正常进行服务

使用set命令来获取锁, Lua脚本来释放锁:

  • 使用map结构存储锁,key为自己设置的锁value为requestid(id使用UUID随机生成,代表请求的id,这样就知道这个锁是哪个请求加的了,才能满足安全性!!!),并对锁设置一个额外的time作为过期时间
  • 解锁时使用lua脚本判断requestid就可以了

redis分布式锁存在的问题:如果结点挂掉,则只能通过过期时间被动等待锁释放,若结点是master,还会导致命令丢失或者锁丢失