redis

内存数据库

针对key-value组合采用全局hash索引

hash冲突问题:就是链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接

渐进式rehash:Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,采用两个hash表

默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;

把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;

释放哈希表 1 的空间。

支持丰富的value数据结构,String(字符串),List(列表), Hash(哈希),Sorted Set (有序集合)Set(集合);

及数据的底层实现单来说,底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组 集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈希表、压缩列表和跳表。

 各个结构时间复杂度

压缩列表:类似一个数组压,表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

复杂度:查找第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是O(1) 查找其他元素时,只能逐个查找,此时的复杂度就是 O(N) 了。

 跳表 : 有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:当数据量很大时,跳表的查找复杂度就是 O(logN)。

整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

redis为什么是单线程的性能还这么好?

Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步

其实是由额外的线程执行的。

一方面,Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。另一方面,就是 Redis 采用了多路复用机制

基于多路复用的高性能 I/O 模型Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。

Redis单线程处理IO请求性能瓶颈主要包括2个方面:

1、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:

a、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;

b、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;

c、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;

d、淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;

e、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;

f、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;

2、并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。 针对问题1,一方面需要业务人员去规避,一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。

针对问题2,Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

持久化技术

AOF(Append Only File)写后日志,与mysql的写前日志(Write Ahead Log, WAL)不同, aof先在内存执行命令后写日志(及减少语法检查消耗,又能避免出现记录错误命令, 不阻塞当前写操作)

 我把重写的过程总结为“一个拷贝,两处日志”。

 rdb(快照文件)

 Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

主从保证高可用

aof, rdb保证了数据少丢失,而主从架构保证了高可用,从节点使用replicaof (之前是用slaveof)命令同步主节点的数据

主从同步过程过程中,主节点维护两个buff, 一个记录主节点写指针的repl_backlog_buffer和给各个从节点或者客户端(replication buffer 批量刷到socket缓存,输出到网卡)输出的replication buffer 

哨兵机制(sentinel)

哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。(负责主节点宕机后,自动切换主节点,故障转移)

监控:哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态

选主打分:哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库,然后,依次按照优先级、复制进度、ID 号大小再对剩余的从库进行打分,只要有得分最高的从库出现,就把它选为新主库。

底层数据结构

Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,

dictEntry:dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节

RedisObject: 一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针

 info memory 查看内存使用情况

127.0.0.1:6379> info memory
# Memory
used_memory:1039120
127.0.0.1:6379> hset 1101000 060 3302000080
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039136

数据结构使用总结: 

ziplist, 整数数组:使用数组这个紧俏的内存结构,更省内存, 但是数据不易过大, 因为一旦某一数据的修改,大小的改动需要级联改动挪动后面entry的内存布局,性能较差

hash,调表:数据布局较分散,内存占用较大, 但是查询复杂度较低。

string:value为单值非集合,value最好不要太小,因为需要消耗大量的dictentry,redisobject,sds等元数据内存占用

ziplist:为了能充分使用压缩列表的精简内存布局,我们一般要控制保存在 Hash 集合中的元素个数。

这两个阈值分别对应以下两个配置项:

hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。

hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。

如果我们往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。

学习了集合类型的 4 种统计模式,分别是聚合统计、排序统计、二值状态统计和基数统计。为了方便你掌握,我把 Set、Sorted Set、Hash、List、Bitmap、HyperLogLog 的支持情况和优缺点汇总在了下面的表格里,希望你把这张表格保存下来,时不时地复习一下。

可以看到,Set 和 Sorted Set 都支持多种聚合统计,不过,对于差集计算来说,只有 Set 支持。Bitmap 也能做多个 Bitmap 间的聚合计算,包括与、或和异或操作。当需要进行排序统计时,List 中的元素虽然有序,但是一旦有新元素插入,原来的元素在 List 中的位置就会移动,那么,按位置读取的排序结果可能就不准确了。而 Sorted Set 本身是按照集合元素的权重排序,可以准确地按序获取结果,所以建议你优先使用它。如果我们记录的数据只有 0 和 1 两个值的状态,Bitmap 会是一个很好的选择,这主要归功于 Bitmap 对于一个数据只用 1 个 bit 记录,可以节省内存。

redis使用规范

原子性 

redis是用 单命令(incr, decr等)和lua脚本保证原子性

单实例分布式锁:使用单命令set加索, lua脚本释放自己加的锁

使用类型 setnx不存在即设置的命令 set实现了setnx的同时,可以设置EX(秒),PX(毫秒)等超时机制

1.set单命令设置锁
SET key value [EX seconds | PX milliseconds]  [NX]


2.编写xxx.lua脚本实现 原子删除操作
//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

调用lua脚本实现删除操作
redis-cli  --eval  unlock.script lock_key , unique_value 

多实例分布式锁(非主从,多master): redis的开发者给出了redislock来解决

我们来具体看下 Redlock 算法的执行步骤。Redlock 算法的实现需要有 N 个独立的 Redis 实例。接下来,我们可以分成 3 步来完成加锁操作。

第一步是,客户端获取当前时间。

第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作。

第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;

条件二:客户端获取锁的总耗时没有超过锁的有效时间。

如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作

总结

简单总结,基于 Redis 使用分布锁的注意点:

1、使用 SET $lock_key $unique_val EX $second NX 命令保证加锁原子性,并为锁设置过期时间 2、锁的过期时间要提前评估好,要大于操作共享资源的时间

3、每个线程加锁时设置随机值,释放锁时判断是否和加锁设置的值一致,防止自己的锁被别人释放

4、释放锁时使用 Lua 脚本,保证操作的原子性

5、基于多个节点的 Redlock,加锁时超过半数节点操作成功,并且获取锁的耗时没有超过锁的有效时间才算加锁成功

6、Redlock 释放锁时,要对所有节点释放(即使某个节点加锁失败了),因为加锁时可能发生服务端加锁成功,由于网络问题,给客户端回复网络包失败的情况,所以需要把所有节点可能存的锁都释放掉

7、使用 Redlock 时要避免机器时钟发生跳跃,需要运维来保证,对运维有一定要求,否则可能会导致 Redlock 失效。例如共 3 个节点,线程 A 操作 2 个节点加锁成功,但其中 1 个节点机器时钟发生跳跃,锁提前过期,线程 B 正好在另外 2 个节点也加锁成功,此时 Redlock 相当于失效了(Redis 作者和分布式系统专家争论的重要点就在这)

8、如果为了效率,使用基于单个 Redis 节点的分布式锁即可,此方案缺点是允许锁偶尔失效,优点是简单效率高

9、如果是为了正确性,业务对于结果要求非常严格,建议使用 Redlock,但缺点是使用比较重,部署成本高

以及 各位大佬在Redis 分布式锁在各种异常情况是否安全的分析,收益会非常大:http://zhangtielei.com/posts/blog-redlock-reasoning.html。

redis 脑裂导致数据丢失

脑裂:主节点阻塞假故障, 或者网络问题导致,被哨兵判下线,重新选主,导致短时两个主节点,原主节同时在处理着客户端新的写入,原主节点被同步为从节点后,清空本地数据,导致老主节点上新写入的数据丢失了

为了应对脑裂,你可以在主从集群部署时,通过合理地配置参数 min-slaves-to-writemin-slaves-max-lag,来预防脑裂的发生。

min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;

min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

脑裂产生问题的本质原因是,Redis 主从集群内部没有通过共识算法,来维护多个节点数据的强一致性。它不像 Zookeeper 那样,每次写请求必须大多数节点写成功后才认为成功。当脑裂发生时,Zookeeper 主节点被孤立,此时无法写入大多数节点,写请求会直接返回失败,因此它可以保证集群数据的一致性。

使用中需要注意的坑

redis采用两种删除策略: 

1.惰性删除(当再次需要对这个数据进行独写时,会删除该数据,不返回或者返回为null数据)

2.定时删除, 定时小量删除一些长时间没有被访问的数据,减小内存垃圾

两种删除策略,即减少了删除操作对其他请求命令的影响,也及时清理内存垃圾大量占用

使用redis做秒杀场景 

减库存3步骤保证原子性,查询库存,判断库存是否够用,减库

1.把库存信息保存在redis中,使用lua脚本是实现原子性(性能较高,支持高并发)

2.使用分布式锁,每个客户端都先去redis中获取锁,再应用程序中实现临界区代码,最后释放锁

这样一来,大量的秒杀请求就会在争夺分布式锁时被过滤掉。而且,库存查验和扣减也不用使用原子操作了

数据倾斜

数据量倾斜:bigkey,slot分布不均, hashtag

数据访问量倾斜:几个热点数据访问,如果是热点只读数据,如果热点数据是有读有写的话,就不适合采用多副本方法了,因为要保证多副本间的数据一致性,会带来额外的开销。

 

redis的事务

事务的 ACID 属性是我们使用事务进行正确操作的基本要求。通过这节课的分析,我们了解到了,Redis 的事务机制可以保证一致性和隔离性,但是无法保证持久性。不过,因为 Redis 本身是内存数据库,持久性并不是一个必须的属性,我们更加关注的还是原子性、一致性和隔离性这三个属性。

原子性的情况比较复杂,只有当事务中使用的命令语法有误时,原子性得不到保证,在其它情况下,事务都可以原子性执行。

数据切片集群Redis Cluster (redis3.0后推出)

解决数据数据量大,fork rdb/aof重写进程带来的阻塞,减轻但是离访问压力等问题

在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。

Redis Cluster 方案提供了一种重定向机制  

和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以,在上图中,如果客户端再次请求 Slot 2 中的数据,它还是会给实例 2 发送请求。这也就是说,ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例。

客户端MOVED重定向命令

客户端ASK重定向命令

Redis 6.0的新特性:多线程、客户端缓存与安全

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值