Redis

232 阅读9分钟

1.基本数据类型

  • 共6种,分别是字符串、链表linkedlist、字典hashtable、跳表skiplist、整数集合intset和压缩列表ziplist
  • 基于以上的6种基本数据类型,封装了redis对象系统:字符串string、列表list、哈希表hash、集合set和有序集合zset
  • 字符串:redis没有使用c语言的字符串,自己实现了简单动态字符串SDS(simple dynamic string),特点是
    • 变量len记录字符串长度,获取字符串长度时间复杂度为O(1)
    • 二进制安全,可以保存任何格式的数据
  • 链表linkedlist:双向链表结构,每个节点都有前置节点指针和后置节点指针
  • 哈希表hashtable:应该叫map,映射或者字典,使用hash表来实现。每个映射包含两个hash表,在rehash的时候使用。使用链地址法来解决哈希冲突,同一位置的节点构成一个链表。使用渐进式rehash,保证服务性能
  • 跳表skiplist:跳表由zskiplist和zskiplistnode组成,zskiplist保存跳表信息,zskiplistnode表示表跳跃节点
  • 整数数组intset:保存整型数值,不重复,由数组实现
  • 压缩列表ziplist:为了节约内存而开发的结构,一个压缩列表包含多个节点,每个节点保存字节数组或者整数值
  • 6种基本数据类型和5种数据结构的对应关系是
    • string:对应int、sds还有一种针对短字符串的embstr结构

    • list:对应linkedlist、ziplist

    • hash:对应hashtable、ziplist

    • set:对应intset、hashtable

    • zset:对应skiplist、ziplist

1.1 讲一下SDS

  • SDS(Simple Dynamic String,简单动态字符串)是redis默认采用的字符串,没有采用C语言的字符串
  • SDS结构体组成包括四个部分:len、alloc、flags、buf[]
    • len:已使用的数组长度
    • alloc:分配的数组长度
    • flags:标志位,低三位表示header类型,共5种类型(3位就可以表示,高五位没有使用)
    • buf[]:字符数组,存放字符串
  • 优点:
    • 获取字符串长度时间复杂度O(1)
    • 防止缓冲区溢出:拼接之前会检查分配的数组长度是否够用
    • 减少因为修改字符串带来的内存重新分配次数
      • 空间预分配:小于1M,扩一倍;大于1M,扩1M
      • 惰性空间释放:缩短字符串长度时不立即回收多出来的字节,剩余空间用于未来扩充字符串
    • 二进制安全:可以存放各种格式的数据:图片、音频、视频、文件等

1.2 zset是如何实现的

// 待完成

1.3 数据结构的应用场景

// 待完成

2.什么是缓存击穿、缓存穿透、缓存雪崩

  • 缓存击穿是指单个key的访问量过高,过期导致大量请求到达数据库

  • 缓存穿透是指查询不存在缓存中的数据,导致每次访问都会到达数据库

  • 缓存雪崩是指发生大规模的缓存失效,导致大量请求到达数据库

2.1 怎么解决缓存击穿

  • 加锁更新:给key的并发请求加锁,同时去数据查询数据写入到缓存。后续其他的请求就可以通过缓存查询数据了

2.2 怎么解决缓存穿透

  • 缓存穿透问题带来的风险是恶意穿透,通过发送大量不存在的数据,导致大量的请求到达数据库,造成系统崩溃

  • 解决方式可以是缓存null值,对应不存在的key值,直接在缓存中返回null,不需要访问数据库,但这种方式只能解决一部分问题

  • 还可以通过添加布隆过滤器,提前验证数据是否存在,对于不存在的数据可以提前处理,不需要访问数据库

2.2.1 介绍一下布隆过滤器

// 待完成

2.3 怎么解决缓存雪崩

  • 设置不同的key过期时间

  • 系统保护机制,包括限流、熔断、降级

2.3.1 介绍一下限流、熔断和降级

// 待完成

2.3.2 限流和熔断的对比

// 待完成

3.redis的过期删除策略

  • 两种,分别是惰性删除和定期删除
  • 惰性删除:查询key的时候才会检测key是否过期,过期的话直接删除,没过期就返回查询的结果
  • 定期删除:顾名思义就是定期对数据库做一次检测,随机抽取一批key,对过期的数据直接删除。

4.redis内存淘汰机制

  • 针对的是过期删除策略没有删除的数据,使用内存淘汰机制来删除

  • key分两种情况:有过期时间和所有的key,分别使用lru、random来组合

  • 包括:volatile-lru、volatile-random、allkeys-lru、allkeys-random,除此之外还有volatile-ttl、noeviction,ttl表示删除将要过期的key,noeviction表示不删除key,禁止写入新数据。

  • 版本更新新加了两种,引入了lfu算法,组合得到volatile-lfu和allkeys-lfu

4.1 介绍一下lru算法

// 待完成

4.2 手写lru代码

// 待完成

5.持久化方式

  • 持久化是为了故障后数据恢复

  • 有两种方式,分别是AOF和RDB

  • RDB:将某个时间点的数据库状态保存在RDB(Redis DataBase)文件中,RDB是一种压缩的二进制文件,可以用来恢复数据库数据

  • AOF:Append Only File,保存会更改数据的命令到AOF文件中,分为追加、写入、同步三个步骤。每个写命令后会把命令追加到缓冲区末尾,按照配置的持久化方式写入到AOF文件中,配置内容包括always、everysec、no三种

5.1 AOF和RDB的优缺点,分别适用的场景

// 待完成

5.2 AOF压缩方式,如何更加高效

6.redis事务

  • 包括几个命令:MULTI、EXEC、DISCARD、WATCH

  • multi用来开始事务

  • exec用来提交事务

  • discard用来取消事务

  • watch在开启exec命令后,监听整个事务中的key是否有修改

6.1 redis事务和传统数据库事务的对比

  • redis不支持事务回滚,对于传统的acid,redis不支持原子性,特殊情况下可以实现持久性
  • 使用AOF持久化模式,配置为always,则每条命令都保存在磁盘中,也能实现持久性

7.槽(slot)

  • redis数据库分成16384个slot
  • 每个节点可以处理0~16384个slot
  • 每个节点有一个slot数组,用来记录处理的槽。数组类型为char(注意是C语言的char,大小是1字节),长度为16384/8=2048字节,每bit对应一个slot
  • 每个slot都有了对应的节点后,集群进入上线状态,客户端可以发送命令
  • 客户端向节点发送命令
    • 如果该节点正好对应处理那个slot,则执行命令
    • 否则就会返回一个moved命令,指引客户端转向正确的节点,再次发送待处理的命令

8.IO多路复用

  • BIO

    • 应用程序发起read调用后,会一直阻塞,直到内核把数据拷贝到应用程序
    • 内核处理:准备数据、数据就绪、拷贝数据
  • NIO

    • 应用程序在准备数据和数据就绪阶段,可以一直发起read调用
    • 在从内核空间拷贝到用户空间的过程中,线程会被阻塞
  • IO多路复用模型

    • NIO需要一直read调用,查询准备结果,浪费CPU资源
    • 改为select调用,准备好了后会返回就绪结果
    • IO多路复用支持的系统调用:select、epoll、avport、kqueue
  • 选择器(Selector),也称为多路复用器

    • 通过Selector,只需要一个线程,就可以管理多个客户端的连接
  • Reactor设计模式

    // 待完成

  • select函数

    // 待完成

  • epoll函数

    // 待完成

参考1

参考2

参考3

9.redis实现分布式锁

  • 分布式锁对应的概念是单机锁,分布式锁对应分布式系统,单机锁对应单进程系统

  • 加锁命令:setnx(set if not exists),这样可以达到一个互斥的效果,多个进程在访问共享资源之前进行加锁操作

  • 释放锁命令:del

9.1 这样做会导致什么问题,怎么解决?

  • 死锁,当申请到锁的进程出现异常,没有释放锁,会导致其它进程无法访问该资源
  • 解决方式是设置一个过期时间,使用命令:expire(读:x pi er)

9.2 这样就可以避免死锁了吗?

  • 不是,仍然会出现死锁
  • setnx申请到锁后,执行expire命令失败
  • 总的原因是两条命令不是原子操作

9.3 怎么解决这个问题?

  • 使用set命令:set lock 1 ex 10 nx // ex:key过期时间,nx:not exists

9.4 还有没有其他问题?

  • 有,锁误消除
  • 进程A设置过期时间t,在t内未能完成任务,但却过期释放了锁
  • 进程B申请锁成功,开始执行任务
  • 进程A完成任务del锁,删除的是B的锁

9.5 怎么解决?

  • 在删除锁之前判断是不是自己加的锁
  • 可以使用线程ID,也可以是UUID

9.6 这样处理的坏处是什么?

  • 会导致出现新的非原子性命令的问题
    • 进程A判断锁是自己的
    • 进程B申请到了锁
    • 进程A释放了进程B的锁
  • 需要保证判断命令给和释放命令是原子性的

9.7 解决方案?

  • lua(读:lu a)脚本

    // 加锁
    String uuid = UUID.randomUUID().toString().replaceAll("-","");
    SET key uuid NX EX 30
    // 解锁
    if (redis.call('get', KEYS[1]) == ARGV[1])
        then return redis.call('del', KEYS[1])
    else return 0
    end
    

9.8 如何合适地设置锁过期时间?

  • 设置ID来判断是不是自己加的锁可以解决锁误删除的问题,但仍然会导致两个进程访问同一个资源
  • 解决方式是合理地设置锁过期时间,设置守护线程,给要过期但未释放锁的进程延长过期时间
  • 以上操作都是针对单机redis的分布式锁实现

  • 但是在集群redis上仍然会出现问题

// 待完成

参考1

参考2

参考3

10.哨兵

// 待完成

11.redis集群

  • redis集群的组成是节点node
  • 节点之间通过cluster meet(读:class ter)命令连接(构建一个包含多个节点的集群)
    • 客户端发送cluster meet命令给节点A
    • A收到IP地址和端口号,向B发送meet消息
    • B收到meet消息,向A返回pong
    • A收到B的返回消息,向A返回ping消息,建立连接
    • 节点A广播节点B的信息,集群的其他节点和B建立连接

12.redis为什么快

  • 关键点:内存、C语言、单线程、IO多路复用
  • 基于内存操作
  • 由C语言实现,底层数据结构都经过优化
  • 单线程,无上下文切换的成本
  • IO多路复用机制

13.如何保证缓存一致性

// 待完成

14.如何基于redis设计秒杀系统

// 待完成

14.1 超卖问题如何解决

// 待完成

15.redis主从复制