目录
Windows下Redis安装教程
https://www.cnblogs.com/lezhifang/p/7027903.html
官网下载的是linunx版本,从https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504可下载windows版本
启动Redis:在cmd下切到redis目录:cd C:\Program Files\Redis-x64-3.0.504然后使用命令:redis-server redis.windows.conf
启动成功页面如下:
启动后默认端口为6379.将路径加入系统变量path中
在另一个cmd下使用redis-cli来使用redis
这样启动的redis,但只要关闭启动的那个cmd,redis服务就会停止,可以将redis设为windows下的服务。(1067错误)
Redis概述
-
为什么要使用nosql(redis)
https://blog.youkuaiyun.com/a909301740/article/details/80149552
原始架构:App->DAO->Mysql(DAO层-数据访问层,同图中DAL)
使用场景:网站的访问量不大,多是静态网页,动态交互类型比较少。
瓶颈:1 数据量的总大小一个机器放不下 时 2 数据的索引(B+ Tree)一个机器内放不下时 3 访问量(读写混合)一个实例不能承受。
架构演变1:缓存+MySQL+垂直拆分
APP->DAO->Cache->Mysql*n(垂直拆分)
垂直拆分:不同服务对应不同的mysql服务。例如userinfo一个mysql服务器,business1一个mysql服务器,bussiness2一个mysql服务器)
分布式:一个业务分拆多个子业务,部署在不同的服务器上。
优点:缓解了数据库的读写压力
缺点:只能缓解数据库的读取压力,让写集中在一个数据库上让数据库不堪重负。
架构演变2:mysql读写分离
APP->DAO->Cache->mysql(master,写userinfo)+mysql(salver*n,读userinfo)
优点:使用主从复制技术达到读写分离,提高读写性能和读库的可扩展性。master-slave模式。理解:读写分离,使得主库只写不读,提高了性能。又读又写的话性能较低。
缺点:主库的写入压力可能会遇到瓶颈。由于MyISAM在写数据的时候会使用表锁,在高并发写数据的情况下会出现严重的锁问题,大量的高并发MySQL应用开始使用InnoDB引擎代替MyISAM。MyISAM在写的时候锁表,InnoDB只锁行,发生锁冲突的概率低,并发性能高。
架构演变3:分库分表+水平拆分+mysql集群
APP->DAO->Cache->mysql cluster(数据库集群)(热点放在一个库,其它的放在一个库,这叫分库。将大表分成小表)
现在的服务架构
最前面是企业级的防火墙,后面通过负载均衡主机(软负载:Nginx,硬负载:F5)在web集群之间进行调度,再由具体的web服务器(Tomcat)去访问缓存,访问数据库。
分布式:多个人在一起做不同的事。将一件事拆分成不同的部分给不同的人干
集群:多个人干同样一件事。数据库集群,热点数据放一个服务器,其它数据放一个服务器。
Redis是什么:Redis是一种支持Key-value等多种数据结构的存储系统。(非关系型数据库)
-
Redis能做什么不能做什么?
Redis能干什么事?
- 缓存:毫无疑问这是Redis当前为止最为人熟知的使用场景,再提升服务器性能方面非常有效。
- ZSet排行榜:如果使用传统的关系型数据库来做这个事,非常的麻烦,而利用Redis的SortSet数据结构能够非常方便搞定。
- 计算器/限速器:利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果使用mysql,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力。
- Set好友关系:利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能。
- 简单消息队列:除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以使用List来完成异步解耦
- Session共享:以PHP为例,默认Session都是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就将导致用户频繁登录;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session消息。
Redis不能干什么事?
Redis能干的事情特别多,但它不是万能的,适合的地方用它事半功倍。如果滥用可能导致系统的不稳定、成本增高等问题。
- 数据量太大不适合:会增加成本
- 数据访问频率太低不适合:保存在内存中纯属浪费资源
比如,用Redis去保存用户的基本信息,虽然它能够支持持久化,但是它的持久化方案并不能保证数据绝对的落地,并且还可能带来Redis性能下降,因为持久化太过频繁会增大Redis服务的压力。
数据量太大、数据访问频率非常低的业务都不适合使用Redis,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源。
-
为什么使用Redis?
上述说了很多Redis的应用场景,那么这些场景的解决方案也有很多其它选择,比如缓存可以用Memcache,Session共享还能用MySql来实现,消息队列可以用RabbitMQ,我们为什么一定要用Redis呢?
- 速度快,数据都在内存中;使用C语言实现距离系统更近;单线程避免了线程切换开销和多线程的竞争问题;网络层采用epoll()解决高并发问题(非阻塞I/O,不在网络上浪费时间)
- 丰富的数据类型:Redis有八种数据类型,主要的是String、Hash、List、Set、ZSet这5中类型。他们都是基于键值的方式组织数据。每一种数据类型提供了非常丰富的操作命令,可以满足绝大部分需求,如果有特殊需求还能自己通过 lua 脚本自己创建新的命令(具备原子性);
- 功能丰富:Redis还提供了像慢查询分析、性能测试、Pipeline、事务、Lua自定义命令、Bitmaps、HyperLogLog、发布/订阅、Geo等个性化功能。
- 服务器简单:Redis的代码开源在GitHub,代码非常简单优雅,任何人都能够吃透它的源码;它的编译安装也是非常的简单,没有任何的系统依赖;
- 有非常活跃的社区,各种客户端的语言支持也是非常完善。
- 另外它还支持事务(没用过)、持久化、主从复制让高可用、分布式成为可能。
Redis的五种基本类型及底层实现
-
底层数据结构
- SDS简单动态字符串
len标记长度,alloc记录总分配内存大小,flags记录字节数组属性,buf记录字符串真正的值,char[]型。
flag分为5,8,16,32,64,其中5没有len,针对不同长度有不同数据结构。冗余分配,小于1m两倍,大于1m扩展1m。惰性空间释放,甚至不清零,因为采用len标记长度,不需要用/0来标识。优势:二进制安全,获取长度时间复杂度O(1),动态化,惰性释放和冗余分配降低内存分配次数。
- ADList(A generic doubly linked list)双向链表
typedef struct listNode { // 双向节点
struct listNode *prev;
struct listNode *next;
void *value; // 空指针,可以被指向任何类型
} listNode;
typedef struct list {
listNode *head; // 头指针
listNode *tail; // 尾指针
void *(*dup)(void *ptr); // 复制函数
void (*free)(void *ptr); // 节点释放函数
int (*match)(void *ptr, void *key); // 对比函数函数
unsigned long len; // list长度
} list;
void*实现多态,len记录长度。迭代操作由专门的迭代器实现。
- dict字典
dict中包含两个哈希表dictht,dictht是一个哈希表结构,使用拉链法解决哈希冲突
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
struct dictEntry *next; // 拉链法解决冲突,下一个节点
} dictEntry;
typedef struct dictht { // hash表
dictEntry **table; // 节点数组
unsigned long size; // hash表大小
unsigned long sizemask; // hash表掩码,等于size-1,用于计算hash值
unsigned long used; // 已有节点数量
} dictht;
typedef struct dict { // 字典
dictType *type; // 各种字典操作方法
void *privdata; // 私有数据,用于传给操作函数
dictht ht[2]; // 两个hash表,一个用来存储当前使用的,一个用来rehash
long rehashidx; // rehash标志位,用于判断是否在rehash和记录rehash进度
int iterators; // 迭代器的运行数量
} dict;
有两个哈希表是为了方便进行rehash操作。在扩容时,将其中一个dictht上得的键值对rehash到另一个dictht上,完成之后释放空间并交换两个哈希表的角色。
如果hash表很大,但是键值对太少,也就是hash表的负载(dictht->used/dictht->size)太小,就会有大量的内存浪费;如果hash表的负载太大,就会影响字典的查找效率。这时候就需要进行rehash将hash表的负载控制在一个合理的范围。
rehash操作不是一次性完成,而是采用渐进式rehash,这是为了避免一次性执行过多的rehash给操作给服务器带来过大的负担。
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n * 10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while (n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long) d->rehashidx);
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while (de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
渐进式的rehash通过记录dict的rehashidx完成,从0开始,然后每执行一次rehash都会递增。例如在以喜rehash中要把dict[0]rehash到dict[1],这一次会把dict[0]上table[rehashidx]的键值对rehash到dict[1]上,dict[0]的table[rehashidx]指向null,并且令rehashidx++。
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的查找操作也需要到对应的 dictht 去执行。
- intset整数集合
地址在内存中连续,增删查改通过地址偏移完成。只能存储整数,通过二分法查询。
重点是升级——
对新元素进行检测,看保存这个新元素需要什么类型的编码;
将集合 encoding 属性的值设置为新编码类型,并根据新编码类型,对整个 contents 数组进行内存重分配。
调整 contents 数组内原有元素在内存中的排列方式,从旧编码调整为新编码。
将新元素添加到集合中。即:集合中的所有元素的编码方式必须和最大或最小的一致。
- ziplist压缩表
previous_entry_ength的长度可能是1个字节或者是5个字节,如果上一个节点的长度小于254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于254,则previous length的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。
注意,encoding其实 是encoding+length
- quicklist快速列表
在3.2之前,list是根据元素数量的多少采用ziplist或者adlist作为基础数据结构,3.2之后统一改用quicklist,从数据结构的角度来说quicklist结合了两种数据结构的优缺点,复杂但是实用:
链表在插入,删除节点的时间复杂度很低;但是内存利用率低,且由于内存不连续容易产生内存碎片
压缩表内存连续,存储效率高;但是插入和删除的成本太高,需要频繁的进行数据搬移、释放或申请内存
而quicklist通过将每个压缩表用双向链表的方式连接起来,来寻求一种收益最大化。
- skiplist跳跃表
是有序集合的底层实现之一。
跳跃表是基于多指针有序链表实现的,可以看成是多个有序链表。
查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。
与红黑树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
-
基本数据结构
- String 1.int 2.embstr 3.sds
- Hash 1.ziplist 2.dict
- List 1.quicklist
- Set 1.inset 2.dict
- Sorted set 1.ziplist 2.skiplist + dict(skiplist范围操作,dict用于查找)
使用场景
除了上面的之外,还可以
- 查找表
例如DNS记录就很适合使用Redis进行存储。
查找表和缓存类似,也是利用了Redis快速查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
- 消息队列
List是一个双向链表,可以通过Ipush和rpop写入和读取消息
不过最好使用 Kafka、RabbitMQ 等消息中间件。
- 分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
Redis和Memcached
两者都是非关系型内存键值数据库,主要有以下不同
- 数据类型
Memcached仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。
- 数据持久化
Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
- 分布式
Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
Redis Cluster 实现了分布式的支持。
- 内存管理机制
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
键的过期时间
定期删除+惰性删除:Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。定期100ms检查,有过期的就删除,这种主动删除的方式还是基于概率检测,即如果随机测试了100个设置了过期时间的key,发现有大于阈值的key是过期的,则重复运行主动删除操作。如果这样还是内存占用还是高,那就进行内存淘汰机制,比如采用LRU的策略。
数据淘汰策略
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Redis 具体有 6 种淘汰策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止驱逐数据 |
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。Redis提供了两种持久化的方式。
- RDB:在指定的时间间隔内对数据进行快照备份
- AOF:每次记录对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据。
-
持久化的配置
为了使用持久化的功能,我们需要先知道该如何开启持久化的功能。
# 时间策略
save 900 1
save 300 10
save 60 10000
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
持久化的时间策略:
- save 900 1:表示900s内如果有1条是写入命令,就触发产生一次快照,可以理解为就进行一次备份
- save 300 10:表示300s内有10条写入,就产生快照
下面的类似,那么为什么需要配置这么多条规则呢?因为Redis每个时段的读写请求肯定不是均衡的,为了平衡性能与数据安全,我们可以自由定制什么情况下触发备份。所以这里就是根据自身Redis写入情况来进行合理配置。可以自定义时间策略。
stop-writes-on-bgsave-error yes:这个配置也是非常重要的一项配置,这是当备份进程出错时,主进程就停止接受新的写入操作,是为了保护持久化的数据一致性问题。如果自己的业务有完善的监控系统,可以禁止此项配置, 否则请开启。
rdbcompression yes:建议没有必要开启,毕竟Redis本身就属于CPU密集型服务器,再开启压缩会带来更多的CPU消耗,相比硬盘成本,CPU更值钱。
当然如果你想要禁用RDB配置,也是非常容易的,只需要在save的最后一行写上:save ""
-
AOF的配置
# 是否开启aof
appendonly yes
# 文件名称
appendfilename "appendonly.aof"
# 同步方式
appendfsync everysec
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof时如果有错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
- appendfsync everysec 同步方式:有三种模式
- always:把每个写命令都立即同步到aof,很慢,但是很安全
- everysec:每秒同步一次,是折中方案
- no:redis不处理交给OS来处理,非常快,但是也最不安全
一般情况下都采用 everysec 配置,这样可以兼顾速度与安全,最多损失1s的数据。
aof-load-truncated yes:如果该配置启用,在加载时发现aof尾部不正确是,会向客户端写入一个log,但是会继续执行,如果设置为 no
,发现错误就会停止,必须修复后才能重新加载。
-
工作原理
定时任务
在介绍原理之前先说下Redis内部的定时任务机制,定时任务执行的频率可以在配置文件中通过 hz 10
来设置(这个配置表示1s内执行10次,也就是每100ms触发一次定时任务)。该值最大能够设置为:500,但是不建议超过:100,因为值越大说明执行频率越频繁越高,这会带来CPU的更多消耗,从而影响主进程读写性能。
定时任务使用的是Redis自己实现的 TimeEvent,它会定时去调用一些命令完成定时任务,这些任务可能会阻塞主进程导致Redis性能下降。因此我们在配置Redis时,一定要整体考虑一些会触发定时任务的配置,根据实际情况进行调整。
RDB的原理
在Redis中RDB持久化的触发分为两种:自己手动触发与Redis触发
什么是fork?
fork调用后会立即生成一个子进程,通过copy父进程的地址空间和资源来实现子进程的创建。
同时,fork函数在子进程中返回的是0;在父进程中返回的是子进程的PID;出现错误,fork返回一个负值;
- 手动触发
- save:会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。
- bgsave:该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。
- 自动触发
- 根据我们的
save m n
配置规则自动触发; - 从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发
bgsave
; - 执行
debug reload
时; - 执行
shutdown
时,如果没有开启aof,也会触发。
由于save
基本不会被使用到,我们重点看看bgsave
这个命令是如何完成RDB的持久化的。
fork操作会阻塞,导致Redis读写性能下降。可以控制单个Redis实例的最大内存,来尽可能降低Redis在fork时的事件消耗。以及上面提到的自动触发的频率减少fork次数,或者使用手动触发,根据自己的机制来完成持久化。
- 优点:RDB数据结构紧凑,非常适合备份和恢复。生成RDB文件采用fork子线程,不干扰主线程。RDB恢复比AOF更快。
- 缺点:RDB数据结构保存了版本,可能出现不兼容。RDB的备份不是实时的,并且bgsave会fork一个子线程导致内存占用大。redis意外宕机会导致最后一次快照的所有修改全部丢失。比如900秒1次,899秒的时候宕机,那这一次就不会写进去。
-
AOF工作原理
AOF的整个流程大体来看可以分为两步,一是命令的实时写入(如果是 appendfsync everysec
配置,会有1s损耗),第二步是对aof文件的重写。
对于增量追加到文件这一步主要的流程是:命令写入=》追加到aof_buf =》同步到aof磁盘。那么这里为什么要先写入buf在同步到磁盘呢?如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。如果直接写入内存,IO增多影响性能。
aof重写是为了减少aof文件的大小,可以手动或者自动触发,关于自动触发的规则请看上面配置部分。fork的操作也是发生在重写这一步,也是这里会对主进程产生阻塞。
手动触发: bgrewriteaof
,自动触发 就是根据配置规则来触发,当然自动触发的整体时间还跟Redis的定时任务频率有关系。
AOF的重写机制:
注意:前面也说到了,AOF的工作原理是将写操作追加到文件中,文件的冗余内容会越来越多。所以聪明的 Redis 新增了重写机制。当AOF文件的大小超过所设定的阈值时,Redis就会对AOF文件的内容压缩。
重写的原理:Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中。并没有读取旧文件(你都那么大了,我还去读你??? o(゚Д゚)っ傻啊!)。最后替换旧的aof文件。
触发机制:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。这里的“一倍”和“64M” 可以通过配置文件修改。
注意:
- 在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;因此它依然会写入旧的AOF file中,如果重写失败,能够保证数据不丢失。
- 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个buf,防止新写的file丢失数据。
- 重写是直接把当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。
- AOF文件直接采用的文本协议,主要是兼容性好、追加方便、可读性高可认为修改修复。
理解就是:在aof文件达到一定阈值的时候,根据内存中的数据重新写一个AOF文件(执行这些文件就能得到内存中的数据)
- 优点:最多丢失2s数据。redis-check-aof可以轻松修正aof文件,即删除掉不正常的命令。aof的可读性比较器强
- 缺点:AOF体积大。AOF因为每秒都要写,因此对性能有影响。RDB储存方式更加健壮。
-
从持久化中恢复数据
数据的备份、持久化做完了,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?
启动时会先检查AOF文件是否存在,如果不存在就尝试加载RDB。那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。
-
RDB和AOF混合持久化
这种持久化能够通过 AOF 重写操作创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF 文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态: 至于那些在重写操作执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 数据之后。
再重写时,创建一个同时包含RDB和AOF的AOF文件,RDB存储了重写开始时的数据,重写之后的数据操作以AOF的形式写在AOF文件后面。
-
性能与实践
通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
- 降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;
- 控制Redis最大使用内存,防止fork耗时过长;
- 使用更牛逼的硬件;
- 合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。
提高性能的一些手段:
- 如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;
- 自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据;
- 单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行;
- 可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;
- RDB持久化与AOF持久化可以同时存在,配合使用。
事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。减少网络通信带来的新能消耗。
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
- MULTI:事务开启的标志
- DISCARD:放弃事务的标志
- EXEC:提交事务
- WATCH:对某个key上锁
WATCH命令的实现:所有被进行写操作的key都会调用multi.c/touchWatchKey函数。这个方法将检查watched_keys这个dict,看看是否有watch标记,如果有,就会被标记为REDIS_DIRTY_CAS,当事务执行的相关key有这个标记的话,就说明在执行事务的同时该数据已经被篡改,因此事务执行失败。
事件
Redis 服务器是一个事件驱动程序。
文件事件
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。
reactor模式:反应器设计模式,是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至相关的工作线程进行处理。
事件事件
服务器有一些操作需要在给定的事件点执行,事件事件是对这类定时操作的抽象。
时间事件又分为:
- 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。
Redis将所有时间事件都放在一个无序链表中,遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。
事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内完成,因此监听应该根据距离现在最近的时间事件来决定。(大概就是最近的时间事件还要多久到达,在这个最近的时间事件到达之前,监听执行文件事件)。
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
# 处理所有已产生的文件事件
procesFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
从事件处理的角度来看,服务器运行流程如下:
复制
主从复制
主从复制,主机数据更新后根据配置策略,自动同步到备机的master/slaver机制,master以写为主,slave以读为主。用处为读写分离和容灾恢复。
- 用法:
- 配从库不配主库
- 从库配置:salveof 主库ip 主库端口,每次与master断开之后,都需要重新连接,除非配置进redis.conf。slaveof host port
- 修改配置文件细节操作
- 一主二从 主挂了从不会代替,从挂了上线就失去了从的身份
- 薪火相连 去中心化,每个都是中间节点
- 反客为主 SALVEOF no one
- 主从复制的实现远原理:
- 从节点中保存主节点信息,包括masterhost与masterport。slaveof是异步指令,实际的复制过程在slaveof之后进行。
- 建立socket连接,从节点连接主节点,并建立一个专门处理复制工作的文件事件处理器,负责后序复制工作。
- 发送ping命令。从节点ping主节点,看看是否连接成功。
- 身份验证,从节点向主节点进行身份验证。
- 发送从节点端口信息。从节点向主节点发送监听的端口号。执行info replication可以显示。
- 数据同步阶段——从节点向主节点发送psync命令,开始同步。此阶段之前,主节点不是从节点的客户端,到了这个阶段后,互为客户端。
- 命令传播阶段。主节点将自己执行的写命令发送给从节点,从节点接受命令并执行,从而保证主从一致性。主从还维持心跳机制ping和replconf ack。repl-disable-tcp-nodela=no的时候TCP马上发送主节点数据包,带宽增加延迟变小,否则会对包进行合并从而减少带宽,降低频率,增加延迟。
- 心跳机制
心跳机制是定时发送一个自定义的结构包(心跳包),让对方知道自己还或者,以确保连接的有效性的机制。
心跳包:
主-从ping:官方文档中是从向主发送ping命令,默认10s。
replconf ack,1s发送一次,命令格式为REPLCONF ACK{offset}。作用:1.实时监测主节点状态。2.比较offset,如果不一致则从复制挤压缓冲区中拿。3.保证从节点的数量和延迟,如果lag值很高,就说明从节点过多,主机会拒绝执行写命令。
- 理解:从服务器保存着主服务器的ip和端口号,建立socket连接,从服务器连接主服务器,然后从服务器会建立一个专门处理复制的文件事件,负责后面的复制工作。从服务器ping主服务器看是否连接成功,并且进行身份验证。从服务器发送端口监听信息。然后进行数据同步,首先主服务器创建文件快照,发送给从服务器,并在发送期间用缓冲区记录执行的写命令,先发送快招,再发送缓冲区的写命令。从服务器丢弃之前的所有数据,先接收快照,之后接收发来的写命令。保证主从一致性。同时还维持心跳机制,保证传输有效。
全量复制与增量复制
- 全量复制:非常重量的操作,主节点fork子进程RDB、从节点接收RDB并读取、从节点清空老数据等都是阻塞的、从节点bgrewriteaof有额外消耗
- 主节点无法进行部分复制/子节点要求全量复制。
- 主节点收到全量复制命令后,执行bgsave,在后台生成RDB文件,使用复制缓冲区记录此时刻开始的写命令。
- 发送RDB给从,从先删除自己的旧数据,然后载入RDB
- 从载入主的缓冲区命令
- 如果从开启AOF,会触发bgrewritebg,从而保证AOF文件更新至主节点最新状态。
- 增量复制
- 复制偏移量,代表主节点向从节点传递的字节数。如果主是A,从是B,即要复制的增量就是A-B.
- 主节点维护一个复制积压缓冲区,固定长度,先进先出,作用是备份主节点最近发送给从节点的数据。这个本质上是为了防止从节点断线之后,再上线的时候可以通过增量复制的形式恢复同步。(这个功能默认是被注释掉的,repl-backlog-size)
- runid,如果从节点断线重连后发现主机runid没变,那就尝试增量,否则全量。
哨兵sentinel
sentinel 哨兵可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
作用:在复制的基础上,哨兵实现了自动化的故障恢复。核心:主服务器的自动故障转移
- 监控:监控会不断地检查主服务器和从服务器是否运转正常。
- 自动故障转移:当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 配置提供者:客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
- 通知:哨兵可以将故障转移的结果发送给客户端。
- 主观下线
- 客观下线
- 投票选举哨兵领导者
- 哨兵领导者进行故障转移,从节点选举为主节点的原则:先过滤掉不健康的从节点,选择优先级最高的,选择复制偏移量最大的,选择runid最小的。
- 通过slaveof no one使得从节点变为主节点,并通过slaveof命令使其他节点成为从节点。之前的主节点变成新的主节点的从节点。
分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。
- 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
- 服务器分片:Redis Cluster。
其它
消息订阅发布
每个redis服务器进程都维护一个redis.h/redisServer结构,pubsub_channles是一个字典,字典的key是正在被订阅的频道,value是订阅这个频道的客户端们list,用于保存订阅的频道:
struct redisServer {
// ...
dict *pubsub_channels;
// ...
};
redisServer/pubsub_patterns,是一个list,list中保存着所有和模式相关的信息,所谓模式就是通配符,比如tweet.shop.*。这个就是一个模式。
struct redisServer {
// ...
list *pubsub_patterns;
// ...
};
typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;
分布式数据库中CAP原理CAP+BASE
- C Consistency 强一致性 一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。
- A Availability 可用性 即服务一直可用,而且是正常响应时间。
- P Partition tolerance 分区容错性 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
- BA Basically Available 基本可用 分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。 - S Soft state 软状态 状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
- Eventual Consistency 最终一致性 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
其它
最终一致性:这是弱一致性的特殊形式;存储系统保证如果没有对某个对象的新更新操作,最终所有的访问将返回这个对象的最后更新的值。
缓存穿透——黑客故意去请求缓存上不存在的数据,导致所有请求都要记录在数据库上,从而造成数据库连接异常。
解决方案:1、布隆过滤器先去布隆过滤器中查询数据库是否有这个key,如果没有,返回null,不用查数据库了。2、缓存中依然缓存这个key,value为null,失效时间5分钟,这样就不会穿透mysql了。
缓存雪崩——缓存同一时间大面积的失效。
解决方案:1、失效时间加一个随机值,便面计提时效。
缓存击穿——热点数据过期后,数据库被击穿。
解决方案:1、采用互斥锁,即其他无法对这个key进行操作,然后load db,然后同步到缓存,解除锁,这样后面的所有请求都不用访问数据库。2、提前采用互斥锁,在value设置一个timeout值,当timeout值接近过期的时候,立刻重新加载。3、不设置过期时间。
并发获取key:如果不要求顺序:加分布式锁,谁拿到锁谁set。要求顺序,给时间戳,时间戳之前的不需要set了。利用Redis的setnx命令。此命令同样是原子性操作,只有在key不存在的情况下,才能set成功。