Redis的复制
复制
Redis支持复制的功能,以实现当一台服务器的数据更新后,自动将新的数据同步到其它数据库。
Redis复制实现中,把数据库分为主数据库master和从数据库slave,主数据库可以进行读写操作,从数据库一般是只读的,当主数据库数据变化的时候,会自动同步给从数据库。
复制带来的好处
1:可以实现读写分离
2:利于在主数据库崩溃时的数据恢复
复制的配置
主数据库不做配置;从数据库需要在配置中设置“slaveof 主数据库ip 主数据库端口”。
复制的基本操作命令
1:info replication :可以查看复制节点的相关信息
2:slaveof:可在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系,转而和新的主数据库同步
3:slaveof no one:使当前数据库停止与其他数据库的同步,转成主数据库
复制的基本原理
1:slave启动时,会向master发送sync命令,2.8版后发送psync,以实现增量复制
2:主数据库接到sync请求后,会在后台保存快照,也就是实现RDB持久化,并将保存快照期间接收到的命令缓存起来
3:快照完成后,主数据库会将快照文件和所有缓存的命令发送给从数据库
4:从数据库接收后,会载入快照文件并执行缓存的命令,从而完成复制的初始化
5:在数据库使用阶段,主数据库会自动把每次收到的写命令同步到从服务器
乐观复制策略
Redis采用乐观复制的策略,容忍在一定时间内主从数据库的内容不同,当然最终的数据会是一样的。这个策略保证了性能,在复制的时候,主数据库并不阻塞,照样处理客户端的请求。
Redis提供了配置来限制只有当数据库至少同步给指定数量的从数据库时, 主数据库才可写,否则返回错误。配置是:min-slaves-to-write、min-slaves- max-lag
无硬盘复制
当复制发生时,主数据库会在后台保存RDB快照,即使你关闭了RDB,它也会这么做,这样就会导致:
1:如果主数据库关闭了RDB,现在强行生成了RDB,那么下次主数据库启动的时候,可能会从RDB来恢复数据,这可能是旧的数据。
2:由于要生成RDB文件,如果硬盘性能不高的时候,会对性能造成一定影响。因此从2.8.18版本,引入了无硬盘复制选项:repl-diskless-sync n 哨兵(sentinel)
Redis提供了哨兵工具来实现监控Redis系统的运行情况,主要实现:
1:监控主从数据库运行是否正常
2:当主数据库出现故障时,自动将从数据库转换成为主数据库
3:使用Redis-sentinel,redis实例必须在非集群模式下运行
开启哨兵功能 建立一个sentinel.conf文件,里面设置要监控的主数据库的名字,形如: sentinel monitor 监控的主数据库的名字 127.0.0.1 6379 1
1 表示选举主数据库的最低票数
(1)这个文件的内容,在运行期间会被sentinel动态进行更改
(2)可以同时监控多个主数据库,一行一个配置即可
Redis的集群
复制的问题
由于复制中,每个数据库都是拥有完整的数据,因此复制的总数据存储量,受限于内存最小的数据库节点,如果数据量过大,复制就无能为力了。
分片
分片(Partitioning)就是将你的数据拆分到多个Redis实例的过程,这样每个 Redis实例将只包含完整数据的一部分。常见的分片方式:
1:按照范围分片
2:哈希分片,比如一致性哈希
常见的分片实现:
1:在客户端进行分片
2:通过代理来进行分片,比如:Twemproxy
3:查询路由:就是发送查询到一个随机实例,这个实例会保证转发你的查询到正确的节点,Redis集群在客户端的帮助下,实现了查询路由的一种混合形式,请求不是直接从Redis实例转发到另一个,而是客户端收到重定向到正确的节点
4:在服务器端进行分片, Redis采用哈希槽(hash slot)的方式在服务器端进行分片: Redis集群有16384个哈希槽,使用键的CRC16编码对16384取模来计算一个键所属的哈希槽
Redis分片的缺点
1:不支持涉及多键的操作,如mget,如果所操作的键都在同一个节点,就正常执行,否则会提示错误
2:分片的粒度是键,因此每个键对应的值不要太大
3:数据备份会比较麻烦,备份数据时你需要聚合多个实例和主机的持久化文件
4:扩容的处理比较麻烦
5:故障恢复的处理会比较麻烦,可能需要重新梳理Master和Slave的关系,并调整每个复制集里面的数据
集群
由于数据量过大,单个复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展,每个复制集只负责存储整个数据集的一部分,这就是Redis的集群。
1:在以前版本中,Redis的集群是依靠客户端分片来完成,但是这会有很多缺点,比如维护成本高,需要客户端编码解决;增加、移出节点都比较繁琐等
2:Redis3.0新增的一大特性就是支持集群,在不降低性能的情况下,还提供了网络分区后的可访问性和支持对主数据库故障的恢复。
3:使用集群后,都只能使用默认的0号数据库 4:每个Redis集群节点需要两个TCP连接打开,正常的TCP端口用来服务客户端,例如6379,加10000的端口用作数据端口,必须保证防火墙打开这两个端口
5:Redis集群不保证强一致性,这意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请
求命令。
集群架构
1:所有的Redis节点彼此互联,内部使用二进制协议优化传输速度和带宽
2:节点的fail是通过集群中超过半数的节点检测失效时才生效
3:客户端与Redis节点直连,不需要中间proxy层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
4:集群把所有的物理节点映射到[0-16383]插槽上,集群负责维护:节点-插槽-值 的关系
集群操作基本命令
1:CLUSTER INFO:获取集群的信息
2:CLUSTER NODES:获取集群当前已知的所有节点,以及这些节点的相关信息
3:CLUSTER MEET <ip> <port>:将ip和port所指定的节点添加到集群当中
4:CLUSTER FORGET <node_id>:从集群中移除 node_id 指定的节点
5:CLUSTER REPLICATE <node_id>:将当前节点设置为 node_id 指定的节点的从节点
6:CLUSTER SAVECONFIG:将节点的配置文件保存到硬盘里面
7:CLUSTER ADDSLOTS <slot> [slot ...]:将一个或多个槽分配给当前节点
8:CLUSTER DELSLOTS <slot> [slot ...]:从当前节点移除一个或多个槽
9:CLUSTER FLUSHSLOTS:移除分配给当前节点的所有槽
10:CLUSTER SETSLOT <slot> NODE <node_id>:将槽分配给 node_id 指定的节点,如果槽已经分配给另一
个节点,那么先让另一个节点删除该槽>,然后再进行分配
11:CLUSTER SETSLOT <slot> MIGRATING <node_id>:将本节点的槽迁移到指定的节点中
12:CLUSTER SETSLOT <slot> IMPORTING <node_id>:从指定节点导入槽到本节点
13:CLUSTER SETSLOT <slot> STABLE:取消对槽的导入(import)或迁移(migrate)
14:CLUSTER KEYSLOT <key>:计算键 key 应该被放置在哪个槽
15:CLUSTER COUNTKEYSINSLOT <slot>:返回槽目前包含的键值对数量
16:CLUSTER GETKEYSINSLOT <slot> <count>:返回 count 个槽中的键
17:migrate 目的节点ip 目的节点port 键名 数据库号码 超时时间 [copy] [replace]:迁移某个键值对
手工创建集群
1:首先进行集群配置
只需要将每个数据库的cluster-enabled配置选项打开,然后再修改如下内 容:pidfile、port、logfile、dbfilename、cluster-config-file
2:分别启动这些Redis数据库,可以用info cluster查看信息
3:连接节点,使用cluster meet,把所有的数据库都放到一个集群中来
4:可以通过cluster info ,或者cluster nodes 查看信息
5:设置部分数据库为slave,使用cluster replicate
6:然后就该来分配插槽了,使用cluster addSlots,这个命令目前只能一个一个加,如果要加区间的话,就得客户端编写代码来循环添加。 有个实用的技巧:把所有的Redis停下来,然后直接修改node的配置文件,
只需要配置master的数据库就可以,然后再重启数据库。 分配完插槽,可以使用cluster slots查看信息。
7:通过cluster info查看集群信息,如果显示ok,那就可以使用了
什么是插槽
插槽是Redis对Key进行分片的单元。在Redis的集群实现中,内置了数据自动分片机制,集群内部会将所有的key映射到16384个插槽中,集群中的每个数据库实例负责其中部分的插槽的读写。
键与插槽的关系
Redis会将key的有效部分,使用CRC16算法计算出散列值,然后对16384取余数,从 而把key分配到插槽中。键名的有效部分规则是:
1:如果键名包含{},那么有效部分就是{}中的值
2:否则就是取整个键名
移动已分配的插槽
这个稍微麻烦点,尤其是有了数据过后,假设要迁移123号插槽从A到B,
大致步骤如下:
1:在B上执行cluster setslot 123 importing A
2:在A上执行cluster setslot 123 migrating B
3:在A上执行cluster getkeysinslot 123 要返回的数量
4:对上一步获取的每个键执行migrate命令,将其从A迁移到B
5:在集群中每个服务器上执行cluster setslot 123 node B
避免在移动已分配插槽过程中,键的临时丢失 上面迁移方案中的前两步就是用来避免在移动已分配插槽过程中,键的临时丢失问题的,
大致思路如下:
1:当前两步执行完成后,如果客户端向A请求插槽123中的键时,如果键还未被转移,A将处理请求
2:如果键已经转移,则返回,把新的地址告诉客户端,客户端将发起新的请求以获取数据
获取插槽对应的节点
当客户端向某个数据库发起请求时,如果键不在这个数据库里面,将会返回一个move重定向的请求,里面包含新的地址,客户端收到这个信息后,需要重新发起请求到新的地址去获取数据。
当然,大部分的Redis客户端都会自动去重定向,也就是这个过程对开发人 员是透明的。redis-cli也支持自动重定向,只需要在启动时加入 -c 的参数。
故障判定
1:集群中每个节点都会定期向其他节点发出ping命令,如果没有收到回复,就认为该节点为疑似下线,然后在集群中传播该信息
2:当集群中的某个节点,收到半数以上认为某节点已下线的信息,就会真的标记该节点为已下线,并在集群中传播该信息
3:如果已下线的节点是master节点,那就意味着一部分插槽无法写入了
4:如果集群任意master挂掉,且当前master没有slave,集群进入fail状态
5:如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态
6:当集群不可用时,所有对集群的操作做都不可用,收到CLUSTERDOWN The cluster is down错误信息
故障恢复 发现某个master下线后,集群会进行故障恢复操作,来将一个slave变成master,基于Raft算法,大致步骤如下:
1:某个slave向集群中每个节点发送请求,要求选举自己为master
2:如果收到请求的节点没有选举过其他slave,会同意
3:当集群中有超过节点数一半的节点同意该slave的请求,则该Slave选举成功
4:如果有多个slave同时参选,可能会出现没有任何slave当选的情况,将会等待一个随机时间,再次发出选举请求
5:选举成功后,slave会通过 slaveof no one命令把自己变成master
如果故障后还想集群继续工作,可设置cluster-require-full-coverage为no,默认yes
对于集群故障恢复的说明
1:master挂掉了,重启还可以加入集群,但挂掉的slave重启,如果对应的master变化了,是不能加入集群的,除非修改它们的配置文件,将其master指向新master
2:只要主从关系建立,就会触发主和该从采用save方式持久化数据,不论你是否禁止save
3:在集群中,如果默认主从关系的主挂了并立即重启,如果主没有做持久化,数据会完全丢失,从而从的数据也被清空
使用redis-trib.rb来操作集群 redis-trib.rb是Redis源码中提供的一个辅助工具,可以非常方便的来操作集群,它是用 ruby写的,因此需要在服务器上安装相应环境
1:安装Ruby
(1)下载ruby安装包,地址https://www.ruby-lang.org/en/downloads/
(2)然后分别configure、make、make install
(3)安装后通过ruby -v 查看一下版本,看是否正常
2:还需要安装rubygems
(1)下载包,地址https://rubygems.org/pages/download
(2)解压后进入解压文件夹,运行 ruby setup.rb
(3)安装后通过gem –v查看一下版本,看是否正常 3:还需要安装redis的ruby library
(1)由于连接国外源不太稳定,请先删除,如gem sources --remove
https://rubygems.org/ ,然后添加gem sources -a https://ruby.taobao.org/
(2)可以通过gem sources -l 查看源
(3)运行gem install redis
4:使用redis-trib.rb来初始化集群,形如:
ruby redis-trib.rb create --replicas 1 127.0.0.1:6381
127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385
127.0.0.1:6386
create表示要初始化集群,--replicas 1表示每个驻数据库拥有的从数据 库为1个
5:使用redis-trib.rb来迁移插槽,如下:
(1)执行ruby redis-trib.rb reshard ip:port ,这就告诉Redis要重新分片,ip:port可以是集群中任何一个节点
(2)然后按照提示去做就可以了
(3)这种方式不能指定要迁移的插槽号
Redis预分区
介绍
为了实现在线动态扩容和数据分区,Redis的作者提出了预分区的方案,实际就是在同一台机器上部署多个Redis实例,当容量不够时将多个实例拆分到不同的机器上,这样就达到了扩容的效果。拆分过程如下:
1:在新机器上启动好对应端口的Redis实例
2:配置新端口为待迁移端口的从库
3:待复制完成,与主库完成同步后,切换所有客户端配置到新的从库的端口
4:配置从库为新的主库
5:移除老的端口实例
6:重复上述过程把要迁移的数据库转移到指定服务器上
以上拆分流程是Redis作者提出的一个平滑迁移的过程,不过该拆分方法还 是很依赖Redis本身的复制功能的,如果主库快照数据文件过大,这个复制的过 程也会很久,同时会给主库带来压力。
Redis的安全
简述 Redis在安全部分并没有做太多的工作,毕竟Redis是按照“Redis是运行在可信环境”这个假定来设计的。
安全相关的配置
1:bind:可以绑定允许访问数据库的地址,只能绑定一个地址
2:requirepass:设置数据库密码,如果设置了,那么客户端每次连接Redis的时候,都需要传入密码,形如:auth 密码,然后才能执行命令。 如果是复制集,就需要配置masterauth参数为主数据库的密码
安全相关的命令
1:rename-command:命令重命名
Redis的管理
常用的管理命令
1:slowlog get:获取慢日志,可以通过配置文件的slowlog-log-slower-than来设置时间限制,默认是10000微秒,slowlog-max-len来限制记录条数。
返回的记录包含四个部分:
(1)日志的id
(2)该命令执行的unix时间
(3)该命令消耗的时间,单位微秒
(4)命令和参数
2:monitor:监控Redis执行的所有命令,这个命令比较耗性能,建议仅用在开发调试阶段
常用的管理工具
1:phpRedisAdmin:地址https://github.com/ErikDubbelboer/phpRedisAdmin
2:rdbtools:地址https://github.com/sripathikrishnan/redis-rdb-tools ,这个是用 python写的,可以提供生成内存报告、 转储文件到JSON、使用标准的diff工具比较两个 dump文件等功能
3:Cacti:用来监控Redis服务的流量
适合使用Redis的场景
缓存
取最新N个数据的操作,如:可以将最新的50条评论的ID放在List集合排行榜类的应用,取TOP N操作,前面操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序
计数器应用
存储关系:比如社交关系,比如Tag等
获取某段时间所有数据排重值,使用set,比如某段时间访问的用户ID,或者是
客户端IP
构建队列系统,List可以构建栈和队列,使用zset可以构建优先级队列 n 实时分析系统,比如:访问频率控制
模拟类似于HttpSession这种需要设定过期时间的功能Pub/Sub构建实时消息系统
记录日志
使用管道(Pipeline) Redis的底层通讯协议对管道提供了支持,通过管道,可以一次性发送多条命令给
Redis,在执行完后一次性将结果取回。 使用管道,可以减少客户端和Redis的通信次数,降低网络延时,从而提供性能。
Redis的管道功能在命令行中没有,但Redis是支持管道的,而且在各个语言版的client中
都有相应的实现。
精简键名和键值
合理设计存储的数据结构和数据关系,尽量减少数据冗余
尽量使用mset来赋值,比set效率高一个数量级;类似的还有lpush、zadd等都可以一次输
入多个指令
如果可能,尽量使用Lua脚本来辅助获取或操作数据 n 尽量使用hash结构来存储对象
将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对 象,省内存的原因是新建一个hash对象时开始是用zipmap来存储的。
使用hash结构时,应尽量保证每个key下面的<field, value>的数目不超过限制(默认值为 64),否则插入效率下降十分明显,同样,内存开销也会显著增加
配置使用ziplist以优化list 如果list的元素个数小于配置值list-max-ziplist-entries且元素值字符串的长度 小于配置值list-max-ziplist-value,则可以编码成ziplist类型存储,否则采用 Dict 来存储,Dict实际是Hash Table的一种实现。
配置使用intset以优化set
当set集合中的元素为整数且元素个数小于配置set-max-intset-entries值时,使用 intset数据结构存储,否则转化为Dict结构
配置使用ziplist以优化sorted set 当sorted set的元素个数及元素大小小于一定限制时,它是用ziplist来存储。这个限制的配置如下:zset-max-ziplist-entries、zset-max-ziplist-value n 配置使用zipmap以优化hash 当entry数量没有超过hash-max-ziplist-entries指定的限制,并且值的最大长度没 有超过hash-max-ziplist-value指定的限制时,会用zipmap来编码。
注意:HashMap的优势就是查找和操作的时间复杂度都是O(1)的,而放弃Hash采用一 维存储则是O(n)的时间复杂度,如果成员数量很少,则影响不大,否则会严重影响性能, 所以要权衡好些个值的设置,在时间成本和空间成本上进行权衡。
一定要设置maxmemory 设置Redis使用的最大物理内存,也就是使用了这么多物理内存后就开始拒绝后续的写入请求,该参数能保护Redis不会因为使用了过多的物理内存而严重影响性能甚至崩溃。
对排序的优化
1:尽量让要排序的Key存放在一个Server上 如果采用客户端分片,那么具体决定哪个key存在哪个服务器上,是由 client端采用一定算法来决定的,因此可以通过只对key的部分进行hash。比 如:client如果发现key中包含{},那么只对key中{}包含的内容进行hash。
如果采用服务端分片,也可以通过控制key的有效部分,来让这些数据分配 到同一个插槽中。
2:尽量减少Sort的集合大小
如果要排序的集合非常大, 会消耗很长时间,Redis单线程的,长时间的排序操作会阻塞其它client的请求。解决办法是通过主从复制,将数据复制到多个 slave上,然后只在slave上做排序操作,并尽可能的对排序结果缓存。
考虑采用复制+RDB的方式 使用复制机制来实现高可用,数据采用RDB的方式进行持久化备份,建议只在Slave上持久化RDB文件,而且只要在一个相对较长的时间备份一次就够了,比 如只保留save 900 1这条规则,大致就是15分钟保存一次。 这样的方式避免了AOF带来的持续的IO,也避免AOF Rewrite最后将rewrite 过程中产生的新数据写到新文件所造成的阻塞。代价是如果Master/Slave同时倒 掉,可能会丢失15分钟的数据。
考虑在一台服务器启动多个Redis实例 由于Redis使用单线程,为了提高CPU利用率,可以在同一台服务器上启动多个Redis实例,但这可能会带来严重的IO争用,除非Redis不需要持久化,或者 有某种方式保证多个实例不会在同一个时间重写AOF。
Redis的Java客户端
Redis官方推荐的Java客户端是jedis 网址:https://github.com/xetorthio/jedis
构建开发环境,在Maven中添加
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
连接池
1:jedis的连接池基于apach的commons-pool,要确保添加了相应的包
2:配置连接池,一般使用JedisPoolConfig,常见的设置:
(1)maxTotal:最大实例数,-1表示不限制,默认8
(2)maxIdle/minIdle:最大/小空闲实例数,默认8/0
(3)whenExhaustedAction:当池中的实例被分配完时,要采取的操作,默认有三种:
WHEN_EXHAUSTED_FAIL:直接抛出NoSuchElementException WHEN_EXHAUSTED_BLOCK:阻塞住,达到maxWaitMillis时抛出例外,默认选项 WHEN_EXHAUSTED_GROW:新建一个实例,也就说设置的maxTotal无用
(4)maxWaitMillis:获取一个实例时,最大的等待毫秒数,如果超时抛出JedisConnectionException,默 认-1,表示永远等待
(5)testOnBorrow:在获取实例时,是否验证对象有效,如果无效,会重新选择一个,默认false
(6)testOnReturn:在归还连接给池时,是否验证对象有效性,默认false
(7)testWhileIdle:是否对空闲实例验证对象有效性,失效的对象会被删除,默认false,仅在 timeBetweenEvictionRunsMillis设置为正值时有效
(8)timeBetweenEvictionRunsMillis:空闲实例验证,两次扫描之间要sleep的毫秒数;默认是-1
(9)numTestsPerEvictionRun:空闲实例验证,每次扫描的最多的对象数,默认是3,仅在 timeBetweenEvictionRunsMillis设置为正值时有效
(10)minEvictableIdleTimeMillis:一个实例至少停留在idle状态的最短时间,然后才能被空闲实例验证扫描并驱逐;默认30分钟,仅在timeBetweenEvictionRunsMillis设置为正值时有效
(11)lifo:设置采用last in first out队列,默认true
连接集群 以前的老版本Redis不支持集群的功能,因此是在客户端采用一致性Hash来对数据进行分片,jedis中对应的是SharedJedis;既然现在Redis已经支持集群了,
就先看新的集群的写法:
Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>(); //Jedis Cluster will attempt to discover cluster nodes automatically jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7379)); JedisCluster jc = new JedisCluster(jedisClusterNodes);
jc.set("foo", "bar");
String value = jc.get("foo");
客户端分片的方式
1:定义一个List,里面包含多个JedisShardInfo
2:创建ShardedJedis对象,然后就可以通过这个对象操作命令了,操作完后可以disconnect
3:可以通过getShardInfo(key)方法来获取使用的分片信息
4:可以通过设置key tag pattern来保证key 位于同一个shard
5:分片的连接池对象是:ShardJedisPool,配置对象仍然是JedisPoolConfig,例如:
ShardJedisPool pool =new ShardedJedisPool(jedisPoolConfig, jedsInfoList, Hashing.MURMUR_HASH,Sharded.DEFAULT_KEY_TAG_PATTERN);
Redis和Spring集成
构建开发环境,在Maven中添加
spring-data-redis支持的jedis版本目前只是到了2.7.0,所以要修改一下前面的配置,然
后添加:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.5.1.RELEASE</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cobertura</groupId>
<artifactId>cobertura</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4</version>
</dependency>
在Spring的配置文件中添加
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="8"></property> <property name="maxIdle" value="8"></property> <property name="maxWaitMillis" value="1000"></property>
</bean>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="192.168.1.106" />
<property name="port" value="6379" />
<property name="usePool" value="true" />
<property name="timeout" value="100000" />
<constructor-arg index="0" ref="poolConfig" /> </bean>
<bean id="testClient" class="com.javass.TestClient">
<property name="connectionFactory" ref="jedisConnectionFactory"></property>
</bean>
在Java程序中,就可以使用RedisTemplate了,
例如:
String s = ""+this.execute(new RedisCallback(){
public Object doInRedis(RedisConnection jr) throws DataAccessException { return new String(jr.get("k1".getBytes()));
}});