1. 数据存储和查找发展
文件:慢,因为全量扫描->IO,因此慢
mysql:为什么数据库存东西,因为快,为什么,因为做了分块(datapage),然后对这些块做了索引,所以我们先查索引,找到对应是哪块,然后只查这一块,就快很多。那要扫描索引?,将索引加载进内存并且采用B+树结构可以避免扫描整个索引。
面试题:如果表很大,增删改查会变慢吗?数据量大了,增删改是一定会变慢的。查询:得分场景,即便表很大,但是如果一个客户端发送一个简单的查询请求,索引命中了情况下,查询还是毫秒级的。但是如果高并发情况下,查询是变慢的,因为查询时间应该包括寻址时间和磁盘io时间,即使寻址是毫秒级的,但是由于带宽,吞吐等硬盘限制,高并发场景下会受到硬盘的限制而变得很慢,否则也不会出现分布式微服务上来就是redis这种事情
redis:综上所述,并发情况下限制主要在硬盘,那所有数据存在内存中呢?那当然快了!还真有这么一种内存型关系数据库:SAP HANA,可是内存它贵呀,动不动就上T的数据存在内存中太烧钱了。redis的产生理念:其实只有一小部分数据频繁被访问(被称为热点数据),很大一部分数据都是不经常用的就存在mysql里,因此使用redis一定要明确使用场景:热点数据。
2. Redis特点
面试可不要只说一点啊,看我的!
NoSQL系列,key-value形式,内存数据库:NoSQL是非关系型数据库的统称,redis以键值对的形式存在,在内存中速度会比mysql快几个量级。为什么redis是key-value型的而不是sql形式:不妨根据上面所说的大胆猜测一下,关系型数据库中通常会接触到范式,A表外键为B表的主键,而redis设计初衷就是只存一部分热点数据,因此无法确定是否包含在B中,因此redis应该只关注自身,不关注依赖。
有自己的数据类型,实现了计算向数据移动(这个词就很专业):有String, Set, List, Hash, Sortedset五种类型,并且每种类型有自己的本地方法,对比memcache(所有的value都是string类型并通过json转换),如果需要取出数组第二个元素,memcache需要将整个list字符串返回,调用方拿到数据转换成列表再取第二个元素,redis可以读取list并通过lindex命令取出第二个元素并返回,也就是先计算再io,这种计算向数据移动的思想在map-reduce中有体现,可以减少传输的数据量,很快。
工作线程是单线程的,看下面第四点
3. 五种数据结构
api我就不多说了,可以看官方文档/中文文档/菜鸟教程
String
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数值和Bitmap。 使用场景包括 常规计数:微博数,粉丝数等
bitmap
重点说一下这个,有一些特殊的场景运用
- SETBIT key offset value
8个位为一个字节,SETBIT key1 6 1为00000010,offset为从左往右从0开始数
超过了7,则为第二个字节,SETBIT key2 9 1为00000000 01000000
- BITCOUNT start end
统计有多少个位为1,注意start和end以字节为单位而不是以位为单位
场景1:统计某个用户某段时间内的登录天数
这个如何用bitmap巧妙完成呢,用户在哪天登录,就将其作为offset,比如用户在第1天和第365天登录
SETBIT wjw 1 1
SETBIT wjw 365 1
统计的时候就可以
BITCOUNT wjw 0 -1
可以计算一下,这个key最多也就占46个字节(46×8=368,因为一个bitmap是8的倍数)
场景2:统计某段时间内的活跃用户数量
key是日期,offset是用户id,value是1
如wjw的用户id是1,whx的用户id是2,那么
SETBIT 20200101 1 1
SETBIT 20200101 2 1
SETBIT 20200102 1 1
也就是wjw在第一天和第二天登录,whx只在第一天登录
那么这两天的活跃用户数量为
BITOP or res 20200101 20200102
BITCOUNT res
得到res等于2
Hash
hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等
底层数据结构如下
- Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
- Redis使用MurmurHash2算法来计算键的哈希值。
- 哈希表使用链地址法来解决键冲突。
注意:这里和Java的HashMap不同的rehash过程
- Redis的rehash过程是扩展和收缩,而且还是渐进式的rehash
- Redis的字典有两个哈希表ht[0]和ht[1]
- 为字典的ht[1]哈希表分配空间,如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used *2的2^n;如果执行的是收缩操作,那么ht[1]的大小第一个大于等于ht[0].used的2^n。(举个例子,ht[0]的长度为10,那么扩展就是2^5的32,如果是压缩的话2^4=16)
- 如果ht[0]的键值非常多的话,一次性转移过去,是一个非常耗时的操作哦,因此并非一次性,采取渐进式rehash转移。
List
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表、粉丝列表、消息列表、评论列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高
Set
当你需要存储一个列表数据,又不希望出现重复数据时用set,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作
场景1:抽奖
SRANDMEMBER key 3,不重复的随机抽3个
SRANDMEMBER key -3,可重复的随机抽3个
场景2:推荐系统
交集:共同关注:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常 方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
sinterstore key1 key2 key3
将交集存在key1内差集:可能认识的人:SDIFF
并集:好友圈:SUNION
Zset
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
底层实现是一个跳表SkipList
放知乎程序员小灰的两篇文章
- 简单来说跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
- 跳跃表平均O(longN),最坏O(N)复杂度的节点查找
- 跳跃表有个层的概念:层带有两个属性:前进指针和跨度,前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。一般情况下,层越多,查找效率越高。
4. Redis的NIO和线程模型
redis是单线程的还是多线程的?
用户对redis操作是单线程的(worker单线程),redis 6.x支持多线程,但是!!!这叫io threads,用于处理用户的请求,实际操作还是单线程的。
5.x和6.x对比如下
可以看到在6.x的时候worker线程(计算那里)还是单线程,但快了不少。
4.1 为什么是单线程的
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。redis核心就是:如果我的数据全都在内存里,我单线程的去操作就是效率最高的,因为免去了线程之间的上下文切换开销。
4.2 秒杀场景的redis使用
高并发场景,多个service线程。如果是mysql不加锁,必定会脏读,造成超卖,必定加锁同步,实际退化成所有service串行,加上前后其他业务逻辑,很慢。改用redis,单线程,本身就是串行化操作,通过decr进行商品数量减一
4.3 strace追踪线程
strace可以追踪一个进程以及所有线程对操作系统的系统调用有哪些。比如是否使用了fork,是否使用了epoll,是否又创建了线程
1 | sudo apt install strace |
进入redis目录(src或者守护进程的都行)
1 | strace -ff -o /home/wjw/stracelog/out |
-ff表示监听fork系,第二个f表示包括线程
-o表示输出目录为/home/wjw/stracelog并以/out为前缀
可以进入/proc/3324/task目录下
这些是什么?3324是线程号,task代表任务,也就是开启了4个线程
用vi或者gedit打开out.3324可以看到很多epoll相关的系统调用,因此redis的线程采用了epoll多路复用的技术
客户端输入bgsave,服务端3324线程开启了新的线程3829进行保存,使用clone系统调用,copy on write技术
查看strace的跟踪
5. 缓存雪崩,穿透,击穿
缓存雪崩:是说redis大量内容同时过期,因此db突然之间多了很多请求。解决方法就是随机过期时间
缓存穿透:查询id=-1,第一次查db,查不到就不放进redis,第二次查一定不会在redis中,又去查db,如此往复…恶意发送大量查询请求,导致一直查db,就是缓存穿透。解决办法就是把-1: null存进redis,或者限制同一个key在一段时间的请求次数,或者是使用过滤器过滤掉
布隆过滤器:就是一个很长的bit数组,通过一级或者多级不同的hash函数之后将key转换成多个位的1,然后在bit数组上看是该key否存在,然后每次添加入redis时也会将这个key的hash位置为1。需要注意的地方有两个
- 这里既然是Hash函数,那就存在一个问题,不存在的key也有可能经过hash函数之后对应位全为1,被误判为key存在,不过没关系。hash判断存在,key不一定存在。hash判断不存在,key一定不存在
- bit数组里面的位只能添加为1不能修改回0,因为改为0可能会影响多个key。所以要判断好这个数组的长度,否则经过多次置1后数组全为1,过滤器就失去了过滤意义,可以重新扩容。
因为这些问题,后面还有其他的过滤器比如布谷鸟过滤器
缓存击穿:跟缓存雪崩类似,是因为过期导致的,但是不是大量内容,而是说对同一个key发出大量的请求(比如秒杀),突然过期了,导致查db。这种场景解决办法就是查db的时候加锁,过期–>加锁查db–>db压力小–>重新放进redis–>查redis
6. Redis内存回收策略
两个方面:
- key过期时被删除
- 内存占用达到maxmemory触发内存溢出控制策略
6.1 删除过期key
redis不会实时监控每个key的过期时间并删除,而是采用惰性删除和定时任务删除
- 惰性删除:过期的key并没有被直接删除,而是等到下一次访问的时候才删除。访问一个key,判断ttl,如果为0过期了,则删除并返回null,否则正常返回value。如果一直没有被访问,则会造成内存泄漏
- 定时任务删除:为了解决上述内存泄漏问题,redis每秒运行10次定时任务
- 慢模式:随机检查20个键,过期则删除,过期超过25%,循环执行上述过程直到低于25%,超时时间为25ms
- 快模式:逻辑和快模式相同,只是超时时间不同。如果慢模式超时则先执行一次快模式,快模式下超时时间为1ms且2s内只能运行1次
6.2 内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:
- noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。如果没有对应的键,则回退到noeviction策略。
参考
作者:杨鑫科
链接:https://www.jianshu.com/p/6a5eb0ddf57b
来源:简书
7. Redis持久化
redis存在内存中,服务器故障就造成了数据丢失,可以通过一些持久化策略来保护数据
7.1 RDB(redis database)
默认开启,将redis中的数据转换成二进制文件,调用save/bgsave/自动化时产生dump.rdb文件,适合全量备份。
- save:阻塞(调用时其他client的命令不能执行),覆盖(有旧的就替换)
- bgsave:后台进行,原理是保存fork出来的副本,因此调用bgsave后即使修改了也不会被保存,因为副本没有变。也阻塞但阻塞的只是fork阶段,不阻塞客户端命令,缺点是需要消耗额外的内存
- 配置自动触发:redis.conf配置文件中设置save m n。表示m秒内数据集存在n次修改时,自动触发bgsave。
rdb的三种触发方式都是保留了触发前最后一刻的状态,因此也称为快照
恢复:将dump.rdb放在redis安装目录(可以通过config get dir命令获取)并启动redis即可
7.2 AOF(append only file)
默认不开启,记录的是操作命令而不是内存数据,类似于日志文件。他不是记录每一步操作,而是调用bgrewriteaof命令时将当前内存中的数据生成命令的形式保存下来
也有三种触发方式
(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
(3)no:从不同步
redis4.x之前开启了AOF就不用rdb了,4.x及以后用混合模式,恢复很快
8. 高可用架构
主从架构+哨兵模式+集群
8.1 主从架构
作用
- 读写分离:从机只读不可写,缓解了主机读压力,写从机会报错(you can’t write read only slave)
- 容灾恢复:从机会复制主机的数据,相当于备份(采用的是记录复制的偏移量的方式,可以断点续传 )
- 高可用:主机一挂,从机能升级为主机
配置方法
如果是同一台主机做实验,那么要修改redis.conf(默认为6379.conf)中的端口号、pid、日志名、rdb名(默认为dump.rdb,会覆盖)
查看角色命令为
info replication
主机会显示 role: master 并显示从机的ip和端口
从机会显示 role: slave 并显示主机ip和端口
从机执行命令
slaveof 主机ip 主机端口
并配置认证密码即可,主机无需配置
主从复制过程
首先明白两个关键字
runid: master节点的唯一标识,每次启动时生成,可以通过info server
查看
offset: slave需要从哪个位置开始同步数据(可以断点续传)
除了全量复制,还有部分复制,防止全量复制中因网络抖动而失去连接
8.2 哨兵模式
哨兵就是用来监管各个机器的健康状态的,当主机宕机时,根据偏移量将某一台从机通过slaveof no one变成主机,其他主从机slaveof newIp newPort,哨兵可以帮我们自动完成这个过程。
8.3 Redis集群
redis3.0之后的新特性,主从复制还不能实现高可用,因为只分担了读压力,使用Redis Cluster,多个”节点”,每个节点都采用s主从架构,每个主节点都可以支持读写操作
每个节点负责的区域不同,数据是不一致的,要获取数据时必须先通过crc16算法再%16384得到对应的节点地址
后期新增节点可以进行hash迁移,潜移部分数据分担其他节点的压力