一.Redis配置多个数据库
redis有没有什么方法使不同的应用程序数据彼此分开同时又存储在相同的实例上呢?就相当于mysql数据库,不同的应用程序数据存储在不同的数据库下.
Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。
用select命令可以手动切换redis数据库,默认从0开始
redis> select 0
每一个数据库都有一个字典,保存这个库里面的所有键值对,我们称之为键空间,键是字符串对象,而值是字符串对象,hash表对象,列表对象,集合对象,有序集合对象中任意一种redis对象。
二. 读写键空间的维护操作
服务器不仅对键空间执行指定操作(增删改查),还会有其他额外的操作
1.读取一个键,会根据键是否存在更新hit,miss的值,标识键空间命中和不命中次数,如果命中会更改RUL(最后使用时间);
2.如果发现键已经过期,服务器会优先删除这个键
3.如果客户端用watch监视了这个键,被watch的这个键修改后,会标记为dirty,让事务知道这个键修改了
4.服务器每次修改一个键后,会对dirty键计数加1,这个计数器会触发持久化操作
三.Redis的持久化
RDB持久化(经过压缩的二进制文件,保存的是键值对信息)
1.RDB文件的创建和载入
可以通过客户端命令创建,或者系统自动创建。命令有save:阻塞创建 BGSAVE:非阻塞创建,会创建子进程来做
RDB文件在服务启动的时候自动载入,redis启动日志:DB load from Disk...就是表示载入RDB文件,如果开启了AOF文件,因为AOF更新频率更高,所有会优先载入AOF文件
2. 自动间隔性保存 (dirty计数器,lastsave属性)
我们可以在配置文件设置save 属性,让redis自动调用BGSAVE命令,
例如:save 900 2 900秒内对数据库进行至少2次修改,满足这个条件,就会执行保存操作。redis从dirty计数器的获取执行次数。保存一次,会把dirty计数器设为0;lastsave保存上次执行保存的时间。
AOF持久化(保存的是redis执行的写命令)
1.redis的服务器进程就是一个事件循环,文件事件负责接收命令和返回命令回复(同时把命令追加到aof_buf缓冲区)。时间事件负责处理需要定时执行的函数,flushAppendOnlyFile就是把aof_buf缓存区的内容写到AOF文件中,是由appendfsync配置决定。
appendfsync有always,ererysec,no三个值; 是在redis.conf配置文件上
2.AOF文件载入和数据还原
redis命令只能在客户端执行,所有创建一个没有网络连接的伪客户端来执行。
3.AOF文件重写
为了解决AOF文件膨胀的问题,redis提供了AOF文件重写功能(aof_rewrite函数),redis会创建一个新的AOF文件来替代旧的AOF文件,两个文件所保存的数据库状态一样。
实现:通过读取当前的服务器数据库状态来实现,通过遍历数据库所有的键,获取键下的值,然后转化为一条命令,这样就得到了新的AOF文件,并且没有冗余的命令。
由于重写的过程可能比较长,redis是会创建一个子进程来执行重写AOF操作的,在执行的时候,如果客户端有新的命令进来,会写入到AOF重写缓存区,重写子进程结束后,会在把AOF重写缓存区的命令写入到新的AOF文件中。
四.Redis事件
Redis是事件驱动程序,服务器需要处理下面两种事件
文件事件(file event):Redis服务器通过套接字与客户端(或其他redis服务器)连接,文件事件就是服务器对套接字操作的抽象
时间事件(time event):服务器对定时操作的抽象
1.文件事件
如图所示,文件事件包括四个部分,套接字,I/O多路复用程序,事件分派器,事件处理器。redis会有多个套接字,所以可以同时接受多个请求,I/O多路复用程序可以同时监听这些套接字,并将产生事件的套接字放到队列中,事件分派器一次只会把一个事件套接字传递给对应的事件处理器,处理完之后,才会把下一个事件套接字传递给处理器。所以说处理器处理命令是同步的一条一条的执行的。所以可以把redis看成单进程处理。
文件事件处理器
1.连接应答处理器,处理客户端连接请求
2.命令请求处理器,
3.命令回复处理器,
2.时间事件
一个时间事件有三个属性,id,when(记录时间事件到达的时间,毫秒),timeProc(时间事件处理器,时间到达后执行的函数)
五.redis事务
redis事务的执行过程
事务开始->命令入队->事务执行
1.事务开始
redis> multi
multi命令是将改客户端修改为事务状态,通过客户端flag属性来标识;
2.命令入队
当客户端是非事务状态时,服务器接收命令会马上执行。当客户端是事务状态的时候,根据不同命令,执行不同的操作
当命令不是exec,discard,watch,multi时,会把命令放入事务队列中,而不是马上执行
3.事务执行
redis> exec
exec命令会把事务队列里的所有命令按照先进先出顺序执行,返回结果给客户端。
4. watch命令的实现
redis里面保存着一个watched_key字典,键就是数据库的键,值是一个链表,保存着监视这个键的客户端。
当执行set,lpush等修改操作时,会对watched_key字典进行检查,如果那个键修改了,会修改这个键上的客户端REDIS_DIRTY_CASE标识打开。表示这个客户端的事务安全性被破坏,当客户端执行exec命令时,服务器会判断客户端的REDIS_DIRTY_CASE标识是否代开,如果打开,则拒绝执行这个客户端提交的事务。
redis内存回收算法
Redis使用的内存回收算法是引用计数算法和LRU算法
1.引用计数算法:对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,同时将该对象引用的其他对象的计数器进行减一操作。
算法实现方式:引用计数算法的垃圾收集一般有侵入式与非侵入式两种,侵入式的实现就是将引用计数器直接根植在对象内部,用C++的思想进行解释就是,在对象的构造或者拷贝构造中进行加一操作,在对象的析构中进行减一操作;非侵入式思想就是有一块单独的内存区域,用作引用计数器。
算法优点:使用引用计数器,内存回收可以穿插在程序的运行中,在程序运行中,当发现某一对象的引用计数器为0时,可以立即对该对象所占用的内存空间进行回收,这种方式可以避免FULL GC(完全垃圾收集)时带来的程序暂停,Redis中就是在引用计数器为0时,对内存进行了回收。
算法缺点:采用引用计数器进行垃圾回收,最大的缺点就是不能解决循环引用的问题,例如一个父对象持有一个子对象的引用,子对象也持有父对象的引用,这种情况下,父子对象将一直存在于JVM的堆中,无法进行回收。
2.LRU算法:LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录该页面自上次被访问以来所经历的时间 t,当必须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面给予淘汰。LRU算法最为经典的实现,就是HashMap+Double LinkedList,时间复杂度为O(1),但是如果按照HashMap和双向链表实现,需要额外的存储存放next和prev指针,牺牲比较大的存储空间,显然是不划算的。所以Redis中的LRU算法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的那个。
六.redis主从复制
旧版本复制功能实现(redis)
复制分为同步和命令传播两个操作,同步就是把主服务器数据库状态同步到从服务器,命令传播就是主服务器执行写命令,然后把命令传到从服务器执行。
1.同步,通过主服务器生产RDB文件来复制
2.命令传播
主服务器执行写命令后,传递给从服务器执行。
当主从服务器不一致时,就会执行同步操作,比如刚开始建立主从,或者主从连接断了,重新连接的时候就要执行同步操作。但是同步操作是非常消耗资源的,首先主服务器创建RDB文件需要消耗I/O和内存,所以新版本的redis对同步操作进行了优化。
新版本复制功能实现
redis2.8版本开始,使用PSYNC命令替代SYNC来执行同步的操作,PSYNC命令具有完整同步和部分重同步两种模式,完整同步模式时,和SYNC步骤差不多,都是生成RDB文件来同步,部分重同步模式主要用于主从断线重连后执行。
部分重同步模式:包含三部分,
1.主服务器的复制偏移量和从服务器的复制偏移量;
主服务器每次想从服务器发送N个字节数据时,主服务器复制偏移量会加N, 从服务器从主服务器接受N个字节数据时,复制偏移量加N。
当主服务器偏移量和从服务器偏移量不一致时,说明主从不一致,从服务器就会想主服务器发送PSYNC命令,同时发送从服务器的offset给主服务器, 执行部分重同步步骤,而丢失的部分就是偏移量的差。
2.主服务的复制积压缓存区
复制积压缓存区是主服务维护的一个固定长度的,先进先出的队列。默认大小是1MB。当主服务器传播命令时,也会把命令写入到这个队列中,并且保留着对应的偏移量。主服务接收到PSYNC命令和从服务器的offset,再根据自己的offset,就可以获取到从服务器缺失的命令了。
3.服务器的运行ID(run id)
每个redis服务器,不论是主服务器还是从服务器,都会有自己的运行ID,由40个随机十六进制字符组成。初次复制时,主服务器会把自己的run id传给从服务器,所以从服务器需要执行同步时,可以判断重新连接的服务器是否是之前同步过的,否则就是完整同步。
心跳检测
在命令传播阶段,从服务器会每秒钟想主服务器发送 REPLCONF ACK <1088>, 1088就是从服务器的复制偏移量,这样做有三个好处,1.检测主从连接状态 2.实现mini-slave 3.检测命令丢失
七,Redis数据结构
https://www.jianshu.com/p/f8d7e8e63cfd
Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。
用select命令可以手动切换redis数据库,默认从0开始
redis> select 0
typedef struct redisDb { //一个redis库的结构
int id; // 数据库ID标识
dict *dict; // 键空间,存放着所有的键值对
dict *expires; // 过期哈希表,保存着键的过期时间
dict *watched_keys; // 被watch命令监控的key和相应client
long long avg_ttl; // 数据库内所有键的平均TTL(生存时间)
} redisDb;
键空间
字典格式(这个字典的底层数据结构就是hash表),键是字符串对象, 值对应五种不同对象, string对象,list对象,hash对象,集合对象,有序集合对象中任意一种redis对象。
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针,一共有6种底层数据结构
void *ptr;
//引用计数
int refcount;
//记录最后一次被程序访问的时间
unsigned lru:22;
}robj
1.String对象
Redis 是用 C 语言写的,但Redis的字符串是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型。
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
string对象的编码类型有 int,raw或者embstr。 int 编码是用来保存整数值,raw编码是用来保存长字符串,而embstr是用来保存短字符串。
2.List对象
底层数据结构是双向链表或者 压缩列表(ziplist), 当列表保存元素个数小于512个且每个元素长度小于64字节时为ziplist, 可以更改list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置。 压缩列表其实就是把数据放在连续的内存中。
3.Hash对象
底层数据结构是哈希表
# hash表
typedef struct dictht{
//哈希表节点数组数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht
# hash表节点
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一个具有相同索引值的哈希表节点,形成链表,链地址法解决哈希冲突问题
struct dictEntry *next;
}dictEntry
4.Set对象,集合对象
底层数据结构是整数集合(intset)或者hash表, 当集合对象中所有元素都是整数且所有元素数量不超过512时为intset类型,可通过set-max-intset-entries 进行配置。
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
5.zset对象,有序集合
底层数据结构为 跳跃表(skiplist),是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。
跳跃表类似公司的组织架构:董事会->C?O->部门->组长->员工
typedef struct zskiplistNode {
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode
typedef struct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
八.Redis的高可用
Redis Sentinel(哨兵)
https://www.cnblogs.com/Eugene-Jin/p/10819601.html
Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自懂切换。 首先我们有一个redis的master-slave集群,然后再开启Redis-sentinel进程来监控和管理这个集群。
搭建redis主从
修改redis.conf文件 ,只需要配置从服务器
# 使得Redis服务器可以跨网络访问
bind 0.0.0.0
# 设置密码
requirepass "123456"
# 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
slaveof 192.168.11.128 6379
# 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
masterauth 123456
搭建哨兵
配置3个哨兵,每个哨兵的配置都是一样的。在Redis安装目录下有一个sentinel.conf文件
# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 192.168.11.128 6379 2
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456
启动redis服务和哨兵
# 启动Redis服务器进程
./redis-server ../redis.conf
# 启动哨兵进程
./redis-sentinel ../sentinel.conf
注意启动的顺序。首先是主机(192.168.11.128)的Redis服务进程,然后启动从机的服务进程,最后启动3个哨兵的服务进程。
哨兵原理
1.Sentinel集群通过给定的配置文件发现master,启动时会监控master。通过每隔10s向master发送info信息获得该服务下面的所有从服务器。
2.每隔1秒每个哨兵会向主节点、从节点及其余哨兵节点发送一次ping命令做一次心跳检测,然后标记主观下线,并且通过指令sentinel is-masterdown-by-addr寻求其它哨兵节点对主节点的判断,然后确定客户下线
3.如果主节点被判定为客观下线之后,就要选取一个哨兵节点来完成后面的故障转移工作。选举过程就是2中发送sentinel is-masterdown-by-addr的过程。
4.选择slave-priority最高的节点,如果由则返回没有就继续选择,选择出复制偏移量最大的系节点,因为复制便宜量越大则数据复制的越完整,如果由就返回了,没有就继续选择进程id最小的节点
客户端连接redis,首先要连接sentinel获取master的连接信息。
//初始化redis对象
$redis = new Redis();
//连接sentinel服务 host为ip,port为端口,哨兵的ip和端口号
$redis->connect($host, $port);
//获取主库列表及其状态信息
$result = $redis->rawCommand('SENTINEL', 'masters');
//根据所配置的主库redis名称获取对应的信息
//master_name应该由运维告知(也可以由上一步的信息中获取)
$result = $redis->rawCommand('SENTINEL', 'master', $master_name);
//根据所配置的主库redis名称获取其对应从库列表及其信息,读数据可以用从库
$result = redis->rawCommand('SENTINEL', 'slaves', $master_name);
//获取特定名称的redis主库地址
$result = $redis->rawCommand('SENTINEL', 'get-master-addr-by-name', $master_name)
//以上部分可以获取到主库的ip和对应端口,程序可以直接像链接单台redis一样链接操作使用
Redis Cluster- 分布式架构
即Redis Cluster中有多个节点,每个节点都负责进行数据读写操作,每个节点之间会进行通信。
redis cluster搭建
安装好redis后,进入源码目录,创建目录redis-cluster,并在redis-cluster目录下穿件7000,7001,7002,7003,7004,7005六个目录,复制redis.conf到每个目录中
daemonize yes #后台启动
port 7001 #修改端口号,从7001到7006
cluster-enabled yes #开启cluster,去掉注释
cluster-config-file nodes.conf #自动生成
cluster-node-timeout 15000 #节点通信时间
appendonly yes #持久化方式
复制redis解压文件src下的redis-trib.rb文件到redis-cluster目录并安装gem
2、启动所有的redis节点
cd 7000
redis-server redis.conf
cd ..
cd 7001
redis-server redis.conf
cd ..
cd 7002
redis-server redis.conf
cd ..
cd 7003
redis-server redis.conf
cd ..
cd 7004
redis-server redis.conf
cd ..
cd 7005
redis-server redis.conf
cd ..
可以看到redis的6个节点已经启动成功
使用redis-trib.rb创建集群
下面这个命令,就会创建集群
./redis-trib.rb create --replicas 1 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7000
--replicas 1 表示 自动为每一个master节点分配一个slave节点 上面有6个节点,程序会按照一定规则生成 3个master(主)3个slave(从)
1.Redis Cluster中的每个节点都保存了集群的配置信息,并且存储在clusterState中,节点间采取gossip协议进行通信,通讯端口为服务端口号+10000,它们之间通过互相ping-pong判断节点是否可以连接,判断主观下线
新增一个主节点
新增一个节点7006作为主节点,操作步骤如下:
-
修改配置文件,新建一个对应的文件7006,并把复制配置文件redis.conf放入到文件7006下面,并修改配置文件,把端口修改为7006,其他配置信息也参考前面的案例对应修改。节点配置信息成功后,启动7006下面的redis。
-
将7006加入到现有的集群中,输入指令:./redis-trib.rb add-node 192.168.210.128:7006 192.168.210.128:7002。指令说明:dd-node是加入集群节点,192.168.210.128:7006为要加入的节点,192.168.210.128:7002 表示加入的集群的一个节点,用来辨识是哪个集群,理论上那个集群的节点都可以。
-
目前cluster已经定义7006为主节点,但是Cluster并未给7006分配哈希卡槽(0 slots)。
-
redis-cluster在新增节点时并未分配卡槽,需要操作者手动对集群进行重新分片迁移数据,需要重新分片命令:reshard。操作如:redis-trib.rb reshard 192.168.210.128:7002。指令说明:这个命令是用来迁移slot节点的,后面的192.168.210.128:7002是表示是哪个集群,端口填[7000-7006]都可以,执行后:它提示需要迁移多少slot到7006上平分16384个哈希槽给4个节点:16384/4 = 4096,可移动4096个槽点到7006上。填写7006的id:如ee3efb90e5ac0725f15238a64fc60a18a71205d7。
新增从节点
-
新增一个节点7007作为从节点修改配置文件,新建一个对应的文件7007,并把复制配置文件redis.conf放入到文件7007下面,并修改配置文件,把端口修改为7007,其他配置信息也参考前面的案例对应修改。节点配置信息成功后,启动7007下面的redis并加入到现有集群中。
-
redis-trib增加从节点的命令为:./redis-trib.rb add-node --slave --master-id $[nodeid] 192.168.210.128:7007 192.168.210.128:7000 。操作指令含义:nodeid为要加到master主节点的node id,192.168.210.128:7007为新增的从节点,192.168.210.128:7000为集群的一个节点(集群的任意节点都行),用来辨识是哪个集群;如果没有给定那个主节点--master-id的话,redis-trib将会将新增的从节点随机到从节点较少的主节点上。
-
从节点不存在分片操作,与主节点对应的片一致。
移除主节点
-
移除节点使用redis-trib的del-node命令,redis-trib del-node 192.168.210.128:7002 ${node-id} 。操作指令含义: 192.168.210.128:7000为指定集群,node-id为要删除的主节点。 和添加节点不同,移除节点node-id是必需的。
-
测试删除7001主节点,redis cluster提示7001已经有数据了,不能够被删除,需要将他的数据转移出去,也就是和新增主节点一样需重新分片。
-
分区指令: ./redis-trib.rb reshard 192.168.210.128:7002
-
输入提示的需要移动的分片大小,分配给7001的slots为4096,输入需要移动的片为4096。
-
输入这些移除的slots如何分配给其他node,可指定一个具体node的id或者选择所有。
-
最后确认后,开始移除节点。
移除从节点
-
移除节点使用redis-trib的del-node命令,redis-trib del-node 192.168.210.128:7002 ${node-id} 。操作指令含义: 192.168.210.128:7000为指定集群,node-id为要删除的节点。 和添加节点不同,移除节点node-id是必需的。
-
从节点不存在分片问题,直接执行命令,确认移除即可。
集群原理
1.Redis Cluster中的每个节点都保存了集群的配置信息,并且存储在clusterState中,节点间采取gossip协议进行通信,通讯端口为服务端口号+10000,它们之间通过互相ping-pong判断节点是否可以连接,判断主观下线
2.当节点被判定为客观下线时,从节点发起竞选
3.当竞选从节点收到过半主节点同意,便会成为新的主节点。此时会以最新的Epoch通过PONG消息广播,让Redis Cluster的其他节点尽快的更新集群信息。当原主节点恢复加入后会降级为从节点。
3.客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot
如果在本地就在本地处理,否则返回moved给客户端,让客户端进行重定向