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函数
// 待完成
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 end9.8 如何合适地设置锁过期时间?
- 设置ID来判断是不是自己加的锁可以解决锁误删除的问题,但仍然会导致两个进程访问同一个资源
- 解决方式是合理地设置锁过期时间,设置守护线程,给要过期但未释放锁的进程延长过期时间
以上操作都是针对单机redis的分布式锁实现
但是在集群redis上仍然会出现问题
// 待完成
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 超卖问题如何解决
// 待完成