Redis主从异步复制和Sentinel脑裂导致数据丢失的问题与解决方案

Redis主从异步复制导致数据丢失

因为Redis主从之间复制数据是异步进行的,那么就有可能在某一时刻Redis Master没有保存数据就突然宕机,此时还没来得及向Slave节点同步数据,就导致Master节点新写入的数据丢失。

Redis Sentinel脑裂问题导致数据丢失

Redis Master节点每次写入新数据,都会异步发送给Slave节点,在Redis Sentinel集群环境中,如果某一个时刻Redis Master节点出现网络故障,Sentinel与Master之间、Master与Slave之间不能通信,但是Sentinel和两个Slave节点还能正常通信,此时Sentinel会根据Slave节点判断Master sdown的数量是否达到qorum数量,达到条件就认为Master odown了,Sentinel会选举出一个新的Master节点提供服务,并且把另一个Slave节点的主从配置指向新的Master节点。与此同时,Client端与原有的Redis Master之间的通信并未中断,那么此时就会出现两个Master节点同时接收写入数据,而且两者之间没有主从同步。这就是Sentinel集群中的脑裂现象。当原有的Redis Master节点网络恢复后,Sentinel会把它变成Slave节点指向新的Master节点,那么原来的Master节点中的数据会被清空,重新从新的Master节点同步全量数据。

解决这两类问题的思路

Master节点记录了上次与Slave节点数据同步的offset值,并把新增数据写入本地缓冲区,异步发送给Slave节点。如果Master节点与Slave节点网络上断开,那么Master节点的新增数据会产生堆积,Master可以判断每个Slave节点从上次同步到现在的延迟时间,如果延迟时间达到一定值T,就可以认为与Slave节点断开连接,不再提供数据写服务。从客户端再写入的新数据,需要通过代码进行特殊处理。

  • 配置Redis Master达到与Slave的断开条件后,Master停止提供数据写入服务
  • 对Redis Master数据写入接口进行限流、降级处理,把无法写入Redis的数据,临时写到消息队列中,保全数据,并再进行数据写入的重试

redis配置及代码实现

Redis配置

min-slaves-to-write 1

min-slaves-max-lag 10

两个选项相互关联,如果min-slaves-to-write配置为0,则Redis此特性表示关闭。

用此处的值解释为,至少有1个slave节点的数据同步延迟<=10s,否则master节点将停止接收一切请求。

Redis接口隔离和消息队列降级处理

采用hystrix对Redis Master接口进行隔离,当Master节点不可用时,Redis服务接口降级为消息队列异步处理。定时(例如5分钟)从消息队列中获取未保存到Redis中的数据重新尝试写入redis。

消息发送端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
public class SaveECourseInfo2RedisCmmd extends HystrixCommand<ECourseInfo> {
private ECourseInfo eCourseInfo;

public SaveECourseInfo2RedisCmmd(ECourseInfo eCourseInfo){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RedisGroup")));
this.eCourseInfo = eCourseInfo;
}

@Override
protected ECourseInfo run() throws Exception {
RedisService redisService = (RedisService) SpringContext.getContext().getBean(RedisService.class);
redisService.setValue(ECourseInfoKey.ID,eCourseInfo.getPkid()+"", JSONObject.toJSONString(eCourseInfo));
log.info("======="+ JSONObject.toJSONString(eCourseInfo)+"=========");
return eCourseInfo;
}

@Override
protected ECourseInfo getFallback() {
JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(eCourseInfo));
jsonObject.put("serviceId","rewriteEcourseRedisDataService");
EcKafkaSender sender = SpringContext.getContext().getBean(EcKafkaSender.class);
sender.sendMsg("redisDataRewrite",JSONObject.toJSONString(jsonObject));
return eCourseInfo;
}
}

消息接收端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Component
public class RedisDataRewriteScheduleKafkaConsumer {

@KafkaListener(topics="redisDataRewrite")
public void listen(ConsumerRecord<?, ?> cr) throws Exception{
log.info("=======收到消息["+cr.toString()+"]========");

//收到课程管理服务数据更新的消息message,根据message对象返回的数据判断要进行的缓存处理操作
JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(cr));
String serviceId = ((String) jsonObject.get("serviceId"));
if("rewriteEcourseRedisDataService".equals(serviceId)){
rewriteEcourseRedisData(jsonObject);
}
}

private void rewriteEcourseRedisData(JSONObject jsonObject) {
jsonObject.remove("serviceId");
ECourseInfo eCourseInfo = JSONObject.parseObject(JSONObject.toJSONString(jsonObject), ECourseInfo.class);
ReSaveECourseInfo2RedisCmmd reSaveECourseInfo2RedisCmmd = new ReSaveECourseInfo2RedisCmmd(eCourseInfo);
reSaveECourseInfo2RedisCmmd.execute();
}


}

备用方案

Redis配置min-slaves-to-write和min-slaves-max-lag两个选项,在代码层,把从Client端提交到Redis-Master的数据先保存到磁盘文件,然后定时从磁盘文件中读取数据重新写入Redis。

补充

这种解决方案尽可能减少Redis主从复制和Sentinel脑裂后Master节点丢失的数据,不是100%不丢失数据。

谢谢你请我吃糖果