首页 > 编程知识 正文

多个线程访问redis,redis单线程是针对单个节点

时间:2023-05-05 11:20:55 阅读:130350 作者:1853

redis摘要redis是一个基于内存的无SQL数据库。 因为数据存在于内存中,主要用于缓存。

redis的保存方法是Key-Value。

redis线程模型redis采用单线程模型,为什么采用单线程呢?

因为redis是基于内存的数据库,所以它只是操作内存,不涉及IO,是一项CPU计算密集型任务。 如果不小心部署多线程,则线程之间的切换只会浪费资源,单线程的执行效率较差。

此外,单线程不会同时出现问题,保证了线程安全问题。

单线程如何同时处理多个连接?

redis采用的epoll,也就是多路IO复用。 关于epoll的详细情况,可以在我以前写的文章linux中看到TCP连接的详细情况

在这里简单说明一下。1、epoll的做法是创建event_poll对象。

event_poll对象维护一个epitem就绪列表,其中记录了相应的套接字对象指针和event_poll对象指针。

然后,可以创建新线程并调用epoll_wait )方法来检索相应的数据。 如果event_poll就绪列表不为空,则可以通过检索数据并检索相应的套接字对象来读取数据。

2、Linux上的每个连接都对应一个套接字对象,该套接字具有用于存储包的接收队列。 套接字在epoll模式下同时维护包含event_poll_callback ()和epitem信息的等待队列。 当内核将数据包放入套接字的接收队列时,相应的event_poll_callback ()和epitem将作为参数传递。

33558 www.Sina.com/event_poll _ callback ) )首先将epitem放入相应的event _ poll对象的就绪队列中,然后启动线程读取数据。

在一个准备列表中,一个线程可以读取多个套接字信息。

redis5种数据类型redis可以容纳5种数据类型

3、string提供了两种存储方法:

第一,如果string支持数字,则以int方式保存

第二个是按字符数组存储的

struct sdshdr{ int len; /*字符串长度*/int free; /*未使用字节长度*/char buf[]; /*存储字符串的字节数组*/} 1、stringsdso(1)时间获取长度http://www.Sina.com/预分配空间。 如果string大小小于1M,则预先分配两倍的可用空间;如果大小大于或等于1M,则预先分配1M的区域1、懒惰删除。 如果缩小字符串,则不会回收对应的空间,只会增加对应的自由。 等到后续的append再继续使用

Redis规定字符串的大小不能超过512MB

列表中列出了2、list,支持两种实现方式。3、

如果列表长度小于512,且列表中的所有元素大小小于64字节,则使用ziplist存储。

从名称中可以看出,ziplist的目的是减少内存使用量。

ziplist使用数组而不是链表。

链表的好处是不要求存储空间的连续性,但缺点是需要指针来存储相应的后续节点信息。 在64位操作系统上,存储一个指针需要8个字节。

如果list中的元素小于8个字节,则所有指针都将高于数据占用空间,这可能会稍微损害到它的真实性。

因此,ziplist所使用的数组由于数组是地址连续的,所以可以通过加上直接对应的偏移来得到对应的地址。

ziplist的配置如上图所示,ziplist使用严格的代码结构,主要分为以下几个部分:2、list

1、ziplist

ziplist的字节数,占用4字节

1、头部

ziplist末尾的偏移offset占用4个字节

zlbytes

ziplist的元素数。 占用2个字节

zltail

实际保存的要素、组织结构如下zllen

上一个条目的长度。 用于从后向前遍历list。

在动态长度编码中,如果当前一个条目的长度小于254字节,则以1字节记录大小,如果当前一个条目的长度超过254字节,则以5字节表示

大小。

怎么实现的动态编码呢?
首先观察第一个字节,如果第一个字节是0xFE的话,就说明是5个字节编码,后面的4个字节存储的是长度。否则就是按照1个字节编码,存储的就是对应的长度。

encoding
encoding用来表示content的存储数据类型和content的长度信息,同样采取动态编码

从上表可以看出,将前几位比特分类,对应着不同的存储类型和content长度编码。

content
content就是存储的实际数据

3、zlend
代表列表的末尾,占用一个字节,恒为0xFF

2、linkedlist
当list的长度大于512或者list中元素大于64字节的时候,就采用linkedlist来存放,因为数据太大了,不太好提供这么大的连续内存来存放数据了。

linkedlist就是一个双向链表

3、set

set对应的是元素不重复的集合,也有两种存储方式
1、intset

typedef struct intset { // 编码方式 uint32_t encoding; // 集合包含的元素个数 uint32_t length; // 保存元素的数组 int8_t contents[];

intset适用于元素为整数,并且set中元素个数小于512的情况,还是采用数组的方式来存储,使用连续内存,减少内存使用。

intset中是按照元素的大小顺序来排列。

encoding
intset采用变长编码的方式来存储,encoding分为int_16,int_32,int_64三种,分别对应16位、32位、64位 能够表示的整数范围。

当整数的范围在int_16的时候,就将整数按照16位来存储,减少内存的使用。

length
length为set中存储的元素个数。

contents
contents存储的是真正的数据,按数组方式存在。

插入元素
如果插入的元素大于encoding表示范围,就会引发升级。首先会通过元素个数和encoding的存储大小计算出新元素的对应位置,这个位置一定是在尾部,因为intset中是按照元素大小排列的。然后从后往前的将前面的数据一个一个的升级。

如果插入的元素在encoding的表示范围,就按照二分法找到对应的插入位置,将在插入位置之后的元素往后移动,最后把新元素插入到对应的位置。

dictht

typedf struct dict{ dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和value能够存储 void *private;//私有数据 dictht ht[2];//两张hash表 int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1 unsigned long iterators; //正在迭代的迭代器数量}dict;typedef struct dictht{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 //总是等于 size-1 unsigned long sizemask; //该哈希表已有节点的数量 unsigned long used; }dictht;typedf struct dictEntry{ void *key;//键 union{ void val; unit64_t u64; int64_t s64; double d; }v;//值 struct dictEntry *next;//指向下一个节点的指针}dictEntry;


通过代码和图就可以看出,dictht是一个简化的hashmap,通过拉链来解决哈希冲突。

dict中含有两个dictht,ht[0]用于存放数据,ht[1]用于扩容和收缩。

扩容详情
以下两种情况会进行扩容。
首先说下 负载因子 = 元素个数(ht.used) / 哈希表数组大小
1、 如果没有进行持久化,负载因子 > 1
2、 如果正在持久化,负载因子 > 5

因为当持久化的时候,会往磁盘中写入对应的数据,尽量要避免扩容,所以负载因子会变成5。

1、 首先将哈希表的数组大小增加到 第一个 大于等于 ht.used * 2的 2n。比如当前ht.used = 100,那么新的哈希表大小就为 256。

2、 对ht[1]分配对应的哈希表空间,不会直接将ht[0]的数据重新hash然后搬运到ht[1]上,而是采用渐近扩容

渐近扩容

如果哈希表中存在很多数量的键值对,一次性的将键值对搬运到ht[1]的话,会占用redis的大量时间,造成用户服务变慢。所以采用渐近扩容。

ht中维护了一个rehash变量,在渐近扩容的时候,会将rehashindex = 0,当操作了一次ht[0]中的元素的时候,会将rehashindex ++,当rehashindex = ht[0].used的时候,就说明渐近扩容结束了,将rehash = -1,代表扩容结束。

增加:
首先查看ht[0]是否存在对应的元素,如果存在,就将其重新hash加入到ht[1]中,并将ht[0]中的元素删除。rehashindex+1

如果不存在,就将其加入到ht[1]中。

查询:
首先查询ht[1],如果ht[1]中存在,就直接返回,如果不存在就查询ht[0],将其重新hash加入到ht[1]中,并将其从ht[1]中删除。rehashindex+1

删除:
查询ht[0],如果ht[0]中存在,就将其删除,rehashindex+1.

更新:
查询ht[1],如果存在,直接更新。

如果不存在,就查询ht[0],如果存在,就将新值插入到ht[1]中,并且将旧值从ht[0]中删除,rehashindex+1.

收缩详情

当负载因子小于 0.1的时候,会触发收缩

ht[1]的大小变为第一个大于等于 ht[0].used 的 2n

然后继续开始渐近收缩,和渐近扩容一样。

当redis线程空闲的时候,也会帮助扩容或者收缩。

4、hash

hash是用来存储字典的,也有两种方式存储
1、ziplist
当key-value的大小小于64字节并且hash中元素个数小于512的时候,会使用ziplist来存储


ziplist的话,就需要遍历列表来查找对应的key和value了,所以ziplist适用于那种小规模数据。

2、dictht
当hash中的元素比较多的时候,申请比较大的连续内存也比较困难,就开始通过哈希表存储,具体原理和set中对应的hash一致。

5、zset

zset存储的是有序的set,也有两种存储格式:
1、ziplist
利用数组有序的排列元素,当元素个数小于128,并且元素小于64字节的时候采用ziplist

2、skiplist
skiplist叫做跳表,通过对一个底层链表建立多个上层抽象链表建立对应的目录来加快查找速度。

skiplist的具体实现原理我们单独开一篇文章说明。

skiplist是一个概率型结构,通过概率来决定新插入的节点插入到哪一层。

为什么不用红黑树或者B+树?

1、 红黑树只支持固定key的查询,不支持区间查询。
2、 相较于B+树,跳表的实现更加简单,并且跳表可以通过降低概率来减少索引项,减少内存的占用。

skiplist作者的原话在这里:

1)、 They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

2、 A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

3、 They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

使用场景 类型使用场景string计数器hash存储对象,key-value代表着对象属性和对应的值list列表:消息列表、粉丝列表等setset是去重的,可以用来求交集、并集、差集。可以用来快速统计共同关注、可能认识的人、朋友圈设计等zset有序的set。用来处理排行榜问题

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。