参考文档:
redis持久化: http://blog.csdn.net/freebird_lb/article/details/7778981
https://blog.csdn.net/jy692405180/article/details/72026745
redis数据结构&底层存储 https://www.cnblogs.com/hjwublog/p/5639990.html
redis事务
https://www.cnblogs.com/kyrin/p/5967620.html
redis与memcached对比
https://www.biaodianfu.com/redis-vs-memcached.html
redis主从复制: http://blog.csdn.net/freebird_lb/article/details/7778989
redis数据结构优化机制: http://blog.csdn.net/freebird_lb/article/details/7733994
io多路复用: http://blog.csdn.net/baixiaoshi/article/details/48708347
reactor模式详解: http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html
一 redis是什么
Redis(Remote Dictionary Server)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、一个高性能的key-value数据库。并提供多种语言的API
二 常用缓存数据库分析
三 redis支持的数据结构&底层存储结构
Redis数据类型内存结构分析
Redis内部使用一个redisObject对象来表示所有的key和value。redisObject主要的信息包括数据类型(type)、编码方式(encoding)、数据指针(ptr)、虚拟内存(vm)等。type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部式。
字符串是Redis值的最基础的类型。Redis中使用的字符串是通过包装的,基于c语言字符数组实现的简单动态字符串(simple dynamic string, SDS)一个抽象数据结构。其源码定义如下:
struct sdshdr {
int len; //len表示buf中存储的字符串的长度。
int free; //free表示buf中空闲空间的长度。
char buf[]; //buf用于存储字符串内容。
};
Hash是一个String类型的field和value之间的映射表,即redis的Hash数据类型的key(hash表名称)对应的value实际的内部存储结构为一个HashMap,因此Hash特别适合存储对象。相对于把一个对象的每个属性存储为String类型,将整个对象存储在Hash类型中会占用更少内存。
当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
Redis的List类型其实就是每一个元素都是String类型的双向链表。我们可以从链表的头部和尾部添加或者删除元素。这样的List既可以作为栈,也可以作为队列使用。
Redis 集合(Set类型)是一个无序的String类型数据的集合,类似List的一个列表,与List不同的是Set不能有重复的数据。实际上,Set的内部是用HashMap实现的,Set只用了HashMap的key列来存储对象。
Set中的元素,只是存放在了底层HashMap的key上,底层HashMap的value列为空,遍历HashSet的时候从HashMap中取出keySet来遍历
SortSet顾名思义,是一个排好序的Set,它在Set的基础上增加了一个顺序属性score,这个属性在添加修改元素时可以指定,每次指定后,SortSet会自动重新按新的值排序。
sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score
三 redis的持久化
Redis的持久化方式与传统数据库的方式有比较多的差别,Redis一共支持四种持久化方式,分别是:
1)定时快照方式(snapshot)
2)基于语句追加文件的方式(aof)
3)虚拟内存(vm)
4)Diskstore方式
后两种方式并不成熟,下面介绍下前两种持久化方式
定时快照方式(snapshot)
快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。
出现下面的情况redis会快照内存里的数据
1 用户发送bgsave命令(此时redis会fork一个子进程,子进程负责生成硬盘文件,父进程负责继续接受命令)
2 用户发送save命令(和bgsave命令不同,发送save命令后,到系统创建快照完成之前系统不会再接收新的命令,换句话说save命令会阻塞后面的命令,而bgsave不会)
3 用户在配置文件了配置了类似这样的命令
save 60 1000
这个的意思是说,自从上次快照成功算起,如果满足"60秒内有1000次写入"这个条件,系统就自动调用bgsave,如果配置文件里有多个save命令,只有满足一个就调用bgsave命令
4 用户发送shutdown,系统会先导员save命令阻塞客户端,然后关闭服务器
5 当有主从架构时,从服务器向主服务器发送sync命令来执行复制操作时,只有主服务器当时没有进行bgsave操作,那么主服务器就会执行bgsave操作。更多的关于主从复制的信息,见下文
快照的配置信息
save 60 1000 60秒内有1000次写入"这个条件,系统就自动调用bgsave
stop-writes-on-bgsave-error no 当持久化出现错误之后,是否继续提供写服务
rdbcompression yes 写的时候是否压缩
dbfilename dump.rdb 快照文件名称
dir ./ 快照文件目录
快照保存过程:
1)Redis 调用forks. 同时拥有父进程和子进程。
2)子进程将数据集写入到一个临时 RDB 文件中。
3)当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件
快照持久化的优点
1)快照持久化的方法采用创建一个子进程的方法来将 Redis 的内存数据保存到硬盘中,因此,它并不会对 Redis 服务器性能造成多大的影响,这可以说是快照持久化方法最大的一个优点。
2)快照持久化使用的 RDB 文件是一种经过压缩的二进制文件,可以方便地在网络中传输与保存
3) RDB的编码格式与redis内存中的数据编码格式是一致的。所以在启动redis重建数据库的时候,加载较快
快照持久化的缺点
1)如果 RDB 文件生成后,Redis 服务器继续处理了写命令导致 Redis 内存数据有更新,这时恰好 Redis 崩溃了而来不及保存新的 RDB 文件,那么 Redis 将会丢失这部分新的数据。因此,快照持久化方法只适用于那些即使丢失一部分数据也不会造成问题的应用场景。
2)快照持久化方法需要调用fork()方法创建子进程。当 Redis 内存的数据量较大时,创建子进程和生成 RDB 文件得占用较多的系统资源和处理时间,这会对 Redis 正常处理客户端命令的性能造成较大的影响
基于语句追加方式( Append-only file)
aof 比快照方式有更好的持久化性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容
AOF将数据写入磁盘的流程如下:
服务器接收到写操作请求,调用write()将请求写入内存缓冲区
Redis或操作系统调用fsync将缓冲区中的数据写入磁盘缓存
磁盘将缓存中的数据写入物理介质(大多数磁盘只开启读缓冲,所以写操作会直接写入物理介质)
aof配置
appendonly no
控制AOF的开关
appendfilename "appendonly.aof"
控制AOF文件的名字 appendfsync everysec
控制Redis调用fsync的频率(fsync:将数据从缓冲区写到磁盘)
调用fsync会使操作系统将数据完全写入到文件中,而不是放在输出缓冲区里面。appendfsync项支持三种模式:
no:从不调用fsync,由操作系统自行决定什么时候将缓冲区里的数据写入文件。大多数Linux系统是每30秒进行一次。这种模式的速度最快,效率最高,但是不能保证数据安全。 always:每次执行写操作之后都调用fsync。这种模式最慢,但也是最安全的,可以保证出错时最多只损失一次操作。这种模式由于不断地写入少量数据,会严重缩短固态硬盘的寿命。 everysec:每一秒调用一次fsync。是一种折中的方案,也是配置文件的默认值。当fsync调用时长超过一秒时,Redis会延迟下一次fsync的调用到下一秒。
no-appendfsync-on-rewrite no
控制当执行重写时是否阻止调用fsync
当调用fsync的时候如果正好在进行后台保存操作或者AOF重写,这时会有大量数据写入缓冲区,在某些情况下会导致阻塞。这个问题目前还没有从根本上得到修复,为了解决这个问题,可以将该项设置为yes,使Redis在执行BGSAVE或者BGREWRITEAOF时阻止调用fsync,不过这可能会导致损失一部分数据。
auto-aof-rewrite-percentage 100
用于控制AOF文件的自动重写规则。Redis会记录最后一次重写AOF文件后的文件大小(如果没有进行过重写,则记录AOF文件一开始的大小),如果当前AOF文件的大小大于指定的百分比,就会触发一次AOF重写。如果将该值指定为0,则Redis不会自动重写AOF文件。AOF的重写流程跟RDB一样。所以在新的AOF文件完全写入之前发生的写操作,都会写入旧的AOF文件以及缓存中。
auto-aof-rewrite-min-size 64mb
用于控制AOF重写的最小文件大小。如果AOF文件小于auto-aof-rewrite-min-size规定的值,则不会触发重写操作,这避免了在AOF文件不是很大的时候多次触发重写。
aof-load-truncated yes
当重启Redis进行AOF加载时,可能会发现AOF文件尾部不完整。这种情况只会发生在系统崩溃之后,尤其是使用了ext4文件系统且没有设置data=ordered的情况下。当aof-load-truncated被设置为yes时,Redis发现了问题之后会从AOF文件中尽可能地加载更多的数据,然后在log文件里记录这次错误。设置为no时,服务器会拒绝启动,此时用户可以使用redis-check-aof工具修复损坏的AOF文件
dir ./
配置AOF文件保存的路径。RDB产生的文件也会被保存到这个路径下面
AOF rewrite 重写
aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100 就够了。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下:
1)redis fork一个子进程
2)子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
3)父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题
4)当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件
5)现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加
需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
AOF的优点:
AOF可以将数据的损失降到最低
AOF的缺点:
比RDB更占用存储空间、加载速度慢(一是因为存储的命令较多,二是因为编码格式与内存不一致,还需要处理编码)
五redis事务
redis对事务的支持目前还比较简单。redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令。
相关命令
用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,然后才能使用EXEC命令原子化地执行这个命令序列。
这个命令的返回值是一个简单的字符串,总是OK。
-
EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
当使用WATCH命令时,只有当受监控的键没有被修改时,EXEC命令才会执行事务中的命令,这种方式利用了检查再设置(CAS)的机制。
这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。 当使用WATCH命令时,如果事务执行中止,那么EXEC命令就会返回一个Null
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。
这个命令的返回值是一个简单的字符串,总是OK。
Redis使用WATCH命令实现事务的“检查再设置”(CAS)行为。
作为WATCH命令的参数的键会受到Redis的监控,Redis能够检测到它们的变化。在执行EXEC命令之前,如果Redis检测到至少有一个键被修改了,那么整个事务便会中止运行,然后EXEC命令会返回一个Null值,提醒用户事务运行失败。
这个命令的返回值是一个简单的字符串,总是OK。
对于每个键来说,时间复杂度总是O(1)
在每个代表数据库的 redis.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。
比如说,以下字典就展示了一个 watched_keys 字典的例子:
触发过程:
在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,诸如此类), multi.c/touchWatchedKey 函数都会被调用 —— 它检查数据库的 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:
当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务
-
UNWATCH
清除所有先前为一个事务监控的键。
如果你调用了EXEC或DISCARD命令,那么就不需要手动调用UNWATCH命令
这个命令的返回值是一个简单的字符串,总是OK
时间复杂度总是O(1)
事务的 ACID 性质
在redis中事务总是具有原子性(Atomicity),一致性(Consistency)和隔离性(Isolation),并且当redis运行在某种特定的持久化模式下,事务也具有耐久性(Durability).
①原子性
事务具有原子性指的是,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。
但是对于redis的事务功能来说,事务队列中的命令要么就全部执行,要么就一个都不执行,因此redis的事务是具有原子性的。
②一致性
事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然一致的。
”一致“指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。redis通过谨慎的错误检测和简单的设计来保证事务一致性。
③隔离性
事务的隔离性指的是,即使数据库中有多个事务并发在执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全
相同。因为redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事物进行中断,因此,redis的事务总是以串行
的方式运行的,并且事务也总是具有隔离性的
④持久性
事务的持久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保持到永久存储介质里面。
因为redis事务不过是简单的用队列包裹起来一组redis命令,redis并没有为事务提供任何额外的持久化功能,所以redis事务的持久性由redis使用的模式决定
- 当服务器在无持久化的内存模式下运行时,事务不具有耐久性,一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失
- 当服务器在RDB持久化模式下运作的时候,服务器只会在特定的保存条件满足的时候才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE不
能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性
- 当服务器运行在AOF持久化模式下,并且appedfsync的选项的值为always时,程序总会在执行命令之后调用同步函数,将命令数据真正的保存到硬盘里面,因此
这种配置下的事务是具有持久性的。
- 当服务器运行在AOF持久化模式下,并且appedfsync的选项的值为everysec时,程序会每秒同步一次命令数据到磁盘因为停机可能会恰好发生在等待同步的那一秒内,这种 可能造成事务数据丢失,所以这种配置下的事务不具有耐久性
五 redis lua
redis支持lua脚本原子执行
|
请发表评论