Redis 高可用篇

Redis

1、主从复制

实现读写分离,主服务器可读可写,从服务器只能读;

主服务器挂掉时,无法进行写操作

拷贝多个redis.conf文件
include(写绝对路径) #获取该路径上公共配置文件
开启daemonize yes
Pid文件名字pidfile
指定端口port
Log文件名字
dump.rdb名字dbfilename
Appendonly 关掉或者换名字

---------------------------------
eg:
include /redis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

-----------------------------
配从库
slaveof  <ip><port> 成为某个实例的从服务器
eg: slaveof 127.0.0.1 6379

info replication 打印主从复制的相关信息

主从数据同步原理 即复制原理

  1. Slave启动成功连接到master后会发送一个sync命令
  2. Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
  3. 全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
  4. 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
  5. 只要是重新连接master,一次完全同步(全量复制)将被自动执行
薪火相传

上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险

使用 slaveof <ip><port>

中途变更主服务器:会清除之前的数据,重新建立拷贝最新的

风险:一旦某个slave宕机,该salve下的slave都没法备份;主机挂了,从机还是从机,无法写数据了

反客为主

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。用 slaveof no one 将从机变为主机,需手动进行。

2、哨兵模式

反客为主的主动版,能够后台监控主机是否故障,故障根据投票数自动将从服务器转换为主服务器,原主机重启后会变为从机(根据优先级别:slave-priority

在redis.conf目录下新建sentinel.conf文件
配置信息:sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量
执行redis-sentinel  /redis/sentinel.conf -- 存放sentinel.conf文件的位置

复制延时

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重

故障恢复

  1. 从故障的主服务器的所有从服务器里面挑选一个从服务,将其转换为主服务器
    • 选择优先级靠前的 在redis.conf中默认:slave-priority 100,值越小优先级越高
    • 选择偏移量最大的 即获得原主机数据最全的
    • 选择runid最小的 每个redis实例启动后都会随机生成一个40位的runid
  2. 挑选出新的主服务器之后,sentinel向原主服务器的从服务器发送slaveof 新主服务器的命令,从服务器复制新master,进行数据同步
  3. 当故障的原主服务器重新上线后,sentinel会向其发送slaveof 命令,让其称为目前服务器的从服务器

3、redis 集群

主从模式,哨兵模式主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。

集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。

Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。

include /xx/redis.conf -公共配置文件
-----------------------------------
eg:
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/xx/redis_cluster"
logfile "/xx/redis_cluster/redis_err_6379.log"
cluster-enabled yes - 打开集群模式
cluster-config-file nodes-6379.conf - 设定节点配置文件名
cluster-node-timeout 15000 - 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换
---------------------------------------
redis登录命令
redis-cli -c -p 端口号
eg:
redis-cli -c -p 6379

采用槽得概念,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确得节点上执行。

一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个, 使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

集群中的每个节点负责处理一部分插槽, 如果一个集群可以有主节点, 其中:

节点 A 负责处理 0 号至 5460 号插槽。

节点 B 负责处理 5461 号至 10922 号插槽。

节点 C 负责处理 10923 号至 16383 号插槽

  • 在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口

  • 不在一个slot下的键值,是不能使用mget,mset等多键操作

  • 可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oxnmw4XT-1649646362055)(C:\Users\蔡晓娜\AppData\Roaming\Typora\typora-user-images\image-20220410203840313.png)]

故障恢复 与哨兵模式相同

如果某一段插槽的主从都挂掉,而cluster-require-full-coverage yes ,整个集群都挂掉

cluster-require-full-coverage no ,该插槽数据全都不能使用,也无法存储。

4、应用问题

4.1缓存穿透

key对应的数据并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。简而言之,即redis 和 数据源 均不存在的key。

解决方案:

  1. **对空值缓存:**如果一个查询返回的数据为空(不管数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
  2. 采用布隆过滤器:过滤不合法参数,若判断数据不存在则一定不存在,判断存在则不一定存在。
布隆过滤器的实现原理

位图:int[10],总共320bit(4x8x10),每个bit非0即1,初始化时都是0;添加数据时,将数据进行hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个,则一个数据添加会将多个(hash函数个数)bit改为1,多个hash函数目的是减少hash碰撞得概率。

查询数据:hash函数计算得到hash值,对应到bit中,如果有一个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中。

优点:

  • 占用内存小 320bit
  • 增加和查询元素的时间复杂度都是o(K),k为hash函数个数,与数据量无关
  • 哈希函数之间没有关系,方便硬件并行运算
  • 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  • 数据量大时,布隆过滤器可以表示全集
  • 使用同一组散列函数的布隆过滤器可以进行并、查、交

缺点:

  • 误判率,不能准确判断元素在集合中
  • 不能获取元素本身
  • 一般情况下不能从布隆过滤器删除元素
4.2 缓存击穿

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库中加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。简而言之,即redis 中不存在的key – 一个热点key(如秒杀,并发量大)

解决问题:

  • **热点数据提前写入:**在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的有效期

  • 对某些高频访问的Key,设置合理的TTL或永不过期

  • 使用锁:

    • 就是在缓存失效的时候(判断拿出来的值为空),不是立即去加载数据库
    • 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
    • 当操作返回成功时,再进行加载数据库的操作,并回设缓存,最后删除mutex key;
    • 当操作返回失败,证明有线程在加载数据库,当前线程睡眠一段时间再重试整个get缓存的方法
4.3 缓存雪崩

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮 – 即redis出现大量失效的key。

缓存雪崩与缓存击穿的区别在于雪崩针对很多key缓存过期

解决方案:

  • 集群:构建redis 集群,即是个别节点宕机依然可以提供服务
  • 限流 :限制并发访问量
  • 将缓存失效时间分散:过期时间加上随机值,不在同一段时间大量失效
  • :与上一点解决击穿方法一样

5、数据库一致性

5.1 redis 如何保证与数据库的一致性
  1. 旁路缓存模式:

    • 读的时候,先读缓存,缓存命中的话,直接返回数据;

      缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应

    • 更新的时候,先更新数据库,然后再删除缓存

  2. 读写穿透:服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的

    • 从缓存读取数据,读到直接返回;如果读取不到的话,从数据库加载,写入缓存后,再返回响应。
    • 当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新,同步更新缓存和数据库
  3. 异步缓存写入:

    • Read/Write Through是同步更新缓存和数据的,Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。
5.2 分布式数据库实现一致性
  • 延时双删策略:先删除缓存再更新数据休眠一会再次删除缓存

    如果删除失败会导致脏数据

  • 删除缓存重试机制: 保证删除缓存成功

    写请求更新数据库

    缓存因为某些原因删除失败

    删除失败的key放到消息队列

    消费消息队列的消息,获取要删除的key

    重试删除缓存操作

  • 读取binlog异步删除缓存:删除缓存重试机制会造成业务代码的入侵

使用阿里的canal监视binlog的变化,将binlog日志采集发送到消息队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,一直尝试删除缓存直至删除成功

在分布式系统中,缓存和数据库同时存在时,如果有写操作的时候,「先操作数据库,再操作缓存」。如下:

1.读取缓存中是否有相关数据
2.如果缓存中有相关数据value,则返回
3.如果缓存中没有相关数据,则从数据库读取相关数据放入缓存中key->value,再返回
4.如果有更新数据,则先更新数据库,再删除缓存
5.为了保证第四步删除缓存成功,使用binlog异步删除
6.如果是主从数据库,binglog取自于从库
7.如果是一主多从,每个从库都要采集binlog,然后消费端收到最后一台binlog数据才删除缓存,或者为了简单,收到一次更新log,删除一次缓存

redis缓存不适用的场景:

  1. 数据库与缓存需保证数据强一致性:CAP理论牺牲强一致性 保证弱一致性/最终一致性

  2. 业务数据量大但访问频率低时反而浪费内存

实现分布式锁

EX second 设置键的过期时间为 secondSET key value EX second 效果等同于 SETEX key second value 
PX millisecond 设置键的过期时间为 millisecond 毫秒
SET key value PX millisecond 效果等同于 PSETEX key millisecond value 

NX 只在键不存在时,才对键进行设置操作
SET key value NX 效果等同于 SETNX key value 

XX :只在键已经存在时,才对键进行设置操作
分布式锁命令:set key value nx ex -key不存在才操作,设置过期时间避免死锁

为了确保分布式锁可用,至少要确保锁的实现同时满足以下四个条件

  1. 互斥性:在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁 – 设置锁key有效期
  3. 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
  4. 加锁和解锁必须具有原子性 – lua脚本

Redis 整合 SpringBoot

1、引入依赖

<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

2、yml文件 – Redis 配置

#Redis服务器地址
spring.redis.host=192.168.140.136
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

3、Redis 配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
		//key序列化方式
        template.setKeySerializer(redisSerializer);
		//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
		//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
		//解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
		// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))             .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

4、Controller类 测试

@RestController
@RequestMapping("/test")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis() {
        //设置值到redis
        redisTemplate.opsForValue().set("1","2");
        //从redis获取值
        String name = (String)redisTemplate.opsForValue().get("1");
        return name;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值