主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,Master是以写为主,Slave以读为主
能做什么?
读写分离,性能扩展
容灾快速恢复
创建/myredis文件夹
- mkdir /myredis
- cd /myredis
复制redis.conf配置文件到该文件夹中
cp /etc/redis.conf /myredis/redis.conf
修改/myredis/redis.conf
关闭AOF
配置一主两从,创建三个配置文件
新建redis6379.conf,填写如下
新建redis6380.conf,redis6381.conf内容一致把数字6379改一下
启动三台redis服务器
连接指定redis服务器
redis-cli -p 6379
查看三台主机运行情况
当前三台都是主服务器
配从不配主
格式:slaveof ip prot
在6380和6381上执行如下命令
配置完成
测试
此时在主机上设置一个数据,在从机上查看该数据
但在从机中不能做写操作
切入点问题,slave1,slave2是从头开始复制还是从切入点开始复制?
当一个从服务器出现问题宕机,那么在他重启后它就与原来的主服务器没有了主从关系
在此时在主服务器中设置一些键
重新添加主从关系
查看键的情况
可以看到所有的键
结论,从头开始复制
主机shutdown后情况如何?
主机挂掉后,从机就是原地待命,不会上位也不会叛变
主机重启后,新增记录,从机还能顺利复制
从服务器连接上主服务器之后,从服务器向主服务器发送进行数据同步消息(sync命令)(从服务器主动)
主服务器接到从服务器发送过来的同步消息,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件(rdb文件)发送从服务器,从服务器拿到rdb进行读取,完成一次全量复制
每次主服务器进行写操作之后,和从服务器进行数据同步(增量复制)(主服务器主动)
只要重新连接主服务器就自动执行一次全量复制
增量复制:主服务器将新收集到的修改命令依次传给从服务器
从服务器也可以下一个从服务器的主人,它同样可以接收其他从服务器的连接和同步请求,那么该从服务器作为链条中下一个从服务器的主人,可以有效减轻主服务器的写压力,去中心化降低风险
使用命令将6380作为6381的主服务器
这样6379的从服务器只有一台
中途变更转向会清除之前的数据,重新建立拷贝最新的
缺点是某一从服务器宕机,它的从服务器都没法备份
当一个主服务器宕机后,后面的从服务器立刻升为主服务器
使用slaveof no one命令将从机变成主机
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
先将刚才的几个服务器重新调整为一主二仆模式
自定义的/myredis目录下新建sentinel.conf文件
vi sentinel.conf
配置哨兵,编写配置文件
sentinel monitor mymaster 127.0.0.1 6379 1
mymaster是为监控对象起的服务器名称,1为至少1个哨兵同意才更换主机
启动哨兵
redis-sentinel sentinel.conf
关闭主服务器
可以看到哨兵窗口日志,切换了新的主机
原主机重启后会变成从机
哨兵是怎么投票的
优先级在redis.conf中默认:replica-priority 100,值越小优先级越高
如果replica-priority值相同,就选择偏移量大的。偏移量是指获得原主机数据最全的
如果前两个条件一样,那么每个redis实例启动后都会随机生成一个40位的runid,选择runid小的
挑选出新的主服务器后sentinel向原主服务器发送slaveof新主服务器的命令.
复制延时
当所有的写操作都是先在Master上操作,然后同步更新到Slava上,所有从Master同步到Slave机器有一定的延时,当系统很繁忙的时候,延迟问题就会更加严重,Slave机器数量的增加也会使得这个问题更加严重。
容量不够,redis如何进行扩容
并发写操作,redis如何分摊
另外,主从模式,薪火相传模式,主机宕机导致ip地址发生变化,应用程序中配置需要修改对应的主机地址,端口号等信息
之前通过代理主机来解决,但是redis3.0中提供了解决方案,就是无中心化集群配置
Redis集群实现了对Redis的水平扩容,即启动N个redis节点,讲整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
Redis集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
删除持久化数据,将rdb,aof文件都删除掉
制作6个实例,6379,6380,6381,6389,6390,6391(三主三从)
修改redis6379.conf
vi redis6379.conf
开启集群模式,设置节点配置文件名,节点超时时间(超时,集群进行主从切换)
保存文件,将redis6380.conf和redis6381.conf删除
复制文件
ps -ef | grep redis
修改复制而来的五个配置文件的内容(以6380为例)
在编辑页面使用替换命令将6379变为指定数字
:%s/6379/6380
启动6个redis服务(这里是使用6个不同端口号模拟6个服务器)
使用以下命令查看进程
ps -ef | grep redis
将六个节点合成一个集群
先切换到redis最开始安装的目录中
cd /opt/redis-6.2.7/src
执行命令
redis-cli --cluster create --cluster-replicas 1 192.168.199.129:6379 192.168.199.129:6380 192.168.199.129:6381 192.168.199.129:6389 192.168.199.129:6390 192.168.199.129:6391
这里使用自己的ip
replicas 1表示采用最简单的方式配置集群,一台主机,一台从机,正好三组
输入yes
完成搭建
任何一个节点可以作为集群的入口
集群方式连接
一个集群至少要有三个主节点
选项--cluster-replicas 1表示我们希望集群中的每个主节点创建一个从节点
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上
一个Redis集群包含16384个插槽(hash slot),数据库每个键都属于这16384个插槽的其中一个
集群使用公式CRC16(key)%16384来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC16校验和。
集群中的每个节点负责处理一部分插槽,举个例子,如果一个集群可以有主节点,其中:
向集群中添加数据
会根据算得的槽,切换到对应的主机
不在同一个slot下的键值,是不能使用mget,mset等多键操作
可以通过{} 来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中
查询集群中的值 找其所对应的插槽,查看插槽键数量,获得键值
只能查看当前主机所负责的插槽的值
如果主节点下线,从节点是否能自动升为主节点
使用shutdown将6379停掉
从其他节点登入,查看节点状态
发现6389将其替代了,在6379重启后,6379作为它的从机
如果某一段插槽的主从节点都挂掉,而cluster-required-full-coverage为yes,那么整个集群都挂掉
如果某一段插槽的主从节点都挂掉,而cluster-required-full-coverage为no,那么该插槽数据全都不能使用,也无法存储
即使连接的不是主机,集群会自动切换到主机存储,主机写,从机读
无中心化主从集群,无论哪台主机写的数据,其他主机都能读到数据
好处:实现扩容,分摊压力,无中心配置相对简单
不足:不支持多键操作
多建的Redis事务是不支持的,lua脚本不支持
由于集群方案出现比较晚,很多公司采用了其他的集群方案,而代理或客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源,比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
应用服务器的压力突然变大(双十一),redis命中率降低,一直查询数据库(数据库崩溃)
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写入的,并且出于容错考虑,如果存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
对空值缓存:如果一个查询返回的数据为空(不管数据存在不存在),我们仍然会把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过5分钟
设置可访问的名单(白名单):使用bitmaps类型定义一个可访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
采用布隆过滤器(Bloom Filter):是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。(布隆过滤器可以用来检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难)将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
黑客攻击,报警解决!!!
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
某个key过期,大量访问(并发)使用这个key。
此时redis正常运行,也并没有大量key过期,而数据库的访问压力瞬时增加(可能就垮了)
key可能会在某个时间点被超高并发地访问,是一种非常“热点”的数据。这个时候需要考虑缓存击穿的问题
预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据的key的时长
实时调整:现场监控哪些数据热门,实时调整key的过期时长
使用锁:
就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db
先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
当操作成功返回时,再进行load db的操作,并回设缓存,最后删除mutex key(互斥锁)
当操作返回成功,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存学崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key
缓存失效时的雪崩效应对底层系统的冲击非常可怕!
构建多级缓存架构:nginx缓存+reids缓存+其他缓存(ehcache等)
使用锁或队列:用加锁或队列的方式保证不会有大量线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,不适用高并发情况
设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
将缓存失效时间分隔开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效事件
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程,多进程并且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API不能提供分布式锁的能力。为解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:
基于数据库实现分布式锁(乐观锁)
基于缓存(Redis等)
基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
性能:redis最高
可靠性:zookeeper最高
这里我们就急于redis实现分布式锁
首先,为了确保分布式锁可用,我们至少需要确保锁的实现同时满足以下四个条件
1、互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
2、安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
3、死锁:获取锁的客户端因为某些原因(如down机等)而未能释放锁,其它客户端再也无法获取到该锁。
4、容错:当部分节点(redis节点等)down机时,客户端仍然能够获取锁和释放锁。
加锁和解锁必须具有原子性
设置锁和过期时间
setnx命令,设置锁
第一次执行setnx命令,给这个key加上了锁,后面再添加无效
将其删除掉(释放锁),再次设置即可
但此时如果锁一直没有释放就死锁了
可以为锁设置过期时间,到时间自动释放
但是如果在手动设置过期时间前服务器宕机了咋整
那么就需要在上锁的同时设置过期时间 补充:2.6.12版本以后可以通过set命令设置锁
格式:SET key value NX EX max-lock-time
Jedis操作
先设置下值
测试代码如下
- @GetMapping("/testLock")
- public void testLock() {
- //获取锁
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "110", 3, TimeUnit.SECONDS);
- //获取锁成功,查询num的值
- if (lock) {
- Object value = redisTemplate.opsForValue().get("num");
- //判断num为空return
- if (StringUtils.isEmpty(value)) {
- return;
- }
- //有值就转成int
- int num = Integer.parseInt(value + "");
- //把redis的num加1
- redisTemplate.opsForValue().set("num", ++num);
- //释放锁,del
- redisTemplate.delete("lock");
- } else {
- //获取锁失败,每隔0.1s在获取
- try {
- Thread.sleep(100);
- testLock();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
使用ab进行测试
命令如下
ab -n 1000 -c 100 -k http://项目运行ip:8080/app/redis/testLock
查看结果
UUID防止误删
当服务器a上了一个锁,正在执行具体操作时出现卡顿,锁时间到自动释放,然后服务器b抢到了锁,执行具体操作时,服务器a又继续进行操作,然后还把锁释放了,此时它释放的就是b的锁
使用uuid表示不同的操作
释放锁前判断当前uuid和要释放锁的uuid是否相同,代码如下
- @ResponseBody
- @GetMapping("/testLock")
- public void testLock() {
- String uuid = UUID.randomUUID().toString();
- //获取锁
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
- //获取锁成功,查询num的值
- if (lock) {
- Object value = redisTemplate.opsForValue().get("num");
- //判断num为空return
- if (StringUtils.isEmpty(value)) {
- return;
- }
- //有值就转成int
- int num = Integer.parseInt(value + "");
- //把redis的num加1
- redisTemplate.opsForValue().set("num", ++num);
- //释放锁,del
- String lockUuid = (String) redisTemplate.opsForValue().get(lock);
- if (uuid.equals(lockUuid)) {
- redisTemplate.delete("lock");
- }
- } else {
- //获取锁失败,每隔0.1s在获取
- try {
- Thread.sleep(100);
- testLock();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
LUA保证删除原子性
上面代码如果服务器a在比较中结果true,进入了其中,正准备删除但还没删除,锁到了过期时间自动释放了,那么如果有别的服务器拿到了锁,服务器a再进行释放的就是别人的锁。
- /*使用lua脚本来锁*/
- // 定义lua 脚本
- String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
- // 使用redis执行lua执行
- DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
- redisScript.setScriptText(script);
- // 设置一下返回值类型 为Long
- // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
- // 那么返回字符串与0 会有发生错误。
- redisScript.setResultType(Long.class);
- // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
- redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。
在Redis5版本之前,Redis安全规则只有密码控制还有通过rename来调整高危命令比如flushdb,keys *,shutdown等,Redis6则提供ACL功能对用户进行更细粒度的权限控制:
接入权限:用户名和密码
可以执行的命令
可以操作的KEY
参考官网:https://redis.io/topics/acl
命令
acl list展现用户权限列表
查看当前用户
通过命令创建新用户默认权限
没有指定任何规则,如果用户不存在,将使用justcreate的默认属性来创建用户,如果已存在,将不执行任何操作
新建用户设置权限
启用 密码123456 可操作key:cached:* 可执行命令:get
切换用户,验证权限
指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依旧是单线程的。
Redis6加入多线程,但跟Memcached这种从IO处理到数据访问多线程的实现模式有些差异,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程的。之所以这么设计是不想因为多线程而变得复杂,需要去控制key,lua,事务,LPUSH/LPOP等等的并发问题,整体设计如下
另外多线程IO默认也是不开启的,需要在配置文件中配置
io-threads-do-reads yes
io-threads 4
之前老版Redis想要搭集群需要单独安装ruby环境,Redis5将redis-trib.rb的功能集成到redis-cli。另外官方redis-benchmark工具开始支持cluster模式了,通过多线程的方式对多个分片进行压测
Redis6新功能还有:
1、RESP3新的 Redis 通信协议:优化服务端与客户端之间通信
2、Client sidecaching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。
3、Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。
4、Modules API
Redis6中 模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。