Redis学习笔记
2019-04-06 Programming面试中被问到有没有关注过一些热门的开源项目,比如nginx和Redis,对它们的底层实现有什么了解。
很尴尬,自己只知道一些很肤浅的东西,于是就尝试去了解更多。
肤浅的东西
nginx是一个用C编写的Web服务器,主要特点是高性能和支持高并发。
之前搜索反向代理时找到的Apache相关内容比较少,大部分都是使用的nginx,所以觉得它在反向代理、负载均衡方面会用的比较多。
自己尝试搭建直播服务器时看到过很多nginx-rtmp-module的内容,对nginx添加模块时需要自己重新编译有一点印象。
Redis是一个用C编写的非关系型数据库,主要特点是高性能和基于内存。
MySQL像是把数据存到一张表里,每一列是一个属性,每一行是一项,而Redis则像是把数据存储到一个变量里,key是变量的名称,value是变量的值。
尝试了解更多
先在实验楼里跟着Redis基础教程做了一遍,对Redis有了一个初步的了解。然后准备按照给自己提问题 => 解决自己的问题
的步骤循序渐进地了解更深层次的知识,所以本篇学习笔记就是记录下自问自答的过程。
此时Redis官网稳定版本为5.0.4,主要参考资料为官方文档和机械工业出版社的《Redis设计与实现》,查看的源码直接clone自GitHub上的unstable分支。
-
Redis如何使用?
Redis分为服务端和客户端,在我的电脑上安装时是同时安装的。安装后使用redis-server
命令启动服务端,再使用redis-cli
命令打开客户端,就可以在客户端中使用Redis命令对数据库进行操作了。
不使用redis-cli
这个客户端的话,常用的编程语言也都包含Redis支持。 -
Redis有什么特点?
高性能,value支持多种数据类型,原子操作,主从复制,事务处理,持久化,可限定key生存时间。 -
什么是主从复制?
Redis可以使用多台服务器分担压力,如主服务器Master负责写入,从服务器Slave负责读取,一个Master可以伸展出多个Slave,一个Slave也可以伸展出其它Slave。
Slave会不断从自己的Master同步,但可能会有一定延时,不适合要求低延迟的场景。看到的一个使用场景是在Slave上执行集合运算、排序等操作,可以减轻Master的压力。
Redis从2.8版本开始使用新版复制功能,比之前的旧版复制功能在断线后重复制
上性能提高了很多。 -
是不是只能Master负责写入,Slave负责读取?
Master默认情况下读写应该都是可以的,而Slave默认是只读模式。可以关闭Slave的只读模式,但对其的写入不会被同步到Master,也不会被复制到它伸展出的Slave,而且在从Master重新复制时或者重启该Slave时还会丢失。 - 什么是事务处理?
事务是指Redis将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,事务执行期间不会被其它操作打断。
其它数据库中也有事务处理。通常数据库事务具有ACID性质,即:- 原子性(Atomicity):事务中的多个操作是一个整体,要么全部执行,要么一个都不执行。
- 一致性(Consistency):事务执行前后的数据库都应是一致的,一致指数据库中的数据满足完整性约束。数据库的完整性则是指数据具有正确性和相容性,例如:学生的学号必须唯一,性别只能是男或女,所在专业必须是学校已开设的专业。
- 隔离性(Isolation):多个事务并发执行时,各个事务之间不会相互影响。
- 持久性(Durability):执行完毕的事务对数据库的修改应该是持久性的。
用维基百科上的例子来说,某人想要在商店购买100元商品,那么就需要该人账户减少100元,商店账户增加100元,此时可以使用事务确保两条操作都执行或者都不执行。
-
Redis事务执行过程中某个命令出错,整个事务也会继续执行,直至所有命令执行完毕,这与事务的原子性矛盾吗?
原子性是指事务中的多个操作要么全部执行,要么一个都不执行。当某个操作指令存在语法错误时,就会一个都不执行;而执行过程中出现错误,则需要根据原子性的要求来进行判断。
如果原子性只关注事务中命令是否全部执行,不关注执行的结果,那么就不矛盾。如果原子性把执行过程中出现的错误当作未执行,那么Redis的事务就不满足原子性。
看了一些相关介绍,觉得与其讨论Redis的事务满不满足原子性的概念,不如记住Redis事务的特点,用到的时候根据它的特点对程序做出相应的修改和完善。 - 其它的数据库事务在某个操作执行失败时可以进行回滚,回复到事务执行前的状态,Redis为什么不支持回滚?
官方文档中做出了解释Why Redis does not support roll backs?,按我的理解原因主要是以下两点:- Redis和其它的关系型数据库相比结构更为简单,不会有那么多的属性约束,所以它的事务中某个指令操作失败主要分为两种情况:入队失败和数据类型不匹配。
入队失败一般是命令语法错误,它会直接导致整个事务不被执行;数据类型不匹配就是把指令用在了错误的数据类型上。这两种错误都是由编程错误造成的,应该在开发过程中就被发现并修复,所以生产环境下应当不需要事务回滚。 - 不需要支持回滚就不需要事务日志一类的东西,所以Redis的性能可以得到一定的保证。
(Redis 2.6.5之前的版本,某个命令发生入队错误后事务一样可以被执行,但只会执行正确入队的命令)
- Redis和其它的关系型数据库相比结构更为简单,不会有那么多的属性约束,所以它的事务中某个指令操作失败主要分为两种情况:入队失败和数据类型不匹配。
- 为什么要使用Redis?
这是刚看完Redis基本用法时想到的问题,为什么不直接在程序中使用自带的数据结构?也是运行在内存中,速度可能比Redis还要快,也可以使用序列化进行存储与恢复。原因有很多,例如:- 一部分数据可能是多个进程共用,甚至是不同机器上的进程,每个进程都存一份空间利用率太低。
- Redis常用做缓存服务器,用来加速对数据库的访问。程序自己存一份,相当于是有了本地缓存,再加上Redis做缓存服务器,可以进一步降低对数据库的压力。
- Redis中提供了众多高级功能,如主从复制,事务处理,限定key的过期时间等,不适合在自己的程序中再实现一遍。
- 数据与程序分离开,方便了对数据进行分布式处理,突破单台机器的性能限制。
- 为什么Redis说自己的Strings是二进制安全的?二进制安全是什么意思?
这里的二进制安全应该是指Strings数据的存取与使用是安全的。
举个例子,C语言中char数组以\0
作为数组的结尾,所以在使用字符串函数如strcat(des, src)
时,如果目标字符串中间存在\0
,那么\0
就会被当作它的结尾,后面的部分就会被源字符串覆盖,因此C中的字符数组不是二进制安全的。
而根据《Redis设计与实现》,Redis底层字符串的结构为:struct sdshdr { int len; // buf数组中已使用字节的数量,等于字符串的长度 int free; // buf数组中未使用字节的数量 char buf[]; // 保存字符串的字符数组 }
不需要担心字符串中存在特殊字符,任意字符都会被当作普通的字节,因此它是二进制安全的,可以放心地使用Redis的Strings存储图片文件的二进制数据,存储对象序列化的结果等。
- 为什么string可以加减?它的string是以什么方式存储的?
加减指令可以根据README.md在src/server.h
中找到void incrCommand(client *c);
,其实现在src/t_string.c
中,调用的是void incrDecrCommand(client *c, long long incr)
。
该函数执行过程中先查找到原键值对,取得其oldvalue,然后加减得到新value,根据原键值对的类型,确定直接修改原值或者覆盖原对象或者新建一个对象。
Redis中的键和值都是作为对象来存储的,每个对象都是这样的结构:typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) */ int refcount; void *ptr; } robj;
变量声明中的冒号是位段,可以起到压缩存储空间的作用。type的类型有:
- OBJ_STRING 0 /* String object. */
- OBJ_LIST 1 /* List object. */
- OBJ_SET 2 /* Set object. */
- OBJ_ZSET 3 /* Sorted set object. */
- OBJ_HASH 4 /* Hash object. */
数据库中的键总是一个字符串对象,值可以是任意一种对象。encoding指明了ptr指针指向的实际对象的类型,可能的取值如下:
- OBJ_ENCODING_RAW 0 /* Raw representation */
- OBJ_ENCODING_INT 1 /* Encoded as integer */
- OBJ_ENCODING_HT 2 /* Encoded as hash table */
- OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
- OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
- OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
- OBJ_ENCODING_INTSET 6 /* Encoded as intset */
- OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
- OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
- OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
- OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
比《Redis设计与实现》上介绍的多出了几种,应该是后面加入的新内容。
如果encoding是OBJ_ENCODING_INT
,那么ptr指针指向的就是一个long类型的整数,取得oldvalue或者修改value都是对long类型的整数进行操作。127.0.0.1:6379> set name nian OK 127.0.0.1:6379> type name string 127.0.0.1:6379> object encoding name "embstr" 127.0.0.1:6379> set age 21 OK 127.0.0.1:6379> type age string 127.0.0.1:6379> object encoding age "int" 127.0.0.1:6379> _
从以上操作可以看出,字符串和数字虽然type都是
OBJ_STRING
,但encoding不同,也就是说底层的实现是不同的,字符串通常是动态字符串SDS,而数字则是长整型long。 -
为什么不独立出integer类型?难道不会提升Redis的性能吗?
根据Redis对象结构的设计,如果要添加一种integer,也就是type里需要多加一种,而encoding不需要改变,实际的读写、存储方式和现在的并没有太大区别,void incrDecrCommand(client *c, long long incr)
函数整体流程并不会简化,所以没有必要独立出integer类型,那样并不能提升Redis的性能。 -
官方文档中无序集合Set的添加删除和测试是否存在时间复杂度都是写的O(1),那么是最优情况下是O(1)还是严格的O(1)呢?
直接阅读源码可以看到,SADD
中主要的两个函数robj *lookupKeyWrite(redisDb *db, robj *key)
用于查找要操作的Set,int setTypeAdd(robj *subject, sds value)
用于向Set内添加一个值,接下来仔细看看它们的实现。
robj *lookupKeyWrite(redisDb *db, robj *key)
中,先检查key是否过期,然后返回了robj *lookupKey(redisDb *db, robj *key, int flags)
的结果,而lookupKey()
里就是dict
的有关操作了,dict
的有关内容详细解释的话篇幅会比较多,因此我把dict
的有关内容单独拿了出来:Redis中的dict,从中可以看到,dict使用链地址法解决遇到的哈希冲突,当哈希表使用率达到一定程度时,还会通过rehash增大哈希表。因此这里的O(1)应该是指最优情况下O(1),同时redis采取了rehash操作尽量减少哈希冲突来保证性能。 -
使用set修改一个已有的字符串键值对时,具体过程是怎样的?
-
缓存雪崩是什么?有什么应对措施?
-
缓存穿透是什么?有什么应对措施?
-
memcache是什么?与Redis有什么异同?
- Redis中的LRU策略是怎么样的?