目录
一、数据类型及使用场景
redis是kv存储系统,key为字符串类型,值为redis对象。
由5种常用数据类型:String,List,Hash,Set,ZSet。
1、String
底层编码格式:
- int:当保存数据时整形(64位long)时,如果是浮点数类型,redis是现将浮点数转为字符串然后再保存;
- 简单动态字符串。
底层没有直接使用C字符串,而是使用简单动态字符串实现。区别:(1)是二进制安全的,不会遇到'\0'而终止,可以存储图像和序列化对象;(2)结构中有表示长度的字段,获取字符串长度时间复杂度时O(1);(3)空间不足时会自动扩容,大小小于1M时每次扩容一倍空间,大小大于等于1M时,每次扩容1M。惰性空间释放,数据修改导致长度变短时不会立即释放多余空间,而是把多余空间大小记录到free字段中,下次不够时优先使用多余空间,再进行扩容,减少了内存分配开销。
2、List
数据量小时使用ziplist存储,节省内存;当ziplist节点数超过512个,或者单个节点大小超过64字节,就会使用QuikList存储。
(1)ziplist
ziplist是把节点存放在一块连续内存里,节点内存储上一节点大小和当前节点的大小。不采用双向链表形式是因为(1)链表的前驱和后继指针都占用额外的内存空间,在数据量少的情况下实际数据所占大小还不如指针浪费的多(2)每个链表节点单独分配内存,内存不连续续会产生内存碎片(3)根据局部性原理,连续内存也能更好的命中CPU Cache。
操作时间复杂度:
- 头尾压入/弹出操作,查询头尾元素时间复杂度都是O(1);
- 对于中间元素查找复杂度是O(N),当新增/修改某个元素时内存空间要重新分配,可能会触发连锁更新。
- 每种redis数据结构都有单独的字段保存该数据长度,因此获取长度是O(1).
ziplist连锁更新:
每个节点都包含一个字段prevlen表示上一节点的所占大小,该字段是变长编码的。当长度小于254时使用1字节存储prevlen,当大于254时使用5个字节存储prevlen。所以当在e1,e2之间插入新节点,新节点大小超过254而e1小于254,那么e2的prevlen存储方式就会发生改变,以此类推会触发连锁更新。
(2)QuickList
quicklist是使用ziplist组成的双向链表,每个节点使用ziplist存储数据。
3、Hash
数据量小时使用ziplist存储,当ziplist节点数超过512,或者单个节点大小超过64字节使用dict存储。ziplist上面介绍过,这里只介绍dict实现方式。
typedf struct dict{
// 两张hash表
dictht ht[2];
// rehash索引,字典没有进行rehash时,此值为-1
int rehashidx;
...
}dict;
dict中定义了两张哈希表,当数据量增加出现哈希冲突,哈希表通过数组+链表来解决。数据量进一步增加,负载因子(数据量/哈希表槽数)大于一定值时进行扩容操作,当负载因子小于0.1进行缩容。
扩容或缩容过程中,由于哈希表大小改变了存储位置又是hash 值/len,所以会发生rehash过程。dict使用渐进rehash方式:每次执行操作时都将第一张表相同位置的键值rehash到另一张表中,最终所有键值都迁移过去,rehash过程结束,rehashidex=-1.
- 查询时先查第一个哈希表,如果此时正在rehash,查询另一张表;
- 插入时,如果正在rehash直接插入到另一张表中,否则插入到第一张表;
- 删除时删除第一张表的元素。
优缺点:避免一次集中rehash计算量太大阻塞住redis,将计算量分散到每次操作;同时保留两张表,内存占用大,如果机器内存打满此时rehash会导致大量key丢失。
4、set
当数据都是整数值,并且数据个数小于512时使用intset存储,节省内存,否则使用hashtable存储。
整数集合intset
整数集合底层为数组,以有序无重复的方式存储整形数据,插入、查找时间复杂度o(n)?
5、zset
数据量小使用ziplist,当ziplist节点个数大于128,或者节点大小大于64字节时使用skiplist跳表来存储。
跳表:
二、Redis
1、redis线程模型
redis是基于单线程的Reactor模式实现,通过IO多路复用的select、epoll操作监听读写等IO事件,读写事件收集完后,循环的在内存中处理读写请求。
为什么这么快?
- 单线程模型,避免了上下文切换、同步机制以及互斥锁带来的开销;而且更好地利用cpu cache提速;
- 纯内存操作,所有读写均在内存中执行,不访问磁盘,cpu不是redis瓶颈,瓶颈仍然在网络io,redis6.0多线程是是在监听io事件开启多线程;
- 高效数据结构,string、list、hash、set、zset。
redis应用场景有哪些?
- 缓存热点数据,减轻数据库压力;
- 分布式锁
- 分布式限流器
2、redis事务
(1)使用方式:
使用MULTI开启一个事务;之后的命令都会被插入到队列中去,不会立即执行;使用EXEC命令提交事务,也可以使用discard放弃组队。
组队阶段会检查命令语法,如果语法错误会报错;如果是业务逻辑出错(比如逻辑bug),组队过程中不会报错,提交之后才会出错但不影响其他命令执行,不影响其他命令执行。
特点:
- redis是单线程运行,事务队列中的命令会按序执行不会被其他事务所打断,所有命令都会执行,天然具备隔离性;
- redis事务中某条命令执行出错不会影响其他命令执行,不具备原子性,不支持回滚。
- 不支持持久性。redis数据持久依赖于RDB和AOF操作,
- 使用RDB模式,一个事务执行后、下一RDB快照执行前实例崩溃,事务无法持久化;
- 使用AOF模式,AOF配置no、eveerysec都存在数据丢失的情况,always可以保证持久性但性能太差,一般不使用。
(2)lua脚本
lua脚本同样具备隔离性,不具备原子性,不支持回滚。redis中的命令都是原子性的,因为redis是单线程程序,命令执行过程中不会被其他命令影响到。而运行lua脚本使用eval命令,eval是原子性的,当lua脚本执行出错时,之前的操作不会回滚。
3、watch命令实现乐观锁
WATCH
命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行。可以实现乐观锁。
WATCH version;
set version 1;
MULTI;
lpush mylist 123;
...
set version 2;
EXEC;
//如果version等于2说明事务执行了,其他事务不会执行,否则说明当前事务执行失败了
4、持久化机制
(1)RDB:默认方式,是在指定时间间隔,定期将内存数据的快照保存到磁盘上。保存在dump.rdb文件中,重启时根据dump.rdb文件恢复数据到内存。
由自动和手动两种方式来执行rdb持久化。通过save命令可以设定周期,定期同步rdb文件,save执行过程中会阻塞客户端请求,运行过程中不要使用save。bgsave命令是异步方式,会fork一个子进程执行快照保存,不会阻塞主进程处理请求。
优缺点:
- 比aof体积小,rdb数据只是内存快照;
- 比aof速度快,重启时内存快照不用像aof挨个执行命令;
- 效率高,bgsave子进程同步不影响主进程处理请求。
- 故障时丢失数据多,无法做到实时的持久化,bgsave执行时fork子进程属于洪亮吉操作,频繁执行成本高。
(2)AOF(append only file):以日志方式记录redis写命令,重启时会重新执行aof文件中的命令来恢复数据。在配置中开启appendonliy yes,redis每次执行写命令时会将命令写入内存aof buf缓冲区,然后根据具体策略将aof buf命令追加到磁盘的aof文件,具体策略有:everysec:每秒进行同步,no:右槽组系统决定何时同步,always:每执行一条谢明令都会同步。
当aof文件大小大于阈值时会自动重写aof,对同一key的操作进行合并。
- 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。
- 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。
- 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。
三、部署方案
1、主从模式:
一主多从,主节点负责写请求,并将数据同步到从节点,全部读请求都走从节点,可以轻松实现水平扩容。主节点挂掉后需要手动执行主节点,可用度不高,基本不用。
主从模式无法做到故障自动转移,当主节点故障时需要人为的手动指定主节点,这段时间内主服务不可用,而且这段时间的数据变更无法同步。
2、哨兵模式:
- 每个哨兵以每秒一次的频率向主、从节点、其他哨兵节点发送PING命令,如果在规定时间内没有收到实例的有效回复,那么该实例会被标记为主观下线;
- 对于被标记为主观下线的主节点,哨兵会向主节点发送命令确认是否真正下线,当超过半数哨兵认为主节点下线后,主节点被标记为客观下线。
- 哨兵节点通过投票机制,从从节点中重新选举出主节点。
3、Redis Cluster
主动模式(包括哨兵模式)下,只有一master只能单点写,无法对写请求水平扩容,而且每个节点保存所有数据,内存占用高,数据恢复慢。Redis Cluster对数据进行分片,每个节点对应一个分片,Cluster内有主备两类节点,主节点处理读写请求,备节点作为备份不处理请求,主备节点通过异步方式,同步数据保证一致性。
Redis cluster使用哈希槽进行数据分片,而不是一致性哈希,内部有16384个哈希槽,key的存储位置为hash(key) % 16384;
四、常见问题:
1、过期键删除策略:
1、惰性删除。在访问key时,如果发现key已经过期,那么会将key删除,对内存不友好。
2、定期删除。定时清理key,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。
3、内存不够时清理。Redis有最大内存的限制,通过maxmemory参数可以设置最大内存,当使用的内存超过了设置的最大内存,就要进行内存释放, 在进行内存释放的时候,会按照配置的淘汰策略清理内存。
2、