redis 采用的是单线程的 KV 模型,由 C 语言编写。由于redis采用了单线程,避免了不必要的线程切换所带来的性能消耗,且不用考虑加锁问题(没有加锁和释放锁的操作)。并且完全是基于内存的,所以性能比较好。
事务指的是单独的隔离操作,事务中的命令要么都执行,要么都不执行。
Redis 事务的本质是通过multi,exec,discard,watch 等一组命令的集合。一次性,顺序性,排他性的执行一个队列中的一系列命令
一次性: 一次执行多个命令
顺序性: 所有命令都被序列化且按照顺序执行
排他性: 不会被其他客户端发送的命令打断
MULTI: Redis 使用 MULTI 命令标记事务开始,它总是返回OK,MUlTI 执行之后,客户端可以发送多条命令,Redis 会把这些命令放到队列中,而不是立马执行这些命令。所有命令会在 EXEC 命令之后执行。
DISCARD : 命令取消事务,放弃执行事务队列内的所有命令,恢复连接为非 (transaction) 模式。
如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH 。
EXEC: EXEC命令触发所有的命令执行,所以客户端如果是在 EXEC命令前丢失连接,那么所有的命令都不会被执行,相反,如果EXEC被调用,那么所有命令会被执行。当使用 append-only file 方式持久化时,Redis使用单个 write(2) 系统调用将事务写到磁盘上。但是,如果Redis服务器崩溃或被系统管理员以某种硬方式杀死,则可能只注册了部分操作。Redis重启的时候会检测到这种情况,并返回错误退出。使用 redis-check-aof 工具可以删除部分事务,这样Redis可以重新启动。
WATCH: 监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
UNWATCH: 取消WATCH对所有key的监视。
127.0.0.1:6379> keys *
1) "user"
2) "count"
3) "test"
127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key2 value2
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value11
QUEUED
127.0.0.1:6379(TX)> sets key2 value22
(error) ERR unknown command `sets`, with args beginning with: `key2`, `value22`,
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379> get key2
"value2"
可以看到在语法错误时,提交事务时 key1,key2 值并没有被更改
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379> get key2
"value2"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value11
QUEUED
127.0.0.1:6379(TX)> lpush key2 value22
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get key1
"value11"
127.0.0.1:6379> get key2
"value2"
我们在事务开启后,将key2设置为错误的类型 List(key2 原本是 String类型),此时 Redis 没有检测出异常,直到事务 EXEC 执行时,才事务失败,但是我们可以看到 key1 设置的值并没有进行回滚。
多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。
Redis 中存在五种数据结构,String、Hash、Set、Zset、List
想了解数据结构的底层实现,可以看一下博文:Redis五种数据结构详解
字符串类型,一个key对应一个value
常用命令(set ,get,del,incr,decr)
127.0.0.1:6379> set test helloworld
OK
127.0.0.1:6379> get test
"helloworld"
127.0.0.1:6379> set count 2
OK
127.0.0.1:6379> get count
"2"
127.0.0.1:6379> incr count
(integer) 3
127.0.0.1:6379> incrby count 10
(integer) 13
127.0.0.1:6379> del test
(nil)
哈希表类型,一个key对应多个键值对
常用命令:hget 、hset 、 hdel、hgetall
127.0.0.1:6379> hset user name wang
(integer) 1
127.0.0.1:6379> hset user email1 wddong@162 email2 wdding@163
(integer) 2
127.0.0.1:6379> hget user
(error) ERR wrong number of arguments for 'hget' command
127.0.0.1:6379> hget user name
"wang"
127.0.0.1:6379> hgetall user
1) "name"
2) "wang"
3) "email1"
4) "wddong@162"
5) "email2"
6) "wdding@163"
127.0.0.1:6379> hdel user name
(integer) 1
127.0.0.1:6379> hget user name
(nil)
底层实现:Hash对象的实现方式有两种分别是ziplist(压缩列表)、hashtable
无序不重复集合类型
常用命令:sadd、srem、scard、smembers、sismember
Sadd | Sadd key-name item [item…]——将一个或多个元素添加到集合里面,并返回被添加元素当中原本并不存在于集合里面的元素数量 |
---|---|
Srem | Srem key-name item [item…]——从集合里面移除一个或多个元素,并返回被移除元素的数量 |
Spop | Spop key-name——随机地移除集合中的一个元素,并返回被移除的元素 |
Smove | Smove source-key dest-key item——如果集合source-key包含元素item,那么从集合source-key里面移除元素item,并将元素item添加到集合dest-key中;如果item被成功移除,那么命令返回1,否则返回0 |
Sismember | Sismember key-name item——检查元素item是否存在于集合key-name里 |
Scard | Scard key-name——返回集合包含的元素的数量 |
Smembers | Smembers key-name——返回集合包含的所有元素 |
Srandmember | Srandmember key-name [count]——从集合里面随机地返回一个或多个元素。当count为正数时,命令返回的随机元素不重复;当count为负数时,命令返回的随机元素可能会重复 |
Sdiff | Sdiff key-name [key-name…]——返回那些存在于第一个集合、但不存在于其他集合中的元素(数学上的差集运算) |
Sdiffstore | Sdiffstore dest-key key-name [key-name…]——将那些存在于第一个集合、但不存在于其他集合中的元素(数学上的差集运算)存储到dest-key键里面 |
Sinter | Sinter key-name [key-name…]——返回那些同时存在于所有集合中的元素(数学上的交集运算) |
底层实现:Set的底层实现是**「hashtable和intset(整数集合)」**
有序集合类型,元素不能重复,可排序。根据score分数作为排序的依据。
常用命令: zadd 、 zrange、 zscore
127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao
(integer) 2
127.0.0.1:6379> ZRANGE myscoreset 0 -1
1) "xiaohao"
2) "hao"
127.0.0.1:6379> ZSCORE myscoreset hao
"100"
底层实现:ziplist
和skiplist(跳表)
应用场景可以用来做网站的排行榜。
列表类型,一个key对应一个链表(双端链表,有序,value可重复,可以通过下标取出对应值)
常用命令:lpush、rpush、lrange、lpop、rpop
命令 | 注释 |
---|---|
lpush | [lpush key value1 value2 value3 …]左边插入 |
rpush | 右边插入 |
lrange | [lrange key start stop]获取列表指定范围的元素,lrange key 0 -1:stop=-1代表取出列表中所有元素 |
lpop | 从左边取出一个元素 |
lindex | [lindex key index]通过索引获取列表中的元素 |
llen | 获取列表的长度 |
lrem | [lrem key count value]移除列表元素 |
rpoplpush | [rpoplpush sourceList targetList]:从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
可以实现 「阻塞队列」,结合lpush和brpop命令就可以实现。生产者使用lupsh从列表的左侧插入元素,消费者使用brpop命令从队列的右侧获取元素进行消费。
底层实现:3.2之前的版本是使用ziplist
和linkedlist
进行实现的。在3.2之后的版本就是引入了quicklist
无论先操作db还是cache,都会有各自的问题,根本原因是cache和db的更新不是一个原子操作,因此总会有不一致的问题。想要彻底解决这种问题必须将cache和db的更新操作归在一个事务之下(例如使用一些分布式事务,或者强一致性的分布式协议)。
注意看上面的图片,当有两个写请求的线程,线程一比线程二先执行,反而是线程二先执行完。这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了。
如果采用写请求,先删除缓存,再更新数据库就会出现如上图的情况,线程B读到的是老的数据,并且缓存中也保存的是老的数据。
可以看到一个读请求和一个写请求,读请求可能会读取到旧的数据,或者当写请求删除缓存失败,读请求会一直读取的是旧的缓存数据。只不过是这种情况,相对于其他的实现方式概率要低很多。
第二次删除缓存一般会采用延时的操作,主要是用来删除读请求产生的缓存数据
延时双删和普通写操作的删除操作都有可能会操作失败,导致数据不一致,删除重试机制就是为了保证删除可靠性。(删除失败的key放到消息队列中)这种机制会造成大量的业务代码入侵。
通过binlog日志,将要删除的key发送到消息队列中。
将需要延时执行的任务放到 Redis 中的 Zset 类型中,Zset会根据 score 自动进行数据排序(score使用时间戳),定义一个延时任务检测器,检测器使用 zrangebysocre 命令查询 Redis 中符合执行条件的任务执行
Redis的队列list
是有序的且可以重复的,作为消息队列使用时可使用rpush/lpush
操作入队,使用lpop/rpop
操作出队。当发布消息是执行lpush
命令,将消息从列表左侧加入队列。消息接收方执行rpop
命令从列表右侧弹出消息。
如果队列空了,消费者会陷入pop死循环,即使没有数据也不会停止。空轮询不但消耗消费者的CPU资源还会影响Redis的性能。并且需要不停的调用rpop
查看列表中是否有待处理的消息。每调用一次都会发起一次连接,势必造成不必要的资源浪费。
入队的速度大于出队的速度,消息队列长度会一直增大,时间长了会占用大量的空间。
针对上面的 rpop
命令会一直阻塞队列,Redis提供了一种更优的 brpop
命令,brpop
可以设置一个超时时间,
Redis 中的过期策略共有三种
Redis 采用的过期策略是 定期+惰性 删除
在设置key的过期时间的同时为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
优点: 保证内存被尽快释放
缺点: 过期的key太多,删除这些key会占用很多的CPU时间。设置了过多的定时器,会对redis 的性能造成影响。
默认一段时间就去随机部分扫描redis中的设置了过期时间的key,检查是否过期,过期的话就移除key
因为扫描全部的key会非常多,很影响性能
惰性删除就是等到有查询key的请求过来的时候,我看看这个key有没有过期,过期的话就删除这个key
缺点: 可能会造成内存泄漏
设置方式: config set maxmemory-policy volatile-lru
关于volatile-lru:LRU 算法实现:1.通过双向链表来实现,新数据插入到链表头部;2.每当缓存命中(即缓 存数据被访问),则将数据移到链表头部;3.当链表满的时候,将链表尾部的数据丢弃。
指定redis 的淘汰策略
# maxmemory-policy noeviction
Redis采用的是多路I/O复用模型。
多路:多个socket连接,
复用:复用一个线程
多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量的减少网络IO的时间消耗)。
Redis采用多路I/O复用模型,且完全基于内存操作。
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis客户端可以订阅任意数量的频道。
客户端订阅频道
# 第一个客户端
redis 127.0.0.1:6379> SUBSCRIBE runoobChat # 这个客户端订阅了一个叫 runoobChat 的频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "runoobChat"
3) (integer) 1
# 第二个客户端
redis 127.0.0.1:6379> PUBLISH runoobChat "Redis PUBLISH test" #向runoobChat 频道中发布消息
(integer) 1
redis 127.0.0.1:6379> PUBLISH runoobChat "Learn redis by runoob.com"
(integer) 1
发布消息之后,就能再 订阅频道的客户端上看到发布的消息数据
//psubscribe 订阅命令
//以 keyspace 为前缀的频道被称为键空间通知(key-space notification)
//而以 keyevent 为前缀的频道则被称为键事件通知(key-event notification)
//0 表示数据库 表示对1号库操作
//expired 通知(每当一个键因为过期而被删除时产生通知)
psubscribe __keyevent@0__:expired
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "name"
memcache如何实现主从?
memcache自己没有防止单点的措施,为了保障memcache服务的高可用,需要借用外部的Repcached 工具来实现,Repcached 用来实现 Memcached 复制功能的一个工具。它所构建的主从方案是一个单主单从的方案,不支持多主多从。但是,它的特点是,主从两个节点可以互相读写,从而可以达到互相同步的效果。
假设主节点坏掉,从节点会自动切换为监听状态从而成为主节点,等待其他从节点额加入。
为什么高并发下有时单线程的 redis 比多线程的 memcached 效率要高?
原因:memcache 多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。
Redis 中的数据都是保存在内存中的,当Redis服务重启后,内存中的数据都会丢失,所以需要将内存中的数据保存到磁盘上,方便系统故障时,从磁盘上的备份数据恢复到内存中。
Redis 中的持久化方式有两种,RDB全量持久化和AOF增量持久化。一般情况下,Redis 中的RDB和AOF方式都需要开启。两者同时开启的情况下,默认使用 AOF方式。
RDB持久化的方式是对Redis中的数据,进行周期性的存储。RDB方式会生成一个RDB文件,RDB文件中记录的都是某一时刻里Redis中的数据。
RDB方式,Redis会是通过一个fork程来做持久化的工作。fork线程先将数据写入到一个临时文件中,写入成功之后,再替换掉之前的 RDB 文件,不会影响主线程的工作,所以这种方式对Redis的性能影响很小。
通过调整 redis.conf 配置文件中的save参数
save 900 1 代表 900s 之内有 1个 key发生修改,则发起内存快照保存
save 300 10 代表 300s 之内有 10个 key发生修改,则发起内存快照保存
#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作
stop-writes-on-bgsave-error yes
#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes
#rdb文件的名称
dbfilename dump.rdb
#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
dir /var/lib/redis
由于RDB都是快照文件,并不是实时更新的,当Redis突然宕机,会造成数据丢失的情况。
AOF方式默认是通过一秒一次的一个后台线程 fsync 来进行数据备份操作的,操作方式是 append-only (只追加)的方式
Redis 运行时打开AOF
redis-cli> CONFIG SET appendonly yes
修改 redis.conf 文件,设置不同的fsync策略
appendfsync yes # 开启AOF功能
appendfsync no # 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。由操作系统决定何时同步
appendfsync always # 每次有数据修改时都会写入AOF文件
appendfsync everysec # 每秒同步一次,该策略为AOF的缺省策略
#aof文件名, 保存目录由 dir 参数决定
appendfilename "appendonly.aof"
# AOF文件重写时,是否需要将写指令通过追加的方式追加到原有的AOF文件中。(yes只写入到内存缓冲区,no写入到内存缓冲区同时追加到原有AOF文件)
no-appendfsync-on-rewrite no
#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb
#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes
AOF方式采用只追加的方式,每条命令都会追加到磁盘中,对Redis性能的影响很大。
由于AOF是采用只追加的方式,这种方式会导致 AOF 文件越来越大,所以 Redis 为 AOF 提供了rewrite(重写)机制。
rewrite机制:当AOF文件大小超过所设定的阈值,redis就会启动AOF的文件内容压缩。
rewrite是怎么实现文件内容压缩的?
比如我们的AOF文件中由 100 条INCR指令,压缩时会把这一百条指令合并成一条SET指令。
FLUSHALL指令被执行后,如何通过AOF文件恢复?
FLuSHALL是将redis中的内存数据全部清空,这种情况下,`只要开启了AOF方式,而且AOF还没有被重写`,我们只需要通过快速停止Redis并编辑AOF文件,删除最后的FLUSHALL指令。重启Redis就能将数据恢复。
rewrite方式的内部工作原理
在重写开始时,Redis 会创建一个 fork(重写子进程),这个fork进程会读取现有的 AOF 文件,将现有AOF文件中的指令压缩并写入到一个临时文件中。
在fork进程压缩写入临时文件的同时,主工作进程会将新接收到的写指令一边写入到内存缓冲区一边继续写入到原有的AOF文件中(保证原有AOF文件的可用性,防止重写时发生意外)
当fork进程重写完成后,会向主进程发送一个信号,主进程收到信号后,会将内存缓冲区中的写指令以追加的方式追加到临时文件中,当临时文件(新AOF文件)追加结束后,就会替换掉原有的AOF文件。之后再有新的写指令就会追加到新的AOF中了;
性能:RDB方式使用一条fork线程来进行持久化,对Redis性能影响较小,AOF方式通过后台线程 fsync 通过追加的方式,对Redis的性能影响较大。
文件:相同数据集的情况下,AOF文件的大小会远大于RDB文件
存储方式:RDB采用快照的方式,AOF采用只追加的方式
恢复速度:RDB快于AOF
数据方面:AOF和RDB都有可能造成数据丢失,AOF方式更加可靠。
为了避免单点Redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上,连接在一起,并保证数据同步。这样即使是有一台服务器宕机,其他服务依然可以提供服务。实现Redis的高可用。同时实现数据的冗余备份。这种形式也就是主从复制架构
一个master可以拥有多个slave节点,一个slave节点只能有一个master节点
master:
1. 写数据
2. 执行写操作时,将变化的数据自动同步到slave节点
slave:
1. 读数据
master节点是可以用来读数据的(不建议master节点做读操作)
slave节点是禁止用来写数据的(redis 2.6后默认从服务器时不能进行写操作的,但是可以通过 replica-read-only yes|no 开启从服务器写)
建立连接阶段工作流程(结果:master中保存slave的IP端口,slave保存master的IP端口,同时之间建立socket连接)
设置master的地址和端口,保存master信息
建立socket连接(slave节点:根据保存的信息创建和master连接的socket)
发送ping命令
身份验证
redis设置密码的方式和连接方式
# 配置文件方式
master节点:redis.conf ==> requirepass
slave节点:redis.conf ==> masterauth
# 命令方式
master:config set requirepass
config get requirepass
slave:auth
客户端:redis-cli -a
数据同步阶段(结果:slave包含有master端的全部数据,master端保存有slave当前数据同步的位置)
请求同步数据(slave发送psync2指令)
redis 1.0 :sync 指令
redis 2.8 :psync 指令
redis 4.0 :psync2 指令
创建RDB文件同步数据
恢复RDB同步数据
上述的几个步骤统称为“全量复制”
请求部分同步数据
恢复部分同步数据
“全量复制”后的三步称之为 “增量复制”
命令传播阶段(实时保证数据同步)
当master数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致状态。master将接收到的数据变更命令发送给slave,slave接收命令后执行命令
命令传播阶段中的心跳机制
命令传播阶段,master和slave间的交换信息通过心跳机制进行维护,实现双方连接保持在线
服务器运行ID(runid): 每台服务器的唯一身份标识(同一个服务器每次运行都有不同的runid),40位字符组成,runid在服务器间传输被用作身份标识,master首次连接slave时,会将自己的runid发送给slave,slave保存runid。
复制缓冲区: 一个FIFO(先进先出)队列,用于存储服务器执行过的命令,每次传播命令,master都会将传播的命令记录下来,并保存在复制缓冲区。
复制缓冲区的组成:
复制缓冲区的内部工作原理:
master 通过命令传播程序发送命令到slave节点时,可能会由于网络原因导致一部分slave节点没有接收到命令,通过复制缓冲区保存命令,并通过master节点和slave节点确认偏移量来保证数据的准确同步。
偏移量:同步信息,比对master和slave的差异,当slave断线后,恢复数据使用
psync2 ? -1
# psync2
# slave 节点第一次连接不知道master节点的runid所以为 “?”
# -1 :代表全部
+FULLRESYNC runid offset
# +FULLRESYNC:全量
通过socket发送RDB文件给到slave(发送RDB文件期间offset可能会发生变化)
slave收到 +FULLRESYNC 保存master的runid和offset,清空当前全部数据,通过socket接收RDB文件,恢复RDB文件(2,3,4为全量复制的过程)
slave发送命令
psync2 runid offset
master 接收命令,判断runid是否匹配,判定offset是否在复制缓冲区中
master判断如果 runid和offset有一个不满足,执行全量复制(回退到2节点)
master判断如果 runid和offset校验通过,master.offset 和 slave.offset 相同,忽略
master判断如果 runid和offset校验通过,master.offset 和 slave.offset 不相同,发送
+CONTINUE offset
通过socket发送复制缓冲区中的 master.offset 到 slave.offset 的数据
slave 收到 +CONTINUE 保存master的offset,接收信息后,执行 bgwriteaof 恢复数据(5,6,7,8,9,10 为增量复制的过程)
slave发送心跳命令 replconf ack offset
master判断如果 offset 不满足,执行全量复制(回退到2节点)
master判断如果 offset校验通过,master.offset 和 slave.offset 相同,忽略
master判断如果 offset校验通过,master.offset 和 slave.offset 不相同,发送
+CONTINUE offset
通过socket发送复制缓冲区中的 master.offset 到 slave.offset 的数据
slave 收到 +CONTINUE 保存master的offset,接收信息后,执行 bgwriteaof 恢复数据
数据溢出就会造成复制缓冲区中的数据不完整(数据丢失),不完整的情况下,master和slave进行增量同步时,就不能正常完成。在增量同步不能正常完成的情况下,slave和master会再次进行全量复制。全量复制的过程中,又有大量的信息进入到复制缓冲区导致数据溢出,就会进入一个死循环。
# 调整复制缓冲区的大小
master节点: repl-backlog-size 1mb
多个slave节点同时对master请求数据同步,master发送的RDB文件增多,会对带宽造成巨大冲击,如果master带宽不足,需要适当错峰
slave节点过多时,可以调整拓扑结构变为树状结构,中间的节点既是master又是slave节点(采用树状结构时,由于层级关系,导致深层次的slave节点与最顶层master数据同步延迟较大,数据一致性会变差)
连接slave节点,在slave节点的redis.conf配置文件中添加一行配置
slaveof
# masterip: master节点的ip
# masterport: master节点的port
master_replid : 使用 runid 想用的策略生成,长度41 位,并发送给所有的slave节点(通过 redis-check-rdb [dump文件名] 命令可以查看该信息)
shutdown save 命令:关闭时进行RDB持久化,将runid与offset 保存到RDB文件中,重启时加载RDB文件,恢复数据。
如果从服务器断线重连,会根据新的主服务器id和之前的主服务器id进行对比来判定是否是之前的主服务器来选择进行完整重同步还是部分重同步。
由于网络原因导致slave节点间的数据不一致(slave 中的偏移量不相同)(分布式环境中很常见的问题,是否需要解决看业务容忍度),
可以通过优化主从间的网络环境解决,同机房部署。或者通过监控 主从节点的延迟(比较slave节点的offset差异),暂时屏蔽延迟大的slave节点。(通过 slave-serve-stale-data yes|no ,这个命令执行后,slave节点的数据就不能访问了。)
哨兵也是一台redis服务器,只是不提供数据服务。通常哨兵配置数量为单数()
bind 0.0.0.0
port 26379
dir /tmp # 工作信息的存储目录
sentinel monitor mymaster 192.168.200.128 6379 1
sentinel down-after-milliseconds mymaster 10000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
# mymaster 主节点名,可以任意起名,但必须和后面的配置保持一致。
# 192.168.200.129 6379 主节点连接地址。
# 1 将主服务器判断为失效需要投票,这里设置至少需要 1个 Sentinel 同意。
# sentinel down-after-milliseconds mymaster 10000:设置Sentinel认为服务器已经断线所需的毫秒数。
# sentinel failover-timeout mymaster 60000:设置failover(故障转移)的过期时间。多长时间内同步数据成功的限值
# sentinel parallel-syncs mymaster 1 设置在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步, 这个数字越小,表示同时进行同步的从服务器越少,那么完成故障转移所需的时间就越长。
/redis-sentinel sentinel01.conf # sentinel01.conf:sentinel的配置文件
监控(Monitoring): Sentinel 会不断地检查(ping指令)你的主服务器和从服务器是否运作正常。
提醒(Notification): 当个 Sentinel 监控 Redis服务器的状态,会在多个Sentinel之间进行数据共享
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时,Sentinel会开始一次自动故障转移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器。
故障转移挑选新master的原则:
1. 在线的slave节点
2. 响应快的(与Sentinel响应)
3. 与原master响应频繁的
4. 优先原则(offset小的,runid 小的)
挑选好新的master节点后,Sentinel 向 新master发送 slaveof no one 指令,向其他的slave发送 slaveof [新master的IP] [新master的端口]
概念:主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断
如果一个 redis 服务器没有在 master-down-after-milliseconds 选项所指定的时间内, 对向它发送 PING 命令的 Sentinel 返回一个有效回复, 那么Sentinel 就会将这个服务器标记为主观下线
单个Sentine判断Redis服务器主观下线之后,会通过提醒(流言传播(Gossip))告知其他的Sentinel服务器,其他的Sentinel就来围观这台Redis服务器,超过半数的Sentinel认为Redis主管下线后,则该Redis服务器的状态变为客观下线。
概念:多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断ODOWN。 (一个Sentinel 可以通过向另一个 Sentinel 发送命令来询问对方是否认为给定的服务器已下线)
客观下线条件只适用于主服务器,对于其他类型的 Redis 实例, Sentinel
在将它们判断为下线前不不需要进行协商, 所以从服务器或者其他
Sentinel 不会达到客观下线条件。 只要一个 Sentinel 发现某个主服务器进
入了客观下线状态, 这个Sentinel就可能会被其他 Sentinel 推选出,并对
失效的主服务器执行自动故障迁移操作。
Redis Cluster内置集群,在Redis3.0才推出的实现方案。Redis Cluster是无中心节点的集群架构,依靠Gossip协议(谣言传播(PING-PONG机制))协同自动化修复集群的状态。节点的fail是通过集群中超过半数的节点检测失效时才生效。客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点。
为什么需要三主三从?
因为内部存在一个投票机制,节点的有效性是需要靠投票的,一个节点无法给自己投票,两个节点互相都连不上,A=>B,B=>A互相都说对方挂了没有定论.三个节点才能满足投票的需求
Redis-Cluster中引入虚拟槽分区(哈希槽),共有16384(0~16383)个槽,这些槽平均分配到每个 master 上,在存储数据时利用 CRC16 (slot=CRC16(key)/16384 )算法计算key属于哪个槽。每个master节点都会分配一定槽节点数量,并且其他节点存储的槽范围在节点上也是透明的(内部可知)
需要修改redis.conf 配置文件
cluster-enable yes # 表示这个Redis 是一个cluster节点
cluster-config-file [filename.conf] # cluster的配置文件
cluster-node-timeout 10000 # 节点服务超时时间,用于判定该节点是否下线或切换为从节点
cluster集群中各个redis依次启动完成后,需要使用
./redis-trib.rb create --replicas 1 ip1:port1 ip2:port2 ip3:port3 ip4:port4 ip5:port5 ip6:port6
# create 代表创建集群
# --replicas: 指定内部结构 1:代表一个master连接一个slave节点 ip1:port1->ip4:port4 ip2:port2->ip5:port5 ip3:port3->ip6:port6
# 2: 代表一个master连接两个slave节点 ip1:port1->ip3:port3,ip4:port4 ip2:port2->ip5:port5,ip6:port6
cluster集群搭建好之后,需要注意的是要使用 redis-cli -c 来连接 cluster集群。
Redis-Cluster 支持动态的新增和删除节点
./redis-trib.rb add-node [ip1:port1] [ip2:port],新增成功后我们可以通过 cluster nodes 命令查看集群节点信息,我们可以看到 新增的节点是没有被分配 slot槽的。
cluster setslot 5462 node e4fee6a90d5b071ad514cd30f8d5d9a1c13947b3
cluster setslot [槽] node [目标id]
./redis-trib.rb reshard ip:port
对于已经分配了槽区间的节点,我们是无法直接删除的,需要先将槽区间分配出去,然后再进行删除
./redis-trib.rb reshard ip:port
./redis-trib.rb del-node ip:port 目标id
Twemproxy由Twitter开源,是一个redis和memcache快速/轻量级代理服务器,利用中间件做分片的技术。twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(sharding),再转发给后端真正的redis服务器。
twemproxy主要的角色是代理服务器的作用,是对数据库进行分片操作。twemproxy的分片保证需要存储的数据散列存放在集群的节点上,尽量做到平均分布。如何实现呢,这里就涉及到一致性哈希算法。
判断哈希算法好坏的指标
哈希算法: 哈希算法的哈希函数比较简单,一般是根据某个key的值或者key 的哈希值与当前可用的 master节点数取模,根据取模的值获取具体的服务器
一致性算法:一致性哈希算法可以说是哈希算法的升级版,解决了哈希算法扩展性差的问题,一致性哈希算法跟哈希算法不一样,一致性哈希算法会将服务器和数据都通过哈希函数映射到一个首尾相连的哈希环上,存储节点映射可以根据 ip 地址来进行哈希取值,数据映射到哈希环上后按照顺时针的方向查找存储节点,即从数据映射在环上的位置开始,顺时针方向找到的第一个存储节点,那么他就存储在这个节点上。twemproxy也选择这种算法,解决将key-value均匀分配到众多 server上的问题。它可以取代传统的取模操作,解决了取模操作应对增删 Server的问题。
一部分节点下线之后,虽然剩余机器都在处理请求,但是明显每个机器的负载不不均衡,这样称
为一致性hash的倾斜,虚拟节点的出现就是为了了解决这个问题。在刚才的例子当中,如果Master3节点也挂掉,那么一致性hash倾斜就很明显了:
上面这个例子中,我们可以对已有的两个节点创建虚拟节点,每个节点创建两个虚拟节点。那么实际的Master1节点就变成了两个虚拟节点Master1-1和Master1-2,而另一个实际的Master4节点就变成了两个虚拟节点Master4-1和Master4-2,这个时候数据基本均衡了:
哈希算法: 哈希算法在分布式中的作用,比较简单,可以看出只要你哈希函数设计的好,数据在各个服务器上是比较均匀分布的,但是哈希算法有一个致命的缺点:扩展性特别的差,比如我们的集群中,服务器server3 宕机了,这时候集群中可用的机器只有两台了,这样哈希函数就变成了id % 2了,这就会导致一个问题,所有的数据需要重新计算,找到新的存储节点,每次有服务器宕机或者添加机器时,都需要进行大量的数据迁移,这会使得系统的可用性、稳定性变差。
一致性哈希算法: 一致性哈希会造成数据分布不均匀的问题(数据倾斜)。相比普通哈希算法,能够减少节点扩展时的数据迁移问题
假设有三个master节点
哈希算法:
假设哈希算法中的哈希函数为“id % master 节点数”,结果为 0 的数据存放到 server1 服务器上,结果为 1 的数据存放到 server2 服务器上,结果为 2 的数据存放到 server3 服务器上。
所以经过哈希算法之后,id=3、id=6 的数据与 master 节点数取模为 0 (3%3=0,6%3=0),所以这两个数据会存放到 server1 服务器 ,以此类推,id=1、id=4 的数据将存放到 server2 服务器中,id=2、id=5 的数据将存放到 server3 上。
一致性哈希算法:
按照一致性哈希算法的规则,数据沿着顺时针的方向查找数据,那么 id=4 的数据存放在 server1 服务器,id=2 的数据存放在服务器 server2 上,id=3、id=1、id=5、id=6 的数据都存放在服务器 server3 上,如果你比较敏感的话,也许你就会发现一致性哈希算法的不足之处, 从图中可以看出,我们六条数据分布不均匀,并不是每台服务器存储 2 条数据,而且差距好像还有点大,这里我们就要来说一说一致性哈希算法的缺点:一致性哈希算法会会造成数据分布不均匀的问题或者叫做数据倾斜问题,就像我们图中那样,数据分布不均匀可能会造成某一个节点的负载过大,从而宕机。造成数据分布不均匀有以下两种情况:
第一:哈希函数的原因,经过哈希函数之后服务器在哈希环上的分布不均匀,服务器之间的间距不相等,这样就会导致数据不均匀。
第二:某服务器宕机了,后继节点就需要承受原本属于宕机机器的数据,这样也会造成数据不均匀。
前面我们提到过一致性哈希算法解决了哈希算法中扩展性差的问题,这个怎么理解呢?我们来看看,在一致性哈希算法中当有存储节点加入或者退出时,只会影响应该该节点的后继节点,举个例子说明一下,例如我们要在服务器server3 和服务 server2 之间加入了一个服务器存储节点 server4,只会对服务器server3 造成影响,原本存储到服务器server3 上的数据有一部分会落入到服务器 server4 上,对服务器 server1 和 server2 并没有任何影响,这样就不会进行大量的数据迁移,扩展性就变强了。
redis.conf
# 端口号
port 7000
# 后台启动
daemonize yes
# 开启集群
cluster-enabled yes
#集群节点配置文件
cluster-config-file nodes-7000.conf
# 集群连接超时时间
cluster-node-timeout 5000
# 进程pid的文件位置
pidfile /var/run/redis-7000.pid
# 开启aof
appendonly yes
# aof文件路径
appendfilename "appendonly-7005.aof"
# rdb文件路径
dbfilename dump-7000.rdb
# 是否打开aof⽇志功能(appendonly yes)
appendonly no
# aof⽂件的存放路径与⽂件名称
# appendfilename appendonly.aof
#每⼀个命令,都⽴即同步到aof⽂件中去(很安全,但是速度慢,因为每⼀个命令都会进⾏⼀次磁盘操作)
# appendfsync always
#每秒将数据写⼀次到aof⽂件
appendfsync everysec
#将写⼊⼯作交给操作系统,由操作系统来判断缓冲区⼤⼩,统⼀写到aof⽂件(速度快,但是同步频率低,容易
丢数据)
# appendfsync no
# 在RDB持久化数据的时候,此时的aof操作是否停⽌,若为yes则停⽌
# 在停⽌的这段时间内,执⾏的命令会写⼊内存队列,等RDB持久化完成后,统⼀将这些命令写⼊aof⽂件
# 该参数的配置是考虑到RDB持久化执⾏的频率低,但是执⾏的时间⻓,⽽AOF执⾏的频率⾼,执⾏的时间短,
# 若同时执⾏两个⼦进程(RDB⼦进程、AOF⼦进程)效率会低(两个⼦进程都是磁盘读写)
# 但是若改为yes可能造成的后果是,由于RDB持久化执⾏时间⻓,在这段时间内有很多命令写⼊了内存队列,
# 最后导致队列放不下,这样AOF写⼊到AOF⽂件中的命令可能就少了很多
# 在恢复数据的时候,根据aof⽂件恢复就会丢很多数据
# 所以,选择no就好
no-appendfsync-on-rewrite no
# AOF重写:把内存中的数据逆化成命令,然后将这些命令重新写⼊aof⽂件
# 重写的⽬的:假设在我们在内存中对同⼀个key进⾏了100次操作,最后该key的value是100,
# 那么在aof中就会存在100条命令⽇志,这样的话,有两个缺点:
# 1)AOF⽂件过⼤,占据硬盘空间 2)根据AOF⽂件恢复数据极慢(需要执⾏100条命令)
# 如果我们将内存中的该key逆化成"set key 100",然后写⼊aof⽂件,
# 那么aof⽂件的⼤⼩会⼤幅度减少,⽽且根据aof⽂件恢复数据很快(只需要执⾏1条命令)
# 注意:下边两个约束都要满⾜的条件下,才会发⽣aof重写;
# 假设没有第⼆个,那么在aof的前期,只要稍微添加⼀些数据,就发⽣aof重写
# 当aof的增⻓的百分⽐是原来的100%(即是原来⼤⼩的2倍,例如原来是100m,下⼀次重写是当aof⽂件是20
0m的 时候),AOF重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb #AOF重写仅发⽣在当aof⽂件⼤于64m时
aof-load-truncated yes #如果AOF⽂件结尾损坏,Redis启动时是否仍载⼊AOF⽂件
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务
器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个
功能,大大加快了从服务器下载新邮件的过程
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该
尽可能的将你的数据模型抽象到一个散列表里面。
比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而
是应该把这个用户的所有信息存储到一张散列表里面。
一个客户端运行了新的命令,添加了新的数据。
Redi 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。
使用 keys 指令可以扫出指定模式的 key 列表。
redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指
令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
Codis 是一个分布式 Redis 解决方案