Redis-基础

227 阅读10分钟

image.png

Redis的数据结构

(除了SDS,其他数据结构都是集合)

①简单动态字符串SDS

属性:

buf字节数组-保存实际数据的二进制流!; len属性-保存buf数组的已用长度即字符串长度 alloc属性-buf数组的实际分配长度。 优点: ①len属性使得SDS可以以常数复杂度获取字符串长度,避免了每次都遍历字节数组。

②len字符串长度和alloc最大长度,SDS会计算可用长度并在空间不足时自动扩容,杜绝了缓冲区溢出。

③减少了修改字符串时带来的内存重分配次数。-空间预分配:在对SDS进行扩容时,Redis会为其额外分配alloc空间和1byte的空间用于存放'\0',可以减少内存重分配次数。-惰性空间释放:在SDS被缩短后,程序不会立即回收SDS缩短的字节数,而是使用alloc属性将这些字节数记录下来,用于将来的使用。避免了缩短字符串时所需的内存重分配,并且增大了alloc从而优化了SDS增长操作。

④二进制安全:C字符串无法正确识别'/0'字符从而导致二进制文件识别错误,而SDS的buf字节数组存储了二进制流,并且SDS使用len属性判断字符串的结束位置,保证了二进制安全。

②双向链表:

每个节点都有prev与next指针,可以支持顺序性访问、灵活地增删节点等功能。

③压缩列表:

压缩列表是为了节约内存而出现的。它的每个key对应于一个数组,数组中的每个元素都对应一个kv键值对。压缩列表在表头有三个字段,分别表示列表长度、列表尾偏移量、列表中的元素个数;压缩列表的表尾还有一个字段表示列表结束。

如果我们要定位第一个元素和最后一个元素,那么通过表头的三个字段可以O(1)拿到。

④哈希表:

哈希表其实就是一个元素为哈希桶的数组,每个哈希桶中以KV链表形式存放键值对数据的指针(链式哈希)。这个链式哈希也就是因为哈希冲突而形成的。但随着链式哈希越来越长导致负载因子超过阈值时,在链表上的遍历查找变得困难,所以Redis会对哈希表做渐进式rehash操作。

Redis默认有两个全局哈希表,一开始插入数据时使用哈希表1,哈希表2没有被分配空间,随着数据逐步增多,Redis开始执行渐进式rehash操作:

①给哈希表2分配更大的空间

②进行数据拷贝:Redis每处理一个请求,就从哈希表1中的第一个索引位置开始把该位置上的哈希桶的所有kv键值对拷贝到哈希表2中。索引位置随着Redis处理客户端请求而递增。

③释放哈希表1的空间。

于是,我们从哈希表1切换到哈希表2,哈希表1用于下次rehash的扩容备用。
⑤跳表 - 基于链表的二分查找: Redis的跳表由一个链表与zskiplistNode跳表组成。链表记录了当前跳表的首尾指针、最大层级、跳表长度,zskiplistNode记录了down、next、层级、score、成员变量指针(SDS)。它就是由原链表每几个节点向上抽取,且通过down指针上下层相连的一种数据结构,我们从最顶层开始搜索,根据大小区间定位到下一层的链表位置,最终定位到底层元素,这样查找一个结点需要遍历的结点个数就变少了,查找效率就提高了。跳表插入元素时,为了防止某个区间的元素过多,导致退化成链表,它会通过随机函数来选择将新节点插入到哪一层索引中,实现动态更新索引结构。 跳表中查询任意数据的时间复杂度为O(logn)。跳表还引入随机函数来确定新节点的k层索引,来避免某层节点区间下对应的节点数相差太多,导致退化成链表。

其实红黑树也能完成快速增删改查,但zset还有一个需求是range按区间查询,红黑树的效率没有跳表高,跳表可以以O(logn)的时间复杂度根据索引完成区间查找。 ⑥整数集合: 用于保存整数值的集合。它包括编码方式、元素数量这两个属性和用于保存元素的数组。

Redis的数据类型

①String

SDS

  • 当保存的数据只有整数时,String类型会把它保存为一个8字节的Long类型整数,这叫做int编码类型(int编码)
  • 当保存的数据含有字符时,String类型会把它保存为一个SDS简单动态字符串。若字符串小于等于44字节,则RedisObject中的元数据、指针、SDS被分配为一块连续的内存空间(embstr编码);若字符串大于44字节时,Redis会给SDS分配独立的空间并用指针指向SDS(raw编码)

②list

  • 压缩列表:list中所有元素的字符串长度都小于64字节 && 元素总数小于512个
  • 双向链表

③hash

  • 压缩列表:hash中的所有kv的字符串长度都小于64字节 && 键值对总数小于512个。
  • 哈希表

④set

  • 整数集合:set中的所有元素都是整数 && 元素个数小于512个。
  • 哈希表 应用场景:聚合统计(差值得到用户日增量)

⑤sorted set

  • 压缩列表:zset中的所有元素长度小于64自己 && 元素总数小于128个。
  • 跳表 应用场景:排行榜、最新列表(list具有队列特性,但是若需要分页,在分页查询的过程中又插入了数据,会导致所有数据后移,前一页的数据重复出现。而我们使用sorted set的score做分页,score不变则不会被新插入数据影响-使用zrevrangebyscore传入上一页最后一条记录的score socre为时间戳即可)

zset常量命令:zadd key [score xxx]、zrange key min max {withscores}、zcount key min max、zincrby key number xxx(将xxx分值增加number)

⑦bitmap位图:

位图是String类型的拓展类型,底层为二进制的字节数组,适用于二值状态统计,即元素取值只有0 1两种。它的本质就是用二进制0 1位代替true false。

应用场景:海量数据是否存在、是否连续签到10天(把10天的bitmap做与操作)

⑥BloomFilter布隆过滤器

布隆过滤器用于检索一个元素是否在一个大集合中。布隆过滤器基于bitmap,是bitmap的一种改进。

原理:新进元素通过K个哈希函数被映射到一个位数组中的K个点,把它们置为1。检索某个值是否存在时,我们通过相同的K个哈希函数得到K个位置,判断是否为1。这些位置如果有一个0,则一定不存在;如果全为1,也不一定存在(哈希碰撞)

缺点:误判率(元素不存在但hash全为1)删除困难(置0会影响其他元素)

应用场景(Guava实现):

  • 防止缓存击穿:收集监控数据防止重新写入、
  • 缓存宕机时:临时使用布隆过滤器作为缓存
  • WEB拦截:相同请求拦截防止被攻击、避免爬虫重复爬取URL、过滤发送垃圾邮件的账号

⑧hyperLogLog

用于统计基数的数据类型,每个HyoerLogLog空间固定且只占12KB。把元素加入HyoerLogLog,使用pfcount命令就可以直接得到基数

⑨GEO

GEO是一种基于sorted set和GeoHash编码的扩展数据类型。为了应对可以存放经纬度并能根据经纬度范围查询的场景,如果只有sorted set,它的score只能存放浮点数,不能满足需求。所以需要使用GeoHash把二维地图做划分,然后对区间编码,那金纬度落到的区间编码就是他的score,这样就可以根据相邻经纬度来进行范围查询了。

Redis是单线程的吗?Redis为什么这么快

① Redis单线程主要指的是网络IO和数据读写是由一个线程来完成的。但Redis的其他功能,如持久化、异步删除、集群数据同步等,都是由额外的多线程完成的。(补充:对于多线程系统来说,在合理的资源分配情况下,多线程处理请求可以提高吞吐量增加性能。但多线程就意味着需要面临共享资源并发访问控制问题,就带来了同步机制的额外开销,也会降低系统的可维护性。所以Redis直接采用了单线程模式)

② Redis是纯内存的数据库,无需访问磁盘从而减少了大量的时间开销。

③高效的数据结构:哈希表、跳表

④ Redis在对外提供键值存储的服务上使用单线程,避免了不必要的上下文切换、避免了共享资源并发访问控制带来的资源开销。

⑤Redis采用了IO多路复用机制:对于Redis的bind、accept、recv(从socket中读取请求)等操作,Redis通过epoll机制,让内核同时监听多个监听套接字和已连接套接字,一旦有请求到达就会交给Redis线程处理,实现了多路复用的效果。(补充:epoll提供了基于事件的回调机制,即epoll一旦监听到监听套接字上有请求到达,就会触发对应的事件,Redis会把这些事件放入一个事件队列中,并让Redis单线程对该事件队列不断处理,实现了基于事件的回调。)

Redis如何实现事务

步骤:Redis客户端使用multi命令开启事务,之后服务端接收到的读写命令会被暂存到一个命令队列中。Redis客户端使用exec命令提交事务,服务端执行命令队列中的所有命令。

对于ACID:

①Redis事务不一定会保证原子性:若命令本身有错,命令入队时Redis会记录这个错误,然后执行exec是Redis会返回事务执行事务;若命令本身没错(数据类型不匹配),Redis在exec后执行错误命令会报错,但不对正确执行的命令回滚,即不保证原子性。

②Redis事务保证一致性

③Redis事务保证隔离性,若在exec命令之前出现多客户端并发操作时,Redis会看事务是否使用了watch机制。watch机制会监控多个key的变化情况,当执行exec命令时,watch机制会先检查监控的key是否被其他客户端修改。若修改了则放弃事务执行。

④Redis事务的持久性取决于持久化机制。