目录
五种数据结构
简单动态字符串sds
- 这种数据结构的应用举例,设置过期时间,发短信的时候,设置验证码的过期时间。key一定是String类型,此时value用的是String类型。
- 结构
typedef char *sds;
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};
- 对比C字符串,sds有以下特性:高效的执行长度计算o(1);高效的执行追加操作(使用free 记录未使用空间的大小,sdshdr 可以让执行追加操作所需的内存重分配次数大大减少);二进制安全(程序不应对字符串里面保存的数据做任何假设)
链表
redis 127.0.0.1:6379> LPUSH runoobkey redis
(integer) 1
redis 127.0.0.1:6379> LPUSH runoobkey mongodb
(integer) 2
redis 127.0.0.1:6379> LPUSH runoobkey mysql
(integer) 3
redis 127.0.0.1:6379> LRANGE runoobkey 0 10
1) "mysql"
2) "mongodb"
3) "redis"
- 应用举例:最近访客可以用这种数据结构。采用双端队列的形式。此类型允许重复
- 节点带有前驱和后继指针,访问前驱节点和后继节点的时间复杂度为o(1),链表带有只想表头和表尾的指针,因此对表头和表尾进行处理的复杂度为o(1),链表带有记录节点数量的属性,所以可以在o(1)复杂度内返回链表的节点数量
- 数据量少时用ziplist
hash类型
- 举例
redis 127.0.0.1:6379> HSET myhash field1 "foo"
(integer) 1
redis 127.0.0.1:6379> HSET myhash field2 "bar"
(integer) 1
redis 127.0.0.1:6379> HKEYS myhash
1) "field1"
2) "field2"
- 在一些不确定有多少字段的时候可以使用
- 使用以下两种数据结构作为底层实现:字典(哈希表作为底层实现)、压缩列表。因为压缩列表比字典更节省内存,所以程序在创建新Hash键时,默认使用压缩列表作为底层实现,当有需要时,程序才会将底层实现从压缩列表转换为字典。
-
字典结构,字典有两个hash,另一个用作扩容时候的,还有rehashindex字段用来表示是否在rehash,用链表法解决hash冲突
- rehash执行过程
- 创建一个比ht[0]->table大小至少两倍的ht[1]->table;
- 将ht[0]->table中的所有键值对前一代ht[1]->table;
- 将原有ht[0]的数据清空,并将ht[1]替换成新的ht[0];
- 渐进式rehash 由于ht[0]比较大的时候整体rehash会造成服务器出问题所以,会渐进的rehash,比如渐进4个rehashidx会从-1 变到0,1,2,3再到-1 这时候就可以进行增删操作,查找时是先找ht[0]再找另外个,增加是增加ht[1]里面的.
-
ziplist压缩列表 一个由一系列特殊编码的连续内存块组成的顺序性数据结构
Set集合
- 赞功能实现,就是有喜欢的集合与不喜欢的集合。不允许重复
- 底层使用了intset和hashtable两种数据结构存储的,intset我们可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)
- 结合对象保存的所有元素都是整数值, 集合对象保存的元素数量不超过512个 使用intset
- intset内部其实是一个数组(int8_t coentents[]数组),而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的
Sorted set集合
-
应用举例:排名功能命令举例
- 与红黑数对比 skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现
-
当元素少时使用压缩列表ziplist ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置
定时删除策略
三种定时删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。定时删除策略对内存是最友好的,但对cpu不友好
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。 对cpu是最友好的,但对内存不友好
- 定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多个过期键,以及要检查多少个数据库,则由算法决定。 cpu, 内存折中
- Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡
AOF、RDB和复制功能对过期键的处理
- 在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中
- 当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一个DEL命令,来显式地记录该键已被删除, 和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中
主从复制对过期键处理
- 流程,通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性
- 主服务器在删除一个过期键之后,会显示地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续处理未过期的键一样来处理过期键
- 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键
持久化
RDB持久化
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发
- 触发机制,手动触发分别对应save和bgsave命令
- ·save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用
- ·bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
- 自动触发,使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改 时,自动触发bgsave,bgsave是主流的触发RDB持久化方式
- 优缺点
非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。·Redis加载RDB恢复数据远远快于AOF的方式。
RDB的缺点:·RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运 行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
AOF持久化
- AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF通过追加写命令到文件实现持久化,通过appendfsync参数可以 控制实时/秒级持久化
- 流程(也是通过fork子进程实现的)
- 所有的写入命令会追加到缓冲区中
- AOF缓冲区根据对应的策略向硬盘做同步操作
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
持久化阻塞场景
- RDB save命令
- fork子进程
- AOF文件替换旧的AOF文件
redis实践
实践
设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效.
-
缓存击穿:对于一些设置了过期时间的key, 刚好过期的时候,这时候有个高并发的请求,会导致直接访问数据库,危险.写入的时候先更新数据库再删除缓存
解决方案: 1. 加锁 2. 双重key,一个key时间短缓存key过期时间,当线程查询到快过期时,去数据库更新数据到缓存。另一个key缓存数据。
缓存穿透:查询一个一定不存在的数据,导致直接访问数据库。
解决方法:有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
- 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效。 随机设置过期时间。
为什么是单机
- Redis 利用了多路 I/O 复用机制,处理客户端请求时,不会阻塞主线程;Redis 单纯执行(大多数指令)一个指令不到 1 微秒,如此,单核 CPU 一秒就能处理 1 百万个指令(大概对应着几十万个请求吧),用不着实现多线程(网络才是瓶颈)。
- 还要考虑Redis操作的对象。它操作的对象是内存中的许多复杂的数据结构。如果在多线程中操作,那就需要为这些对象加锁。最终来说,多线程性能有提高,但是每个线程的效率严重下降了。而且程序的逻辑严重复杂化。如果真的加锁那可能带来一个恶果就是吞吐量虽然增大,但是响应延迟可能会增加。在单线程基础上任何原子操作都可以几乎无代价地实现,多么复杂的数据结构都可以轻松运用,甚至可以使用Lua脚本这样的功能。对于多线程来说这需要高得多的代价。并不是所有的KV数据库或者内存数据库都应该用单线程,比如ZooKeeper就是多线程的
redis多机
redis 主从复制
-
全量同步, 不阻塞master,一般在新连接时执行
- 增量同步, 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令
- 从可设置为只读,删除由主发送命令,主从之间保持心跳
redis哨兵sential(有点像zookeeper)
- 监控所有节点数据库是否在正常运行,提供api报警机制, 自动故障转移。
- 当主观下线的节点是主节点时,此时该哨兵寻求其它哨兵节点对主节点的判断,如果其他的哨兵也认为主节点主观线下了,则当认为主观下线的票数超过了quorum(选举)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了
- 客观下线之后哨兵需要宣促 leader来进行自动故障转移,采取先到先得,如果票数超过quorum则选为leader
- leader哨兵进行故障转移时
- 过滤掉主观下线的节点
- 选择slave-priority最高的节点,如果由则返回没有就继续选择
- 选择出复制偏移量最大的系节点
- 选择run_id最小的节点
redis集群
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
- 节点的fail是通过集群中超过半数的节点检测失效时才生效
- 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
- redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value。Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
- Redis Cluster的Resharding是指在Cluster内的节点之间转移slots中的键数据,一个slot之前由某个节点负责,在Resharding之后,可能由另外一个节点负责。
redis分片槽与一致性hash算法
- redis 3.0使用的是hash槽的概念,没有使用一致性hash算法.Redis的作者认为它的crc16(key) mod 16384的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便。当往Redis Cluster中加入一个Key时,会根据crc16(key) mod 16384计算这个key应该分布到哪个hash slot中,一个hash slot中会有很多key和value。可以理解成表的分区,使用单节点时的redis时只有一个表,所有的key都放在这个表里
- 一致性hash参考一致性hash算法和redis集群动态数据存储
redis事务
- 通常会使用MULTI,EXEC,WATCH,redis的事务不支持回滚,事务执行时会阻塞其它客户端的请求执行。
- 事务执行流程 事务开始,命令入队,事务执行
redis > MULTI
OK
redis > SET "username" "bugall"
QUEUED
redis > SET "password" 161616
QUEUED
redis > GET "username"
redis > EXEC
1) ok
2) "bugall"
3) "bugall"
redis事务的acid
- 对于redis的事务功能来说,事务队列中的命令要么就全部执行,要么就一个都不执行,因此redis的事务是具有原子性的。redis事务当事务中的命令执行失败后面的命令还 会执行
- redis通过谨慎的错误检测和简单的设计来保证事务一致性。
- redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事物进行中断,因此,redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的
- RDB,AOF实现持久性
Redis watch命令——监控事务
-
Redis 参考了多线程中使用的 CAS实现的
redis项目使用情况
- redis配置qps 7000, 8台redis 每台16g左右,四个集群,一主一从,平均每个key 几k这样
redis性能优化
优化网络延时
- Redis 的官方博客在几个地方都说,性能瓶颈更可能是网络
总结:
小结一下:
- 使用 unix 进程间通信,如果单机部署
- 使用 multi-key 指令合并多个指令,减少请求数,如果有可能的话
- 使用 transaction、script 合并 requests 以及 responses
警惕执行时间长的操作
- 尽量不要在生产环境的代码使用这些执行很慢的指令
优化数据结构、使用正确的算法
- 可参考本文章的五种数据结构
考虑持久化带来的开销
- 可参考本文章的RDB,AOF持久化
使用分布式架构 —— 读写分离、数据分片
- 主从模式
redis分布式锁
redis经典问题学习
- redis问题总结篇一,其中包括jvm 并发问题
- redis问题总结篇二, 只有redis
- [Redis 由浅入深深深深深剖析] (https://mp.weixin.qq.com/s/5wAp844dUrV5djCcJzZipg)
- [厉害了,如何通过双key来解决缓存并发问题] (https://mp.weixin.qq.com/s/UXeJSoOA-8-6KezV0T0Xmw)
- [Redis 事务] (https://mp.weixin.qq.com/s/US0tbH1SjMNXVy5rdYsxvw)
- [redis原理总结] (https://mp.weixin.qq.com/s/k8G7ZYdKFl1ZZleehGf49g)
参考
- Redis 性能问题分析
- Redis 设计与实现
- redis总结
- redis底层设计(一)
- Redis压缩列表原理与应用分析
- Redis:有序集合类型zset实现原理
- Redis详解:Redis过期键删除策略
- Redis持久化
- redis 事务实现原理
- Redis watch命令——监控事务
- redis主从复制原理总结
- redis主从复制下哨兵模式---选举原理
- Redis哨兵机制原理
- 因 Redis 分布式锁造成的 P0 级重大事故,整个项目组被扣了绩效。。。
- Redis Lua脚本 实现分布式锁
- redis问题总结篇一,其中包括jvm 并发问题
- redis问题总结篇二, 只有redis
- [Redis 由浅入深深深深深剖析] (https://mp.weixin.qq.com/s/5wAp844dUrV5djCcJzZipg)
- [厉害了,如何通过双key来解决缓存并发问题] (https://mp.weixin.qq.com/s/UXeJSoOA-8-6KezV0T0Xmw)
- [Redis 事务] (https://mp.weixin.qq.com/s/US0tbH1SjMNXVy5rdYsxvw)
- [redis原理总结] (https://mp.weixin.qq.com/s/k8G7ZYdKFl1ZZleehGf49g)
- redis MySQL mongoDb区别, mongodb场景,游戏根据id获取所有基本属性,金钱属性之类的。一个所以key -> 很多文档value
- SNS和SQS区别,发布订阅更多的是不是同类型比如发短信和发邮件一起,数据发送s3,发DB。消息系统更多的是同一类型。