Redis高性能内存数据库相关知识

前言

本篇文章包含Redis应用场景、发布订阅、慢查询分析、持久化、删除策略、高可用方案等等。希望对大家有所帮助。

一、 Redis基础知识

Redis应用场景

1. 缓存使用,减轻DB压力

2. DB使用,用于临时存储数据(字典表,购买记录)

3. 解决分布式场景下Session分离问题(登录信息)

4. 任务队列(秒杀、抢红包等等)

5. 乐观锁

6. 应用排行榜 zset

7. 签到 bitmap

8. 分布式锁

9. 冷热数据交换

缓存的使用场景

缓存原指CPU上的一种高速存储器,它先于内存与CPU交换数据,速度很快,现在泛指存储在计算机上的原始数据的复制集,便于快速访问,以空间换时间的一种技术。

1. DB缓存,减轻DB服务器压力,将已经访问过的内容或数据存储起来,当再次访问时先找缓存,缓存命中返回数据,不命中再找数据库,并回填缓存。

2. 提高系统响应能力,在大量瞬间访问时(高并发)MySQL单机会因为频繁IO而造成无法响应,MySQL的InnoDB是有行锁,将数据缓存在Redis中,也就是存在了内存中。

3. 做Session分离,将登录成功后的Session信息,存放在Redis中,这样多个服务器(Tomcat)可以共享Session信息。

4. 做分布式锁(Redis),多个进程(JVM)在并发时也会产生问题,也要控制时序性,可以采用分布式锁。使用Redis实现setNX。

5. 做乐观锁(Redis),同步锁和数据库中的行锁、表锁都是悲观锁,悲观锁的性能是比较低的,响应性比较差,高性能、高响应(秒杀)采用乐观锁 Redis可以实现乐观锁 watch + incr。

常见缓存的分类

1. 客户端缓存(页面缓存和浏览器缓存、APP缓存)

2. 网络端缓存(Web代理缓存Nginx、边缘缓存CDN)

3. 服务端缓存(数据库级缓存Mysql、平台级缓存EhCache、应用级缓存Redis)

缓存的优缺点

优点:

1. 缓存的使用可以提升系统的响应能力,大大提升了用户体验。

2. 减轻服务器压力

3. 提升系统性能,缩短系统的响应时间,减少网络传输时间和应用延迟时间,提高系统的吞吐量,增加系统的并发用户数,提高了数据库资源的利用率

缺点:

1. 额外的硬件支出,空间换时间

2. 在高并发场景下会出现缓存失效(缓存穿透、缓存雪崩、缓存击穿)

3. 缓存与数据库数据同步,Redis无法做到主从时时数据同步

4. 缓存并发竞争,多个redis的客户端同时对一个key进行set值得时候由于执行顺序引起的并发问题

缓存的读写模式

1. Cache Aside Pattern(读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应,更新的时候,先更新数据库,然后再删除缓存)

直接删除缓存而不是更新缓存,因为缓存是一个hash、list结构,更新需要遍历,代价大,懒加载的时候才需要更新缓存,也就是使用的时候。也可以采用异步的方式填充缓存,开启一个线程,定时将DB的数据刷到缓存中。

2. Read/Write Through Pattern(应用程序只操作缓存,缓存操作数据库)

Read-Through(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入 缓存。 Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。

3. Write Behind Caching Pattern(应用程序只更新缓存)

缓存通过异步的方式将数据批量或合并后更新到DB中 不能时时同步,甚至会丢数据。

缓存高并发脏读的三种情况

1. 先更新数据库,再更新缓存,导致update与commit之间,更新缓存,commit失败 则DB与缓存数据不一致。

2. 先删除缓存,再更新数据库,导致update与commit之间,有新的读,缓存空,读DB数据到缓存数据是旧的数据。

3. 先更新数据库,再删除缓存(推荐),update与commit之间,有新的读,缓存空,读DB数据到缓存 数据是旧的数据 commit后 DB为新数据 则DB与缓存数据不一致 采用延时双删策略,也就是先更新数据库,再删除缓存,再设定一个定时时间,大概是300ms以内,再删除一次缓存,就算第一次读到了脏数据,第二次再读就能保证缓存与数据库一致。

缓存的架构设计

1. 多层次,分布式缓存宕机,本地缓存还可以使用

2. 数据类型,简单类型用Memcached,复杂类型用Redis

3. 做集群

4. 缓存的数据结构设计,缓存的字段会比数据库表少一些,缓存的数据是经常访问的

Redis安装

第一步:安装 C 语言需要的 GCC 环境

yum install -y gcc-c++

yum install -y wget

第二步:下载并解压缩 Redis 源码压缩包

wget http://download.redis.io/releases/redis-5.0.5.tar.gz

tar -zxf redis-5.0.5.tar.gz

第三步:编译 Redis 源码,进入 redis-5.0.5 目录,执行编译命令

cd redis-5.0.5/src

make

第四步:安装 Redis ,需要通过 PREFIX 指定安装路径

mkdir /usr/redis -p

make install PREFIX=/usr/redis

Redis启动命令

前端启动

启动命令: ./redis-server

关闭命令: ctrl+c

客户端窗口关闭则 redis-server 程序结束

后台启动(守护进程启动)

第一步:拷贝 redis-5.0.5/redis.conf 配置文件到 Redis 安装目录的 bin 目录

cd redis-5.0.5/

cp redis.conf /usr/redis/bin/

第二步:修改 redis.conf

vim redis.conf

# 将`daemonize`由`no`改为`yes`

daemonize yes

# 默认绑定的是回环地址,默认不能被其他机器访问

# bind 127.0.0.1

# 是否开启保护模式,由yes该为no

protected-mode no

第三步:启动服务

./redis-server redis.conf

第四步:后端启动的关闭方式

./redis-cli shutdown

第五步:关闭RedisServer端的防火墙

systemctl stop firewalld(默认)

systemctl disable firewalld.service(设置开启不启动)

systemctl status firewalld.service(查看防火墙是否关闭)

Redis云服务器端口开放访问不到解决办法

1.开启防火墙:systemctl start firewalld.service2.添加端口:firewall-cmd –zone=public –add-port=6379/tcp –permanent3.重启防火墙:firewall-cmd –reload

Redis数据类型

Redis是一个Key-Value的存储系统,使用ANSI C语言编写。

key的类型是字符串。

value的数据类型有: 常用的:string字符串类型、list列表类型、set集合类型、sortedset(zset)有序集合类型、hash类 型。 不常见的:bitmap位图类型、geo地理位置类型,Redis5.0新增:stream类型。

Redis中命令是忽略大小写,key是不忽略大小写的

Redis中Key的设计:1. 用:分割 2. 把表名转换为key前缀, 比如: user: 3. 第二段放置主键值 4. 第三段放置列名

比如:username 的 key:user:9:username对应{userid:9,username:zhangf}

string字符串类型

Redis的String能表达3种值的类型:字符串、整数、浮点数 100.01 是个六位的串

1、key和命令是字符串

2、普通的赋值

3、incr用于乐观锁 incr:递增数字,可用于实现乐观锁 watch(事务)

4、setnx用于分布式锁

list列表类型

list列表类型可以存储有序、可重复的元素

1、作为栈或队列使用 列表有序可以作为栈和队列使用

2、可用于各种列表,比如用户列表、商品列表、评论列表等。

set集合类型

Set:无序、唯一元素,适用于不能重复的且不需要顺序的数据结构

sortedset有序集合类型

SortedSet(ZSet) 有序集合: 元素本身是无序不重复的 每个元素关联一个分数(score) 可按分数排序,分数可重复

hash类型(散列表)

Redis hash 是一个 string 类型的 field 和 value 的映射表,它提供了字段和字段值的映射。

bitmap位图类型

bitmap是进行位操作的 通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。

1、用户每月签到,用户id为key , 日期作为偏移量 1表示签到

2、统计活跃用户, 日期为key,用户id为偏移量 1表示活跃

3、查询用户在线状态, 日期为key,用户id为偏移量 1表示在线

geo地理位置类型

geo是Redis用来处理位置信息的。在Redis3.2中正式使用。主要是利用了Z阶曲线、Base32编码和 geohash算法。

1、记录地理位置

2、计算距离

3、查找"附近的人"

stream数据流类型

stream是Redis5.0后新增的数据结构,用于可持久化的消息队列。

二、Redis扩展功能

发布与订阅

Redis提供了发布订阅功能,可以用于消息的传输,包括三个部分,publisher,subscriber和Channel

发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。

发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。

在Redis哨兵模式中,哨兵通过发布与订阅的方式与Redis主服务器和Redis从服务器进行通信。

事务特性

ACID特性与redis事务比较

1.Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。

Redis:一个队列中的命令 执行或不执行

2.Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。

Redis: 集群中不能保证时时的一致性,只能是最终一致性

3.Isolation(隔离性):事务之间不会相互影响。

Redis: 命令是顺序执行的,在一个事务中,有可能被执行其他客户端的命令的

4.Durability(持久性):事务执行成功后必须全部写入磁盘。

Redis有持久化但不保证数据的完整性

Redis事务

Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。

Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。

Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行

Redis不支持回滚操作

事务命令

multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列

exec:执行命令队列

discard:清除命令队列

watch:监视key,如果监视中发现key变值了清空队列

unwatch:清除监视key

事务机制

事务的执行

1. 事务开始 在RedisClient中,有属性flags,用来表示是否在事务中 flags=REDIS_MULTI

2. 命令入队 RedisClient将命令存放在事务队列中 (EXEC,DISCARD,WATCH,MULTI除外)

3. 事务队列 multiCmd *commands 用于存放命令

4. 执行事务 RedisClient向服务器端发送exec命令,RedisServer会遍历事务队列,执行队列中的命令,最后将执 行的结果一次性返回给客户端。

5. 如果某条命令在入队过程中发生错误,redisClient将flags置为REDIS_DIRTY_EXEC,EXEC命令将会失败 返回。

Watch的执行

redisDb有一个watched_keys字典,key是某个被监视的数据的key,值是一个链表.记录了所有监视这个数 据的客户端。

监视机制的触发

当修改数据后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS事务执行 RedisClient向服务器端发送exec命令,服务器判断RedisClient的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。

Redis不支持事务回滚的原因

1、大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的

2、Redis为了性能方面就忽略了事务回滚。

Lua脚本

从Redis2.6.0版本开始,通过内置的lua编译/解释器,可以使用EVAL命令对lua脚本进行求值。

脚本的命令是原子的,RedisServer在执行脚本命令中,不允许插入新的命令

脚本的命令可以复制,RedisServer在获得脚本后不执行,生成标识返回,Client根据标识就可以随时执行

EVAL命令

命令说明:

script参数:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该) 定义为一个Lua函数。

numkeys参数:用于指定键名参数的个数。

key [key …]参数: 从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中 所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形 式访问( KEYS[1] , KEYS[2] ,以此类推)。

arg [arg …]参数:可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

举例:eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

lua脚本中调用Redis命令:eval "return redis.call('set',KEYS[1],ARGV[1])" 1 n1 zhaoyun

EVALSHA命令

EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体,为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但 它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)。

SCRIPT FLUSH :清除所有脚本缓存

SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存

SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它

SCRIPT KILL :杀死当前正在运行的脚本

举例:

script load "return redis.call('set',KEYS[1],ARGV[1])"-> "c686f316aaf1eb01d5a4de1b0b63cd233010e63d"

->evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 n2 zhangfei

其实就是把LUA命令进行缓存并返回一段唯一的sha码,通过码来调用脚本执行命令

脚本复制

Redis 传播 Lua 脚本,在使用主从模式和开启AOF持久化的前提下,当执行lua脚本时,Redis 服务器有两种模式:脚本传播模式和命令传播模式。

脚本传播模式是 Redis 复制脚本时默认使用的模式 Redis会将被执行的脚本及其参数复制到 AOF 文件以及从服务器里面。

命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到 AOF 文件以及从服务器里面。

管道(pipeline),事务和脚本(lua)三者的区别

三者都可以批量执行命令

管道无原子性,命令都是独立的,属于无状态的操作

事务和脚本是有原子性的,其区别在于脚本可借助Lua语言可在服务器端存储的便利性定制和简化操作

脚本的原子性要强于事务,脚本执行期间,另外的客户端 其它任何脚本或者命令都无法执行,脚本的执行时间应该尽量短,不能太耗时的脚本。

慢查询日志和监视器

Redis慢查询日志用于监视和优化查询

1、尽量使用短的key,对于value有些也可精简,能使用int就int。

2、避免使用keys *、hgetall等全量操作。

3、减少大key的存取,打散为小key 100K以上

4、将rdb改为aof模式 rdb fork 子进程 数据量过大 主进程阻塞 redis性能大幅下降 关闭持久化 , (适合于数据量较小,有固定数据源)

5、想要一次添加多条数据的时候可以使用管道

6、尽可能地使用哈希存储

7、尽量限制下redis使用的内存大小,这样可以避免redis使用swap分区或者出现OOM错误 内存与硬盘的swap

监视器

Redis客户端通过执行MONITOR命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理的命令请求的相关信息。

三、Redis核心原理

Redis持久化

Redis是内存数据库,宕机后数据会消失。Redis重启后快速恢复数据,要提供持久化机制,Redis持久化是为了快速的恢复数据而不是为了存储数据,Redis有两种持久化方式:RDB和AOF。

RDB特性

RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照( snapshotting )完成的。关注的是这一刻的数据,也就是跟拍照一样,抓拍这一刻,不管前后。

在redis.conf中配置:save 多少秒内 数据变了多少,采用漏洞设计,提升性能。

1. Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(aof文件重写命令)的子进程,如果在执行则bgsave命令直接返回。

2. 父进程执行fork(调用OS函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令。

3. 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令。

4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。 (RDB始终完整)

5. 子进程发送信号给父进程表示完成,父进程更新统计信息。

6. 父进程fork子进程后,继续工作。

优点

RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)

主进程fork子进程,可以最大化Redis性能,主进程不能太大,Redis的数据量不能太大,复制过程中主进程阻塞

缺点

不保证数据完整性,会丢失最后一次快照以后更改的所有数据

AOF特性

AOF(append only file)是Redis的另一种持久化方式。Redis默认情况下是不开启的。开启AOF持久化后 Redis 将所有对数据库进行过写入的命令(及其参数)记录到 AOF文件, 以此达到记录数据库状态的目的,这样当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。 AOF会记录过程,RDB只管结果。

在redis.conf中配置

AOF原理

AOF文件中存储的是redis的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:

命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。

缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加 到服务器的 AOF 缓存中。

文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。

AOF 保存模式

Redis 目前支持三种 AOF 保存模式,它们分别是:

AOF_FSYNC_NO :不保存。

AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)

AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)

Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写,Redis 不希望 AOF 重写造成服务器无法处理请求,所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行,子进程在进行 AOF重写期间,主进程还需要继续处理命令,而新的命令可能对现有的数据进行修改,因此Redis 增加了一个 AOF 重写缓存。

混合持久化

RDB和AOF各有优缺点,Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开,aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。在加载时,首先会识别AOF文件是否以 REDIS字符串开头,如果是就按RDB格式加载,加载完RDB后继续按AOF格式加载剩余部分。

RDB与AOF对比

1、RDB存某个时刻的数据快照,采用二进制压缩存储,AOF存操作命令,采用文本存储(混合)

2、RDB性能高、AOF性能较低

3、RDB在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多 丢2秒的数据

4、Redis以主服务器模式运行,RDB不会保存过期键值对数据,Redis以从服务器模式运行,RDB会保 存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。

应用场景

内存数据库 rdb+aof 数据不容易丢

有原始数据源:每次启动时都从原始数据源中初始化 ,则不用开启持久化

缓存服务器 rdb 一般性能高

在数据还原时 有rdb+aof 则还原aof,因为RDB会造成文件的丢失,AOF相对数据要完整。

底层数据结构

Redis没有表的概念,Redis实例所对应的db以编号区分,db本身就是key的命名空间。

RedisDB结构体源码:

RedisObject结构:

字符串对象

Redis 使用了 SDS(Simple Dynamic String)。用于存储字符串和整型数据。SDS 在 C 字符串的基础上加入了 free 和 len 字段,SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。

跳跃表

跳跃表是有序集合(sorted-set)的底层实现,效率高,实现简单。

字典(核心)

字典dict又称散列表(hash),是用来存储键值对的一种数据结构。 Redis整个数据库是用字典来存储的。(K-V结构) 对Redis进行CURD操作其实就是对字典中的数据进行CURD操作。

数组

数组:用来存储数据的容器,采用头指针+偏移量的方式能够以O(1)的时间复杂度定位到数据所在的内存地址,海量存储效率高的缘由。

Hash函数

Hash(散列),作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值。 hash函数可以把Redis里的key:包括字符串、整数、浮点数统一转换成整数,算出数组下标进行存储。

Redis字典实现包括:字典(dict)、Hash表(dictht)、Hash表节点(dictEntry)。

字典达到存储上限(阈值 0.75),需要rehash(扩容)

1. 初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。

2. rehashidx=0表示要进行rehash操作。

3. 新增加的数据在新的hash表h[1]

4. 修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash中)

5. 将老的hash表h[0]的数据重新计算索引值后全部迁移到新的hash表h[1]中,这个过程称为 rehash。

应用场景:

1、主数据库的K-V数据存储

2、散列表对象(hash)

3、哨兵模式中的主从节点管理

压缩列表

压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构,节省内存,是一个字节数组,可以包含多个节点(entry)。每个节点可以保存一个字节数组或一个整数。

应用场景:

sorted-set和hash元素个数少且是小整数或短字符串(直接使用)

list用快速链表(quicklist)数据结构存储,而快速链表是双向列表与压缩列表的组合。(间接使用)

整数集合

整数集合(intset)是一个有序的(整数升序)、存储整数的连续存储结构。

快速列表

快速列表(quicklist)是Redis底层重要的数据结构。是列表的底层实现。(在Redis3.2之前,Redis采用双向链表(adlist)和压缩列表(ziplist)实现。)在Redis3.2以后结合adlist和ziplist的优势Redis设计出了quicklist。

双向列表(adlist):

1. 双向:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。

2. 普通链表(单链表):节点类保留下一节点的引用。链表类只保留头节点的引用,只能从头节点插 入删除

3. 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结 束。 环状:头的前一个节点指向尾节点

4. 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。

5. 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。

快速列表quicklist是一个双向链表,链表中的每个节点时一个ziplist结构。quicklist中的每个节点ziplist都能够存 储多个数据元素。

数据压缩(ziplist):

quicklist每个节点的实际数据存储结构为ziplist,这种结构的优势在于节省存储空间。为了进一步降低 ziplist的存储空间,还可以对ziplist进行压缩。Redis采用的压缩算法是LZF。其基本思想是:数据与前 面重复的记录重复位置及长度,不重复的记录原始数据。

stream流对象

stream主要由:消息、生产者、消费者和消费组构成。

Redis Stream的底层主要使用了listpack(紧凑列表)和Rax树(基数树)。

listpack表示一个字符串列表的序列化,listpack可用于存储字符串或整数。用于存储stream的消息内容。

Rax 是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。

缓存过期和淘汰策略

Redis长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满 内存与硬盘交换(swap)虚拟内存 ,频繁IO 性能急剧下降。

Redis默认缓存淘汰策略:禁止驱逐

Maxmemory最大内存

不设置场景

Redis作为DB使用,保证数据的完整性,不能淘汰 ,可以做集群,横向扩展

设置的场景

Redis是作为缓存使用,不断增加Key maxmemory : 默认为0 不限制

设置maxmemory后,当趋近maxmemory时,通过缓存淘汰策略,从内存中删除对象,一般是物理内存的3/4

expire数据结构

在Redis中可以使用expire命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除。

删除策略之定时删除

在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。 需要创建定时器,而且消耗CPU,一般不推荐使用。

删除策略之惰性删除

在key被访问时如果发现它已经失效,那么就删除它。

删除策略之主动删除

在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)

maxmemory-policy allkeys-lru

LRU算法

最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想 是“如果数据最近被访问过,那么将来被访问的几率也更高”。

1. 新数据插入到链表头部;

2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

3. 当链表满的时候,将链表尾部的数据丢弃。

4. 在Java中可以使用LinkHashMap(哈希链表)去实现LRU

volatile-lru 从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

LFU算法

LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

Random

volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-random 从数据集(server.db[i].dict)中任意选择数据淘汰

ttl

从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。

volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

缓存淘汰策略的选择

allkeys-lru : 在不确定时一般采用策略。 冷热数据交换

volatile-lru : 比allkeys-lru性能差 存 : 过期时间

allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)

自己控制:volatile-ttl 缓存穿透

通信协议之请求协议响应

Redis是单进程单线程的。 应用系统和Redis通过Redis协议(RESP)进行交互。

Redis协议位于TCP层之上,即客户端和Redis实例保持双工的连接。

串行的请求响应模式(ping-pong)

串行化是最简单模式,客户端与服务器端建立长连接 连接通过心跳机制检测(ping-pong) ack应答 客户端发送请求,服务端响应,客户端收到响应后,再发起第二个请求,服务器端再响应。telnet和redis-cli 发出的命令 都属于该种模式,性能较低。

双工的请求响应模式(pipeline)

批量请求,批量响应 请求响应交叉进行,不会混淆(TCP双工)

pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回。

通过pipeline,一次pipeline(n条命令)=一次网络时间 + n次命令时间

原子化的批量请求响应模式(事务)

Redis可以利用事务机制批量执行命令。

发布订阅模式(pub/sub)

发布订阅模式是:一个客户端触发,多个客户端被动接收,通过服务器中转。

脚本化的批量执行(lua)

客户端向服务器端提交一个lua脚本,服务器端执行该脚本。

请求数据格式

内联格式:可以使用telnet给Redis发送命令,首字符为Redis命令名的字符,格式为 str1 str2 str3…

规范格式(redis-cli) RESP:

1、间隔符号,在Linux下是rn,在Windows下是n

2、简单字符串 Simple Strings, 以 "+"加号 开头

3、错误 Errors, 以"-"减号 开头

4、整数型 Integer, 以 ":" 冒号开头

5、大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制512M

6、数组类型 Arrays,以 "*"星号开头

通信协议之命令处理流程

整个流程包括:服务器启动监听、接收命令请求并解析、执行命令请求、返回命令回复等。

事件处理机制之文件事件

Redis服务器是典型的事件驱动系统。

文件事件即Socket的读写事件,也就是IO事件。客户端的连接、命令请求、数据回复、连接断开。

socket 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据。

Reactor Redis事件处理机制采用单线程的Reactor模式,属于I/O多路复用的一种常见模式。

IO多路复用( I/O multiplexing )指的通过单个线程管理多个Socket。

Reactor pattern(反应器设计模式)是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。

Reactor模式是事件驱动的,也就是文件事件。

有一个Service Handler,有多个Request Handlers,这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。

4种IO多路复用模型

select,poll,epoll、kqueue都是IO多路复用的机制。 I/O多路复用就是通过一种机制,一个进程可以监视多个描述符(socket),一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

Select

调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时 (timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fd列表,来找到就绪的描述符,最大监听1024,采用的是线性扫描的方法,即不管这些socket是不是活 跃的,都轮询一遍,效率比较低。

Poll

poll使用一个 pollfd的指针实现,pollfd结构包含了要监视的event和发生的event,不再使用select“参 数-值”传递的方式,没有1024限制,但是仍然采用的是线性扫描的方法,即不管这些socket是不是活跃的,都轮询一遍,效率比较低。。

Epoll

epoll是在linux2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更 加灵活,没有描述符限制,并且不会采用线性扫描,只扫描活跃的。

Kqueue

kqueue 是 unix 下的一个IO多路复用库。最初是2000年Jonathan Lemon在FreeBSD系统上开发的一个 高性能的事件通知接口。注册一批socket描述符到 kqueue 以后,当其中的描述符状态发生变化时,kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了。能处理大量数据,性能较高。

事件处理机制之时间事件

时间事件分为定时事件与周期事件:

id(全局唯一id)

when (毫秒时间戳,记录了时间事件的到达时间)

serverCron

时间事件的最主要的应用是在redis服务器需要对自身的资源与配置进行定期的调整,从而确保服务器的 长久运行,这些操作由redis.c中的serverCron函数实现。该时间事件主要进行以下操作:

1)更新redis服务器各类统计信息,包括时间、内存占用、数据库占用等情况。

2)清理数据库中的过期键值对。

3)关闭和清理连接失败的客户端。

4)尝试进行aof和rdb持久化操作。

5)如果服务器是主服务器,会定期将数据向从服务器做同步操作。

6)如果处于集群模式,对集群定期进行同步与连接测试操作。

timeProc(时间事件处理器,当时间到达时,Redis就会调用相应的处理器来处理事件)

定时事件

定时事件:让一段程序在指定的时间之后执行一次 aeTimeProc(时间处理器)的返回值是AE_NOMORE 该事件在达到后删除,之后不会再重复。

周期性事件

周期性事件:让一段程序每隔指定时间就执行一次 aeTimeProc(时间处理器)的返回值不是AE_NOMORE 当一个时间事件到达后,服务器会根据时间处理器的返回值,对时间事件的 when 属性进行更新,让这 个事件在一段时间后再次达到。 serverCron就是一个典型的周期性事件。

aeEventLoop

aeEventLoop 是整个事件驱动的核心,Redis自己的事件处理机制 它管理著文件事件表和时间事件列表, 不断地循环处理著就绪的文件事件和到期的时间事件。

aeProcessEvent

首先计算距离当前时间最近的时间事件,以此计算一个超时时间;然后调用 aeApiPoll 函数去等待底层的I/O多路复用事件就绪;aeApiPoll 函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。

四、Redis企业应用

JVM缓存

JVM缓存就是本地缓存,设计在应用服务器中(tomcat)。 通常可以采用Ehcache和Guava Cache,在互联网应用中,由于要处理高并发,通常选择Guava Cache。

适用本地(JVM)缓存的场景:

1、对性能有非常高的要求。

2、不经常变化

3、占用内存不大

4、有访问整个集合的需求

5、数据允许不时时一致

文件缓存

这里的文件缓存是基于http协议的文件缓存,一般放在nginx中。

静态文件(比如css,js, 图片)中,很多都是不经常更新的。nginx使用proxy_cache将用户的请 求缓存到本地一个目录。下一个相同请求可以直接调取缓存文件,就不用去请求服务器了。

Redis缓存

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到110000+的QPS(每秒内查询次数)。80000的写

分布式缓存,采用主从+哨兵或RedisCluster的方式缓存数据库的数据。 在实际开发中作为数据库使用,数据要完整 作为缓存使用,作为Mybatis的二级缓存使用

缓存大小设置

GuavaCache的缓存设置方式:

CacheBuilder.newBuilder().maximumSize(num) // 超过num会按照LRU算法来移除缓存

Nginx的缓存设置方式:

http { …

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

server { proxy_cache mycache;

location / { proxy_pass http://localhost:8000; }

}

}

Redis缓存设置:

maxmemory=num # 最大缓存量 一般为内存的3/4

maxmemory-policy allkeys lru #

key数量:一个key或是value大小最大是512M

命中率

1、缓存的数量越少命中率越高,比如缓存单个对象的命中率要高于缓存集合

2、过期时间越长命中率越高

3、缓存越大缓存的对象越多,则命中的越多

过期策略

Redis的过期策略是定时删除+惰性删除

缓存预热

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询 数据库,然后再将数据缓存的问题!用户直接查询实现被预热的缓存数据。

加载缓存思路: 数据量不大,可以在项目启动的时候自动进行加载 利用定时任务刷新缓存,将数据库的数据刷新到缓存中

缓存问题之缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如 DB)。 缓存穿透是指在高并发下查询key不存在的数据(不存在的key),会穿过缓存查询数据库。导致数据库 压力过大而宕机。

解决方案:

对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或者该key对应的数据insert了之后清理缓存。 问题:缓存太多空值占用了更多的空间

使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,存在再查缓存和DB。

布隆过滤器的原理:当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K 个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。多个K是为了避免hash碰撞。

缓存问题之缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如 DB)带来很大压力。 突然间大量的key失效了或redis重启,大量访问数据库,数据库崩溃

解决方案:

1、key的失效期分散开不同的key设置不同的有效期

2、设置二级缓存(数据不一定一致)

3、高可用(脏读)

缓存问题之缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热 点”的数据。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓 存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案:

1、用分布式锁控制访问的线程 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数 据库。

2、不设超时时间,volatile-lru 但会造成写一致问题

当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的 不一致,应用会从缓存中读取到脏数据。可采用延时双删策略处理。

缓存问题之数据不一致

缓存和DB的数据不一致的根源 : 数据源不一样,强一致性很难,追求最终一致性(时间)。

保证数据的最终一致性(延时双删)

1、先更新数据库同时删除缓存项(key),等读的时候再填充缓存

2、2秒后再删除一次缓存项(key)

3、设置缓存过期时间 Expired Time 比如 10秒 或1小时

4、将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长 7*24)

缓存问题之数据并发竞争

这里的并发指的是多个redis的client同时set 同一个key引起的并发问题。多客户端(Jedis)同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。

第一种方案:分布式锁+时间戳

准备一个分布式锁,大家去抢锁,抢到锁就做set操作。加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。

.Redis分布式锁的实现:主要用到的redis函数是setnx() ,通过SETNX实现分布式锁

第二种方案:利用消息队列

在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。 把Redis的set操作放在队列中使其串行化,必须的一个一个执行。

Hot Key

当有大量的请求(几十万)访问某个Redis某个key时,由于流量集中达到网络上限,从而导致这个redis的 服务器宕机。造成缓存击穿,接下来对这个key的访问将直接访问数据库造成数据库崩溃,或者访问数 据库回填Redis再访问Redis,继续崩溃。

如何发现热key

1、预估热key,比如秒杀的商品、火爆的新闻等

2、在客户端进行统计,实现简单,加一行代码即可

3、如果是Proxy,比如Codis,可以在Proxy端收集

4、利用Redis自带的命令,monitor、hotkeys。但是执行缓慢(不要用)

5、利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如 Storm、Spark Streaming、Flink,这些技术都是可以的。发现热点数据后可以写到zookeeper中。

如何处理热Key:

1、变分布式缓存为本地缓存 发现热key后,把缓存数据取出后,直接加载到本地缓存中。可以采用Ehcache、Guava Cache都可以,这样系统在访问热key数据时就可以直接访问自己的缓存了。(数据不要求时时一致)

2、在每个Redis主节点上备份热key数据,这样在读取时可以采用随机读取的方式,将访问压力负载到 每个Redis上。

3、利用对热点数据访问的限流熔断保护措施,每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。

Big Key

大key指的是存储的值(Value)非常大,比如热门话题下的讨论 大V的粉丝列表 序列化后的图片等等

造成的问题:

大key会大量占用内存,在集群中无法均衡

Redis的性能下降,主从复制异常

在主动删除或过期删除时会操作时间过长而引起服务阻塞

解决办法

1、 string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。

2、 hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。

3、 使用 lazy delete(unlink命令)删除,该命令会在另一个线程中 回收内存,因此它是非阻塞的。

缓存与数据库的一致性

缓存更新策略

利用Redis的缓存淘汰策略被动更新 LRU 、LFU

利用TTL被动更新

在更新数据库时主动更新 (先更数据库再删缓存—-延时双删)

异步更新 定时任务 数据不保证时时一致 不穿DB

Redis乐观锁

利用Watch实现Redis乐观锁

乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消 耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。

1、利用redis的watch功能,监控这个redisKey的状态值

2、获取redisKey的值

3、创建redis事务

4、给这个key的值+1

5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1

Redis分布式锁

Setnx

共享资源互斥

共享资源串行化

单应用中使用锁:(单进程多线程) synchronized、ReentrantLock

分布式应用中使用锁:(多进程多线程)。 利用Redis的单线程特性对共享资源进行串行化处理。

方式1(使用set命令实现)–推荐

方式2(使用setnx命令实现) — 并发会产生问题

释放锁 方式1(del命令实现) — 并发

释放锁 方式2(redis+lua脚本实现)–推荐

分布式锁是CP模型,Redis集群是AP模型。 (base) Redis集群不能保证数据的随时一致性,只能保证数据的最终一致性。

为什么还可以用Redis实现分布式锁?

与业务有关 当业务不需要数据强一致性时,比如:社交场景,就可以使用Redis实现分布式锁 当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账)就不要使用 可以使用CP模型实现,比如:zookeeper和etcd。

Redisson分布式锁的实现原理

如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。 发送lua脚本到redis服务器上。

那么在这个时候,如果客户端2来尝试加锁,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。 接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不 是的,因为那里包含的是客户端1的ID。此时客户端2会进入一个while循环,不停的尝试加锁。只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一 下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

分布式锁特性

互斥性

任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。

同一性

锁只能被持有该锁的客户端删除,不能由其它客户端删除。

可重入性

持有某个锁的客户端可继续对该锁加锁,实现锁的续租 容错性 锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁

Zookeeper分布式锁的对比

五、Redis高可用方案

主从复制及实战

“高可用性”(High Availability)通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服 务的高度可用性。CAP的A AP模型 单机的Redis是无法保证高可用性的,当Redis服务器宕机后,即使在有持久化的机制下也无法保证不丢失数据。所以我们采用Redis多机和集群的方式来保证Redis的高可用性。

主从配置

主Redis配置无需配置

从Redis配置修改从服务器上的 redis.conf 文件:

作用:读写分离、数据容灾

原理与实现:

保存主节点信息,然后slaver与master建立socket连接,slaver关联文件事件处理器,该处理器接收RDB文件(全量复制)、接收Master传播来的写命令(增量复制),Slaver向Master发送ping命令,Master的响应:发送“pong” , 说明正常,主从正常连接后,进行权限验证,在身份验证步骤之后,从服务器将执行命令REPLCONF listening-port ,向主服务器发送从服务器的监听端口号,然后开始同步数据,当同步数据完成后,主从服务器就会进入命令传播阶段,主服务器只要将自己执行的写命令发送给从服务器,而从服务器只要一直执行并接收主服务器发来的写命令。

同步数据集

在Redis 2.8之后使用PSYNC命令,具备完整重同步和部分重同步模式。 Redis 的主从同步,分为全量同步和增量同步。 只有从机第一次连接上主机是全量同步。断线重连有可能触发全量同步也有可能是增量同步( master 判断 runid 是否一致)。除此之外的情况都是增量同步。

Redis 的全量同步过程主要分三个阶段:同步快照阶段、同步写缓冲阶段、同步增量阶段

增量同步:Redis增量同步主要指Slave完成初始化后开始正常工作时, Master 发生的写操作同步到 Slave 的 过程。 通常情况下,Master 每执行一个写命令就会向 Slave 发送相同的写命令,然后 Slave 接收并执行。

心跳检测

在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送命令,进行心跳检测。

1. 检测主从的连接状态

2. 辅助实现min-slaves

3. 检测命令丢失

主从配置实战

第一步:创建master主、salver从文件夹

mkdir redis-master

mkdir redis-slaver1

mkdir redis-slaver2

第二步:进入redis安装目录安装redis

cd /usr/local/redis-5.0.5/src/

make install PREFIX=/var/redis-ms/redis-master

第三步:拷贝redis.conf到master

cp /usr/local/redis-5.0.5/redis.conf /var/redis-ms/redis-master/bin

第四步:编辑redis.conf

vim redis.conf

# 将`daemonize`由`no`改为`yes` daemonize yes

# bind 127.0.0.1

# 是否开启保护模式,由yes该为no protected-mode no

第五步:把所有内容拷贝到从salver文件夹

cp -r /var/redis-ms/redis-master/* /var/redis-ms/redis-slaver1cp -r /var/redis-ms/redis-master/* /var/redis-ms/redis-slaver2

第六步:修改两从的redis.conf配置文件

cd /var/redis-ms/redis-slaver1/bin

vim redis.conf

#端口改为6380 6381

replicaof 127.0.0.1 6379 #添加这段配置,指定主服务器

第七步:启动所有主从节点

./redis-server redis.conf

哨兵模式及实战

哨兵(sentinel)是Redis的高可用性(High Availability)的解决方案: 由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。 当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服 务,从而保证redis的高可用性。

哨兵模式实战

第一步:创建哨兵sentinel节点文件夹

mkdir redis-sentinel1

mkdir redis-sentinel2

mkdir redis-sentinel3

第二步:拷贝redis到sentinel文件夹

cp -r /var/redis-ms/redis-master/* /var/redis-ms/redis-sentinel1

cp -r /var/redis-ms/redis-master/* /var/redis-ms/redis-sentinel2

cp -r /var/redis-ms/redis-master/* /var/redis-ms/redis-sentinel2

第三步:拷贝sentinel.conf 配置文件并修改

cp /usr/local/redis-5.0.5/sentinel.conf /var/redis-ms/redis-sentinel1/bin/

cp /usr/local/redis-5.0.5/sentinel.conf /var/redis-ms/redis-sentinel2/bin/

cp /usr/local/redis-5.0.5/sentinel.conf /var/redis-ms/redis-sentinel3/bin/

vim sentinel.conf

# 哨兵sentinel实例运行的端口 默认26379

# 将`daemonize`由`no`改为`yes`

# 哨兵sentinel监控的redis主节点的 ip port

# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。

# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了

# sentinel monitor

sentinel monitor mymaster 127.0.0.1 6379 2

# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提 供密码

# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码

# sentinel auth-pass

sentinel auth-pass mymaster MySUPER–secret-0123passw0rd

# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒,改成3 秒

# sentinel down-after-milliseconds

sentinel down-after-milliseconds mymaster 3000

第四步:依次启动redis主从客户端和哨兵sentinel客户端

Redis主从启动:./redis-server redis.conf

Sentinel哨兵服务启动:./redis-sentinel sentinel.conf

检测原理

获取主服务器信息:Sentinel默认每10s一次,向被监控的主服务器发送info命令,获取主服务器和其下属从服务器的信息。

获取从服务器信息:当Sentinel发现主服务器有新的从服务器出现时,Sentinel还会向从服务器建立命令连接和订阅连接。 在命令连接建立之后,Sentinel还是默认10s一次,向从服务器发送info命令,并记录从服务器的信息。

向主服务器和从服务器发送消息(以订阅的方式):默认情况下,Sentinel每2s一次,向所有被监视的主服务器和从服务器所订阅的—sentinel—:hello频道 上发送消息,消息中会携带Sentinel自身的信息和主服务器的信息。

接收来自主服务器和从服务器的频道信息:当Sentinel与主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送命令。

检测主观下线状态:Sentinel每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他Sentinel)发送PING命 令 实例在down-after-milliseconds毫秒内返回无效回复(除了+PONG、-LOADING、-MASTERDOWN外) 实例在down-after-milliseconds毫秒内无回复(超时) Sentinel就会认为该实例主观下线(SDown)。

检查客观下线状态:当一个Sentinel将一个主服务器判断为主观下线后 Sentinel会向同时监控这个主服务器的所有其他Sentinel发送查询命令判断它们是否也认为主服务器下线。如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服 务器为主观下线,则该主服务器就会被判定为客观下线(ODown)。

故障转移

选举Leader Sentinel:当一个主服务器被判定为客观下线后,监视这个主服务器的所有Sentinel会通过选举算法(raft),选 出一个Leader Sentinel去执行failover(故障转移)操作。

Raft协议是用来解决分布式系统一致性问题的协议。 Raft协议描述的节点共有三种状态:Leader, Follower, Candidate。

选举流程: Raft采用心跳机制触发Leader选举 系统启动后,全部节点初始化为Follower,term为0。节点如果收到了RequestVote或者AppendEntries,就会保持自己的Follower身份,节点如果一段时间内没收到AppendEntries消息,在该节点的超时时间内还没发现Leader,Follower就会转换成Candidate,自己开始竞选Leader。如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时向所有其他节点发送 AppendEntries,告知自己成为了Leader。

当选举出Leader Sentinel后,Leader Sentinel会对下线的主服务器执行故障转移操作,主要有三个步骤:

1. 它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;

2. 当客户端试图连接失效的 Master 时,集群也会向客户端返回Master的地址,使得集群可以使用现在的 Master 替换失效 Master 。

3. Master和 Slave服务器切换后, Master的redis.conf 、 Slave的redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即Master 主服务器的 redis.conf 配置文件中会多一行replicaof的配置, sentinel.conf 的监控目标会随之调换。

主服务器的选择

1. 过滤掉主观下线的节点

2. 选择slave-priority最高的节点,如果由则返回没有就继续选择

3. 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整,如果由就返回了,没有就继续

4. 选择run_id最小的节点,因为run_id越小说明重启次数越少

集群和分区特性

分区是将数据分布在多个Redis实例(Redis主机)上,以至于每个实例只包含一部分数据。

分区的意义:

1. 单机Redis的网络I/O能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力 可网络带宽,有助于提高Redis总体的服务能力。

2. 即使Redis的服务能力能够满足应用需求,但是随着存储数据的增加,单台机器受限于机器本身的存储 容量,将数据分散到多台机器上存储使得Redis服务可以横向扩展。

分区方式

范围分区:实现简单,方便迁移和扩展,但是热点数据分布不均,性能损失

Hash分区

利用简单的hash算法,支持任何类型的key 热点分布较均匀,性能较好,但是迁移复杂,需要重新计算,扩展较差,可以使用一致性hash环解决。

Client端分区

对于一个给定的key,客户端直接选择正确的节点来进行读写。许多Redis客户端都实现了客户端分区 (JedisPool),也可以自行编程实现。

客户端选择算法:一致性hash

普通hash是对主机数量取模,而一致性hash是对2^32(4 294 967 296)取模。我们把2^32想象成一 个圆,就像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2^32个 点组成的圆,将缓存服务器与被缓存对象都映射到hash环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第 一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器hash后的值是固定的,所 以,在服务器不变的情况下,数据必定会被缓存到固定的服务器上,那么,当下次想要访问这个数据 时,只要再次使用相同的算法进行计算,即可算出这个数据被缓存在哪个服务器上,直接去对应的服务 器查找对应的数据即可。

优点:添加或移除节点时,数据只需要做部分的迁移,比如上图中把C服务器移除,则数据4迁移到服务器A 中,而其他的数据保持不变。并且通过虚拟节点可解决hash偏移量问题。

缺点 复杂度高 客户端需要自己处理数据路由、高可用、故障转移等问题,且不易扩展

Proxy端分区

在客户端和服务器端引入一个代理或代理集群,客户端将命令发送到代理上,由代理根据算法,将命令 路由到相应的服务器上。常见的代理有Codis(豌豆荚)和TwemProxy(Twitter)。

优点

对客户端透明,与codis交互方式和redis本身交互一样

支持在线数据迁移,迁移过程对客户端透明有简单的管理和监控界面

支持高可用,无论是redis数据存储还是代理节点

自动进行数据的均衡分配

最大支持1024个redis实例,存储容量海量

高性能

缺点

采用自有的redis分支,不能与原版的redis保持同步

如果codis的proxy只有一个的情况下, redis的性能会下降20%左右

某些命令不支持

官方RedisCluster分区

Redis3.0之后,Redis官方提供了完整的集群解决方案。

方案采用去中心化的方式,包括:sharding(分区)、replication(复制)、failover(故障转移)。 称为RedisCluster。 Redis5.0前采用redis-trib进行集群的创建和管理,需要ruby支持 Redis5.0可以直接使用Redis-cli进行集群的创建和管理.

去中心化

RedisCluster由多个Redis节点组构成,是一个P2P无中心节点的集群架构,依靠Gossip协议传播的集群。

Gossip协议

Gossip协议是一个通信协议,一种传播消息的方式。

通过gossip协议,cluster可以提供集群间状态同步更新、选举自助failover等重要的集群功能。

Slot

redis-cluster把所有的物理节点映射到[0-16383]个slot上,基本上采用平均分配和连续分配的方式。

当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把 结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数 量大致均等的将哈希槽映射到不同的节点。

RedisCluster的优势

1. 高性能 Redis Cluster 的性能与单节点部署是同级别的。 多主节点、负载均衡、读写分离

2. 高可用 Redis Cluster 支持标准的 主从复制配置来保障高可用和高可靠。 failover Redis Cluster 也实现了一个类似 Raft 的共识方式,来保障整个集群的可用性。

3. 易扩展 向 Redis Cluster 中添加新节点,或者移除节点,都是透明的,不需要停机。 水平、垂直方向都非常容易扩展。 数据分区,海量数据,数据存储

4. 原生 部署 Redis Cluster 不需要其他的代理或者工具,而且 Redis Cluster 和单机 Redis 几乎完全兼 容。

RedisCluster集群搭建实战

RedisCluster最少需要三台主服务器,三台从服务器。

第一步:创建相应集群目录文件夹,端口号7001-7006

mkdir redis-cluster

mkdir 7001

mkdir 7002

mkdir 7003

mkdir 7004

mkdir 7005

mkdir 7006

第二步:进入redis安装目录,给所有端口号文件夹安装redis

cd redis-5.0.5/src/

make install PREFIX=/usr/local/redis-cluster/7001

第三步:拷贝redis.conf到7001安装目录下

cp /usr/local/redis-5.0.5/redis.conf /usr/local/redis-cluster/7001/bin

第四步:修改redis.conf

vim redis.conf

# bind 127.0.0.1 屏蔽127端口

#将`daemonize`由`no`改为`yes` daemonize yes

#是否开启保护模式,由yes该为no protected-mode no

#端口port改为7001

#打开cluster-enable yes

第五步:把redis.conf复制到其他节点并更改相应端口号

cp -r /usr/local/redis-cluster/7001/* /usr/local/redis-cluster/7002

cp -r /usr/local/redis-cluster/7001/* /usr/local/redis-cluster/7003

cp -r /usr/local/redis-cluster/7001/* /usr/local/redis-cluster/7004

cp -r /usr/local/redis-cluster/7001/* /usr/local/redis-cluster/7005

cp -r /usr/local/redis-cluster/7001/* /usr/local/redis-cluster/7006

vim 7002/bin/redis.conf

第六步:创建start.sh批处理,启动所有的实例

cd 7001/bin

./redis-server redis.conf

cd ..

cd ..

cd 7002/bin

./redis-server redis.conf

cd ..

cd ..

cd 7003/bin

./redis-server redis.conf

cd ..

cd ..

cd 7004/bin

./redis-server redis.conf

cd ..

cd ..

cd 7005/bin

./redis-server redis.conf

cd ..

cd ..

cd 7006/bin

./redis-server redis.conf

cd ..

cd ..

第七步:执行赋写和执行的权限并启动RedisCluster

chmod u+x start.sh

./start.sh

第八步:创建Redis集群(创建时Redis里不要有数据)

./redis-cli –cluster create 47.106.138.46:7001 47.106.138.46:7002 47.106.138.46:7003 47.106.138.46:7004 47.106.138.46:7005 47.106.138.46:7006 –cluster-replicas 1

#–cluster create 集群创建

#做三主三从 前面三个ip做主 后面三个ip做从 采用物理IP地址

#–cluster-replicas 1 说明备份一份 也就是一主一从 如果一主两从则数字为2 三主IP后面要跟六从IP

cat nodes.conf #查看集群节点

第九步:命令客户端连接集

./redis-cli -h 127.0.0.1 -p 7001 -c

cluster nodes

Redis集群扩容实战

第一步:创建新增节点文件夹,并安装redis

mkdir 7007

cd redis-5.0.5/src/

make install PREFIX=/usr/local/redis-cluster/7007

cp /usr/local/redis-5.0.5/redis.conf /usr/local/redis-cluster/7007/bin

第二步:修改redis.conf配置文件

vim redis.conf

# bind 127.0.0.1 屏蔽127端口

#将`daemonize`由`no`改为`yes` daemonize yes

#是否开启保护模式,由yes该为no protected-mode no

#端口port改为7007

#打开cluster-enable yes

第四步:拷贝至7008修改端口并启动7007客户端

cp -r 7007 7008

./redis-server redis.conf

第五步:添加7007结点作为新节点

./redis-cli –cluster add-node 47.106.138.46:7007 47.106.138.46:7001

第六步:hash槽重新分配(数据迁移)

./redis-cli –cluster reshard 47.106.138.46:7007

#输入要分配的槽数量

How many slots do you want to move (from 1 to 16384)? 2000

#输入接收槽的结点id

What is the receiving node ID?

#输入源结点id 也就是那些节点分槽给新节点

Please enter all the source node IDs.

Type 'all' to use all the nodes as source nodes for the hash slots. #全部分

Type 'done' once you entered all the source nodes IDs.#指定ID

第七步:启动7008从节点并添加进集群

./redis-server redis.conf

#./redis-cli –cluster add-node 新节点的ip和端口 旧节点ip和端口

#–cluster-slave — cluster-master-id 主节点id

./redis-cli –cluster add-node 47.106.138.46:7008 47.106.138.46:7007 –cluster-slave –cluster-master-id f3852ca45a0995b9a02488dbd2672aa1bbe93b55

分区路由

不同节点分组服务于相互无交集的分片(sharding),Redis Cluster 不存在单独的proxy或配置服务 器,所以需要将客户端路由到目标的分片。

客户端路由

Redis Cluster的客户端相比单机Redis 需要具备路由语义的识别能力,且具备一定的路由缓存能力。

moved重定向

1.每个节点通过通信都会共享Redis Cluster中槽和集群中对应节点的关系

2.客户端向Redis Cluster的任意节点发送命令,接收命令的节点会根据CRC16规则进行hash运算与 16384取余,计算自己的槽和对应节点

3.如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端

4.如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常

5.客户端接收到节点返回的结果,如果是moved异常,则从moved异常中获取目标节点的信息

6.客户端向目标节点发送命令,获取命令执行结果

ask重定向

在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移 当客户端向某个节点发送命令,节点向客户端返回moved异常,告诉客户端数据对应的槽的节点信息 如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁 移到别的节点了,就会返回ask,这就是ask重定向机制

1.客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回ask转 向给客户端 2.客户端向新的节点发送Asking命令给新的节点,然后再次向新节点发送命令

3.新节点执行命令,把命令执行结果返回给客户端

moved和ask的区别

1、moved:槽已确认转移

2、ask:槽还在转移过程中

节点添加

在RedisCluster中每个slot 对应的节点在初始化后就是确定的。在某些情况下,节点和分片需要变更:

1.新的节点作为master加入;

2.某个节点分组需要下线;

3.负载不均衡需要调整slot 分布。

此时需要进行分片的迁移,迁移的触发和过程控制由外部系统完成。包含下面 2 种:

1.节点迁移状态设置:迁移前标记源/目标节点。

2.key迁移的原子化命令:迁移的具体步骤。

1、向节点B发送状态变更命令,将B的对应slot 状态置为importing。

2、向节点A发送状态变更命令,将A对应的slot 状态置为migrating。

3、向A 发送migrate 命令,告知A 将要迁移的slot对应的key 迁移到B。

4、当所有key 迁移完成后,cluster setslot 重新设置槽位。

扩容实战

集群容灾

故障检测

集群中的每个节点都会定期地(每秒)向集群中的其他节点发送PING消息 如果在一定时间内(cluster-node-timeout),发送ping的节点A没有收到某节点B的pong回应,则A将B 标识为pfail。 A在后续发送ping时,会带上B的pfail信息, 通知给其他节点。 如果B被标记为pfail的个数大于集群主节点个数的一半(N/2 + 1)时,B会被标记为fail,A向整个集群 广播,该节点已经下线。 其他节点收到广播,标记B为fail。

从节点选举

raft,每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。

变更通知

当slave 收到过半的master 同意时,会成为新的master。此时会以最新的Epoch 通过PONG 消息广播 自己成为master,让Cluster 的其他节点尽快的更新拓扑结构(node.conf)。

副本漂移

在一主一从的情况下,如果主从同时挂了,那整个集群就挂了,Redis提供了一种方法叫副本漂移,这种方法既能提高集群的可靠性又不用增加太多的从机。

Master1宕机,则Slaver11提升为新的Master1 集群检测到新的Master1是单点的(无从机),集群从拥有最多的从机的节点组(Master3)中,选择节点名称字母顺序最小的从机(Slaver31)漂移 到单点的主从节点组(Master1)。

发表回复

相关推荐

绘本解读《从前有座山》(大卫.麦基)

很喜欢大卫.麦基的书,如果用一个关键词来总结他的书就是:意料之外

· 5秒前

努力了很久,却没什么收获,你需要了解一下滞后效应

真实生活就像用一个有时间延迟效应的热水器去洗澡。

· 55秒前

美女也會漏尿

漏尿這個詞,說起來可能大傢會覺得不理解,可能你還待字閨中,不知道已經生完寶寶的媽媽會發生這種現象,其實很多年輕的美...

· 56秒前

如何快速增加粉丝?以下是4个有效的技巧,可以帮助你提高你的粉丝增长率。

生活中总会遇到一些挫折和困难,但是只要我们坚持不懈地奋斗,就一定能够越过那些阻碍。放下过去的失败,迎接未来的机遇。相 ...

· 56秒前

(3篇)黨外人士座談會發言稿

黨外人士座談會發言稿(3篇)黨外人士座談會發言稿1xx同志在會上所作的重要講話,是按照市委常委會確定的原則,全面貫徹瞭全國...

· 3分钟前