一、Redis基本操作——String(原理篇)
小喵的唠叨话:最近京东图书大减价,小喵手痒了就买了本《Redis设计与实现》[1]来看看。这里权当小喵看书的笔记啦。这一系列的模式,主要是先介绍Redis的实现原理(可能很大一部分会直接照搬原作者的描述),加上小喵自己的想法,之后配合Redis官网上的各种相关的操作命令(原书上貌似没有很多的介绍命令)。
小喵的个人博客地址: http://miaoerduo.com, 随时欢迎各位的大驾。
原文链接: http://www.miaoerduo.com/redis/redis基本操作-string原理.html
排版比这里的要好看一点(小喵可是装了很多美化插件的)。
本章介绍Redis中最常用到的字符串(String)。
Redis的字符串(String)的实现
小喵之前有看到过《Redis设计与实现》的一部分章节。这是第一章的内容,小喵也是因为看了这一章的内容,才决定要买本仔细研究的。
首先,我们知道Redis是由C语言编写的,以高效和轻量著称。而C语言中的字符串是怎么实现的呢?字符数组。
比如一个简单的字符串”hello world”,其实是一个如下的字符的数组:
[‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘\0’]
最后的一个’\0’是空字符,表示字符串的结尾。
Redis由于各种原因,并没有直接使用了C语言的字符串结构,而是对其做了一些封装,得到了自己的简单动态字符串(simple dynamic string, SDS)的抽象类型。Redis中,默认以SDS作为自己的字符串表示。只有在一些字符串不可能出现变化的地方使用C字符串。
SDS的定义如下:
1
2
3
4
5
6
7
8
9
|
struct sdshdr {
// 用于记录buf数组中使用的字节的数目
// 和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[];
};
|
可以看出来,SDS的结构并不复杂。
buf是一块可用的内存空间,通常大小会大于等于需要存储的字符串的大小(大于?为什么要大于呢?读者可以思考一下)。
len表示字符串的长度,也表示buf中已经被使用的空间的大小。
free表示buf中没有被使用的空间的大小。
要注意的是,buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
那么这么封装到底有什么好处呢?我们一点一点剖析。
1,常数复杂度获取字符串长度
在C语言中的字符串只是简单的字符的数组,当使用strlen获取字符串长度的时候,C语言内部其实是直接顺序遍历数组的内容,找到对应的’\0’对应的字符,从而计算出字符串的长度。显然这个算法复杂度和字符串的长度成正比,即O(N)。而对于SDS来说,只需要访问SDS的len属性就能得到字符串的长度,复杂度为O(1)。这样,获取字符串长度的操作就不会成为Redis的瓶颈(当然len的作用不止这么简单,后面还会介绍别的)。
2,杜绝缓冲区溢出
我们知道C++里面的字符串使用了STL的string类型,我们开发者不太需要关注内存的分配和释放的过程。但是Redis是C语言编写的,并没有这么方便的数据类型。对于字符串的拼接、复制等操作,C语言开发者必须确保目标字符串的空间足够大,不然就会出现溢出的情况。
1
2
3
|
char a[10] = "hello";
strcat(a, " world");
strcpy(a, "hello world");
|
上面的三句代码,就是C语言的字符串拼接和复制的使用,但是明显出现了缓冲区溢出的问题。字符数组a的长度是10,而”hello world”字符串的长度为11,则需要12个字节的空间来存储(不要忘记了’\0’)。
然后,我们看看Redis的SDS是怎么处理字符串修改的这种情况。
当使用SDS的API对字符串进行修改的时候,API内部第一步会检测字符串的大小是否满足。如果空间已经满足要求,那么就像C语言一样操作即可。如果不满足,则拓展buf的空间,使得满足操作的需求,之后再进行操作。每次操作之后,len和free的值会做相应的修改。
这就是SDS的全部的高明之处了吗?当然不!
当API发现SDS的buf的容量不够的时候,并不是简单申请正好适合的大小,而是额外申请了一倍的空间!我们以sds的API sdscat函数为例,该函数实现了sds的拼接的功能。
下面的例子是”hello” 和” world”的拼接的过程。
图1 sdscat执行之前的sds
图2 sdscat执行之后的sds
这里的buf的容量是23(free + len + 1)。为什么要这么做呢?耐心向下看吧。
3,减少修改字符串时带来的内存重分配次数
我们之前说到,对于一个N长的字符串,C语言中底层是一个N+1长的字符数组(有一个字节存放空字符)。C字符串的长度和底层数组之间的长度存在着这样的关系,因此当进行字符串的操作而导致字符串长度发生变化的时候,需要对内存进行重新分配。
如果操作会增长字符串,那么在执行之前,就需要进行内存分配扩充底层数组的大小。如果是缩短字符串的操作,则需要释放额外的内存(这是书中的意思,但小喵觉得如果字符串缩小的话,其实并不用立刻释放内存,如果字符串是malloc出来的话,需要释放的直接free就可以,也不需要给定空间的大小,所以不会出现内存泄露。当然,也可能Redis里面是用别的方式实现,这样小喵就不懂了)。
对于一般的程序而言,如果修改字符串的操作并不是特别常出现,那么每次修改都重新分配一下内存也是可以接受的。但是Redis作为一个数据库,其读写速度,数据修改频率都被要求达到很高的效率。因此这种低效的方式并不适合Redis。
为了避免C字符串的这些弊端,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关系。也就是之前说的buf的长度为len和free之和(再加1)。数字里面可以包含未使用的空间,大小用free表示。
Redis主要通过以下两种策略来处理内存问题。
i) 空间预分配
这种方式用于处理字符串长度增加的问题。如果对字符串的修改使得字符串的长度增加,API首先会判断buf的空间大小是否满足,如果满足则直接操作,如果不满足,则进行如下操作:
如果对SDS进行修改之后的,SDS的长度(即len的值)小于1MB。程序将额外分配和len一样大小的未使用空间。以上面的”hello” + ” world”的操作为例。在这个例子中”hello”的len是5(不考虑’\0′),修改之后的字符串”hello world”长度为11,那么新的SDS的buf的容量就是11*2+1。其中len和free都是11,多余的1字节用来存储’\0’。
如果对SDS修改之后的长度大于1MB,那么程序会分配1MB的未使用空间。比如原数据是5MB,修改之后需要6MB的空间,进行修改的操作后,buf的实际空间应该是7MB,其中len为6MB,free为1MB。
那么这些未使用空间能够做什么呢?为什么根据SDS的修改会的大小会有两种不同的分配原则呢?
小喵是这么认为的,如果数据被更改,则说明这个数据很可能会被再次更改,如果能够提前分配多余的空间,那么下一次变化的时候很可能就不需要再次分配空间了。如果数据比较小(<1MB)的时候,可以分配等大的未使用空间。但是如果数据已经很大的时候(>1MB),再分配同等大小的内存会显得十分浪费,毕竟不能确保这个字符串一定会被再次修改,所以只额外分配1MB的空间。
通过这种策略,SDS可以做到N次修改,最多进行N次内存分配。而C字符串在N次修改则一定要进行N次内存分配。一个是至多N次,一个是一定N次。用小喵的脑袋想,也觉得SDS这个策略简单、粗暴、高效。
ii) 惰性空间释放
当执行字符串长度缩短的操作的时候,SDS并不直接重新分配多出来的字节,而是修改len和free的值(len相应减小,free相应增大,buf的空间大小不变化)。通过惰性空间释放,可以很好的避免缩短字符串需要的内存重分配的情况。而且多余的空间也可以为将来可能有的字符串增长的操作做优化。
当然,SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。
4,二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且字符串除了末尾之外不能出现空字符,否则会被程序认为是字符串的结尾。这就使得C字符串只能存储文本数据,而不能保存图像,音频等二进制数据。(这里,小喵的观点是不同的,小喵本人是做图像的,opencv等的库,都是使用unsigned char*来存储图像的数据。我们完全可以把字符数组看成一堆内存,存放任何数据都可以)
使用SDS就不需要依赖控制符,而是用len来指定存储数据的大小。同时所有的SDS操作的API都是二进制安全的(binary-safe),所有的SDS API都会以处理二进制的方式来处理SDS的buf的数据。程序不会对buf的数据做任何限制、过滤或假设,数据写入的时候是什么,读取的时候依然不变。
这也是我们将SDS的buf属性程序字节数组的原因,Redis不是使用这个数组来保存字符,而是储存一系列二进制数据。
5,兼容部分C字符串函数
由于SDS的buf的定义和C字符串完全相同,因此很多的C字符串的操作都是适用于SDS->buf的。比如当buf里面存的是文本字符串的时候,printf函数,也完全可以试用。这样,Redis就不需要为所有的字符串的处理编写自己的函数,大多数通过调用C语言的函数就可以。
总结
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有库中的函数 | 可以使用一部分库的函数 |
以上则是Redis的string结构的原理部分。下一章我们会介绍一些string操作的redis命令。
转载请注明出处。
参考:
一、Redis基本操作——String(原理篇)的更多相关文章
- 二、Redis基本操作——String(实战篇)
小喵万万没想到,上一篇博客,居然已经被阅读600次了!!!让小喵感觉压力颇大.万一有写错的地方,岂不是会误导很多筒子们.所以,恳请大家,如果看到小喵的博客有什么不对的地方,请尽快指正!谢谢! 小喵的唠 ...
- Redis基本操作-string
Redis的5种数据结构:string.list.hash.set和zset; Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数 ...
- 支撑微博亿级社交平台,小白也能玩转Redis集群(原理篇)
Redis作为一款性能优异的内存数据库,支撑着微博亿级社交平台,也成为很多互联网公司的标配.这里将以Redis Cluster集群为核心,基于最新的Redis5版本,从原理再到实战,玩转Redis集群 ...
- 支撑微博亿级社交平台,小白也能玩转Redis集群(实战篇)
上篇文章<支撑微博亿级社交平台,小白也能玩转Redis集群(原理篇)>介绍了Redis集群相关原理,这篇文章将介绍Redis Cluster集群的搭建.配置,运维.扩容等具体操作 集群搭建 ...
- Redis原理篇
Redis原理篇 1.发布 订阅模式 1.1列表 的局限 前面我们说通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消费者需要不停地调用 lpop 查看 List 中是 ...
- 三、Redis基本操作——List
小喵的唠叨话:前面我们介绍了Redis的string的数据结构的原理和操作.当时我们提到Redis的键值对不仅仅是字符串.而这次我们就要介绍Redis的第二个数据结构了,List(链表).由于List ...
- Redis基本操作——List
Redis基本操作——List(原理篇) 学习过数据结构的同学,一定对链表(Linked List)十分的熟悉.相信我们自己也曾经使用过这种数据结构. 链表分为很多种:单向链表,双向链表,循环链表,块 ...
- 【进阶之路】Redis基础知识两篇就满足(一)
导言 大家好,我是南橘,一名练习时常两年半的java练习生,这是我在博客园的第一篇文章,当然,都是要从别处搬运过来的,不过以后新的文章也会在博客园同步发布,希望大家能多多支持^_^ 这篇文章的出现,首 ...
- Redis基本操作-20150608
Redis基本操作-20150608 [http://my.oschina.net/u/241255/blog/206991] Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存 ...
随机推荐
- HTML URL地址解析
通过JavaScript的location对象,可获取URL中的协议.主机名.端口.锚点.查询参数等信息. 示例 URL:http://www.akmsg.com/WebDemo/URLParsing ...
- [Spring]IoC容器之进击的注解
先啰嗦两句: 第一次在博客园使用markdown编辑,感觉渲染样式差强人意,还是github的样式比较顺眼. 概述 Spring2.5 引入了注解. 于是,一个问题产生了:使用注解方式注入 JavaB ...
- ArcGIS 10.0紧凑型切片读写方法
首先介绍一下ArcGIS10.0的缓存机制: 切片方案 切片方案包括缓存的比例级别.切片尺寸和切片原点.这些属性定义缓存边界的存在位置,在某些客户端中叠加缓存时匹配这些属性十分重要.图像格式和抗锯齿等 ...
- Tomcat shutdown执行后无法退出进程问题排查及解决
问题定位及排查 上周无意中调试程序在Linux上ps -ef|grep tomcat发现有许多tomcat的进程,当时因为没有影响系统运行就没当回事.而且我内心总觉得这可能是tomcat像nginx一 ...
- 【原】AFNetworking源码阅读(二)
[原]AFNetworking源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中我们在iOS Example代码中提到了AFHTTPSessionMa ...
- await and async
Most people have already heard about the new “async” and “await” functionality coming in Visual Stud ...
- 代码的坏味道(16)——纯稚的数据类(Data Class)
坏味道--纯稚的数据类(Data Class) 特征 纯稚的数据类(Data Class) 指的是只包含字段和访问它们的getter和setter函数的类.这些仅仅是供其他类使用的数据容器.这些类不包 ...
- 从源码浅析MVC的MvcRouteHandler、MvcHandler和MvcHttpHandler
熟悉WebForm开发的朋友一定都知道,Page类必须实现一个接口,就是IHttpHandler.HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET ...
- Oracle:一个用户操作多个表空间中表的问题(转)
原文地址:http://blog.csdn.net/shmiloy001/article/details/6287317 首先,授权给指定用户. 一个用户的默认表空间只能有一个,但是你可以试下用下面的 ...
- VisualStudio 2015 开启IIS Express可以调试X64项目
现在项目开发时总有时需要在X64下开发,这样我们就需要IIS Express中调试.不要总是放在IIS中,在Attach这样好慢. 如果不设置直接调试X64的程序,我们有可能会受到以下类似的错误 ...