Redis的数据都是存放在内存中,而像关系型数据库Mysql的数据存放在磁盘。访问磁盘数据是要进行网络IO连接,是很耗时的,而内存的数据访问和操作是相当快的。
我们都知道,mysql为了提高效率,采用了B+树的数据结构,对于一个应用场景来说合理的数据结构能够性能更好。我们来看看Redis的数据结构-内部编码。
String :动态字符串SDS。
List :双端链表LinkedList + 压缩链表zipList。
Hash:压缩链表zipList + 字典hashTable。
Set:字典hashTable。
ZSet:压缩链表zipList + 跳跃表skipList。
SDS简单动态字符串
- 字符串长度处理:Redis获取字符串长度的复杂度 O(1),在C语言获取字符串长度从头开始遍历,复杂度为O(N).
- 空间预分配:字符串频繁的修改,内存分配就越频繁,就会消耗性能,而SDS修改和空间扩展,会额外分配未使用的空间,减少性能消耗。
- 惰性空间释放: SDS缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free记录的空间,减少内存的肥分配。
- 二进制安全:Redis可以记录二进制的数据,在C语言中字符串遇到’\0’会结束,而SDS中标志字符串结束的是len属性。
字典
Redis作为K-V型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如HashMap,通过key就可以找到对应的value。哈希表的特性就是,在O(1)时间复杂度下就可以获得对应的值。
跳跃表
- 跳跃表就是在链表的基础上,增加多级索引来提升查询效率
- 跳跃表支持平均O(logN) 到 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
Redis支持多种数据机构,每种基本类型,对应多种数据结构。什么时候使用什么样的数据结构,使用什么样的编码,是redis设计者总结优化的结果。
I/O多路复用
I/O多路复用技术可以让单个线程高效的处理多个连接请求,而Redis将epoll作为IO多路复用的技术实现。并且,Redis的自身事件处理模型将epoll的连接,读写,关闭都转化为事件,不在网络I/O上浪费过多时间。
I/O:网络IO
多路:多个网络连接
复用:复用同一个线程
I/O多路复用,就是一个同步IO模型,它实现了一个线程可以监听多个文件句柄;一旦文件句柄就绪,就会通知应用程序进行相对于的读写操作,当没有文件句柄就绪时,就会阻塞应用程序,交出CPU。
单线程
Redis是使用单线程来进行指令操作的,避免了CPU不必要的上下文切换和锁的竞争的损耗,并且是基于内存操作数据,以达到最快的速度。
Redis6.0版本引入了多线程的概念,但一般是网络相关的指令等IO操作时,会新开一个线程去执行,而执行命令操作数据等指令还是单线程去串行执行,仍是线程安全的。
这样做的目的是因为redis的性能瓶颈在于网络IO而不是CPU,使用多线程提升IO读写的效率,从而提高整个Redis的性能。
Redis直接自己构建了VM机制,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
Redis的虚拟内存机制是什么?
虚拟内存机制就是暂时把不经常使用的数据(冷数据)从内存交换到磁盘,从而腾出宝贵的内存空间以供其他需要访问的数据(热数据)。通过VM机制可以实现冷热数据分离,使热数据在内存中,冷数据保存在磁盘中。这样就可以避免因内存不足而造成访问速度下降的问题。
读取缓存步骤一般没有什么问题,但是一旦遇到数据的更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(Mysql)间的数据一致性问题了。
不管是先更新数据库DB,再删除Redis缓存;还是先删除Redis缓存,再更新数据库DB,都有可能出现数据不一致的情况。
举个栗子:
1,如果删除了Redis缓存,还没有来得及更新数据库DB,另一个线程来读取,发现缓存为空,则去读取数据库的旧值,并写入缓存,此时缓存中为脏数据。
2,如果先更新了数据库DB,在删除缓存前,出现了异常,导致没有删除掉缓存,则也会出现数据不一致。
因为读和写是并发的,没法保证顺序性,就会出现数据库和缓存数据不一致的情况。
目前有三个解决方案供参考。
延时双删流程:
1,写请求;
2,删除缓存;
3,更新DB;
4,休眠一会,删除缓存;
这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒,这样的目的是确保读请求结束,写请求可以删除读请求产生的脏数据。
这个方案还可以,但是如果第二次删除缓存失败了呢?缓存和数据库的数据还是不一致。那么是否可以加上过期时间呢?业务接受在这过期时间内的数据不一致吗?还是有其他更好的方案呢?
删除缓存重试机制流程:
1,写请求更新数据库;
2,某种原因删除缓存key失败
3,将删除缓存失败的key丢到消息队列;
4,消费消息队列的key,获取要删除的key;
5,重试删除缓存操作;
这个方案还不错,最终能够保证缓存和数据库的一致性。然而有一个缺点,会对业务代码造成大量入侵。
binlog是所有数据库表结构变更,以及表数据修改的二进制日志文件。
binlog有两个常用的使用场景:
以mysql为例:
1,可以使用阿里的canal将binlog日志采集发送到MQ消息队列;
2,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。
大啊是的大啊是的
Redis作为一个K-V型的内存数据库,它使用用一张全局的哈希表来保存所有的键值对。这张哈希表,由很多个哈希桶组成,每个哈希桶中的entry元素保存了key和value指针,其中*key指向了实际的健, *value指向了实际的值。
哈希表的查找效率很快,有点类似于java中的HashMap,它能够在O(1)时间复杂度快速找到键值对。首先通过key计算哈希值,通过哈希值找到哈希桶的位置,定位到entry,从entry中找到对应的数据。
什么是哈希冲突?
哈希冲突:通过不同的key,计算出的哈希值一样,导致落到同一个哈希桶中。
Redis为了解决哈希冲突,采用了链式哈希。链式哈希就是同一个哈希桶中的多个元素,用链表来保存,它们之间依次用指针来连接。
可能会有人有疑问,哈希冲突链上的元素只能通过指针逐一查找,当往哈希表插入更多的数据,那么发生哈希冲突的可能性也就越高,冲突链表也就越长,那查询效率不就是很低?
为了保持高效,Redis会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis默认使用两张全局哈希表,一个用于当前使用,称为主哈希表,另一个用作扩容使用,称为备用哈希表。
值得注意的是,rehash的过程不是一次性的,这样会造成redis阻塞,无法提供服务,而是采用了渐进式rehash。
可以的,Redis提供两个指令生成RDB文件,分别是save和bgsave。
布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组Hash映射函数组成,它用于检索一个元素是否在一个集合中,空间效率和查询时间比一般的算法要好得多,缺点是有一定的误识别率和删除困难。
布隆过滤器的原理是:假设我们有集合A,A中有n个元素。利用k个哈希散列函数,将A中的每个元素映射到一个长度为a位的数组B中的不同位置上,这些被映射的位置上的二进制数均设置为1。如果待检查的元素,经过这k个哈希散列函数的映射后,发现其k个位置上的二进制数全部为1,这个元素很可能属于A,反之,一定不属于集合A。
(不理解的反复读5遍)
举个栗子:
假设集合A有3个元素,{d1,d2,d3}。有一个哈希函数为Hash1。现在将集合A的每个元素都映射到一个长度为16的数组B。这个数组只存0和1,默认为0.
现在我们将d1映射过来,假设Hash1(d1)= 2,我们就把数组B中,下标为2的格子改为1。
现在将d2也映射过来,假设Hash1(d2)= 5,我们就把数组B中,下标为5的格子改为1。
现在将d3也映射过来,假设hash1(d3)= 2,我们就把数组B中,下标为2的格子改为1。
目前Hash1(d1)和Hash(d3)都等于2。因此,我们要确认一个元素dn是否在集合A中,只要算出Hash1(dn)得到的索引下标,只要是0,那么元素dn肯定不在集合A中。如果索引下标是1呢?那该元素可能是集合A的某个元素。因为,d1和d3得到的下标值都是1,还有可能是其他元素映射的,布隆过滤器是存在这个缺点的:会存在哈希碰撞的假阳性,判断存在误差。
即使存在误差,我们发现,布隆过滤器没有存储完整的数据,而是通过一系列的哈希函数找出位置,并填充二进制向量。如果数据量很大,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是较为划算的。
目前布隆过滤器已经有实现的类库了,比如Google的Guava类库,Twitter的Algebird类库,或者基于Redis自带的Bitmaps自行实现设计也是可以的。
Reids中,我们可以设置缓存key的过期时间,Redis的过期策略是指,当Reids中缓存的key过期了,Redis如何处理。
过期策略有以下三种:
Redis同时使用了惰性过期和定期过期两种过期策略。
Redis的内存淘汰机制和过期ky的删除策略不要搞混淆了。内存淘汰机制是当Redis的内存空间不够了,设置某一种策略来处理数据。
Redis4.0之后内存淘汰机制一共有8种。
根据系统的不同业务场景,选择合适的淘汰机制。
关于Redis实现的LRU和LFU算法的详细内容可以查阅这篇文章。
参考文档:https://wenku.baidu.com/view/85b703173a68011ca300a6c30c2259010202f3fa.html
在实际项目中使用Redis,肯定不会单点部署Redis服务的,因为单点部署一旦宕机,就不可用了。为了实现高可用,通常是部署多台Redis。
Redis主要有三种部署模式:
主从模式中,Redis部署来多台机器,由主节点master,负责读写操作,由从节点slave,只负责读操作。从节点的数据来自主节点,实现的原理就是主从复制机制。
主从复制包括全量复制和增量复制和两种。
一般当slave节点第一次链接master节点,会采用全量复制。
slave和master全量复制后,当master上的数据发生变更时,采用增量复制。
主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点切换为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis从2.8版本开始正式提供了哨兵模式来解决此问题。
哨兵模式,由一个或多个Sentinel组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态后,自动将下线的主节点所属的从节点升级为新的主节点。但是一个哨兵进程对Redis节点监视,就可能出现单点问题,因此,一般使用多个哨兵来监控Redis节点,并且各个哨兵之间还会进行监控。
哨兵模式的主要功能有:
哨兵模式基于主从复制模式,实现读写分离,而且可以故障自动转移,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此Cluster集群模式应用而是,它是在Redis3.0推出的,实现了分布式存储。对数据进行分片,也就是每台Redis实例存储了不同的数据,来解决在线扩容的问题,并且也提供了主从复制和故障转移的功能。但分布式存储也带了其他但问题,在此不展开描述。
关于主从复制模式,哨兵模式,Cluster集群模式的更多详细内容,查看另外的单独文章。