如何解决Redis主从数据不一致问题
如何解决Redis主从数据不一致问题
Redis过期时间不一致
Redis社区版本在正常的主从复制也会出现过期时间不一致问题,主要是由于在主从进行全同步期间,如果主库此时有expire
命令,那么到从库中,该命令将会被延迟执行。因为全同步需要耗费时间,数据量越大,那么过期时间差距就越大。
Redis expire 命令主要实现如下
expireGenericCommand(c,mstime(),UNIT_SECONDS);
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
robj *key = c->argv[1], *param = c->argv[2];
long long when; /* unix time in milliseconds when the key will expire. */
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
return;
if (unit == UNIT_SECONDS) when *= 1000;
when += basetime;
...
expire 600
到Redis中过期时间其实是(当前timestamp+600)*1000
,最终Redis会存储计算后这个值。所以上面提到的情况,等到命令到从库的时候,当前的timestamp
跟之前的timestamp
不一样了,特别是发生在全同步后的expire
命令,延迟时间基本上等于全同步的数据,最终造成过期时间不一致。
这个问题其实已经是官方的已知问题,解决方案有两个
1. 业务采用expireat timestamp方式,这样命令传送到从库就没有影响。
2. 在Redis代码中将expire命令转换为expireat命令。
官方没有做第二个选择,反而是提供expireat
命令来给用户选择。其实从另外一个角度来看,从库的过期时间大于主库的过期时间,其实影响不大。因为主库会主动触发过期删除,如果该key删除之后,主库也会向从库发送删除的命令。但是如果主库的key已经到了过期时间,Redis没有及时进行淘汰,这个时候访问从库该key,那么这个key是不会被触发淘汰的,这样如果对于过期时间要求非常苛刻的业务还是会有影响的。
而且目前针对于我们大规模迁移的时间,在进行过期时间校验的时候,发现大量key的过期时间都不一致,这样也不利于我们进行校验。
所以针对第一个问题,我们将expire/pexpire/setex/psetex
命令在复制到从库的时候转换成时间戳的方式,比如expire
转成expireat
命令,setex
转换成set
和expireat
命令,具体实现如下
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
int flags)
{
if (server.aof_state != REDIS_AOF_OFF && flags & REDIS_PROPAGATE_AOF)
feedAppendOnlyFile(cmd,dbid,argv,argc);
if (flags & REDIS_PROPAGATE_REPL) {
if (!strcasecmp(argv[0]->ptr,"expire") ||
!strcasecmp(argv[0]->ptr,"setex") ||
!strcasecmp(argv[0]->ptr,"pexpire") ||
!strcasecmp(argv[0]->ptr,"psetex") ) {
long long when;
robj *tmpargv[3];
robj *tmpexpire[3];
argv[2] = getDecodedObject(argv[2]);
when = strtoll(argv[2]->ptr,NULL,10);
if (!strcasecmp(argv[0]->ptr,"expire") ||
!strcasecmp(argv[0]->ptr,"setex")) {
when *= 1000;
}
when += mstime();
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
if (!strcasecmp(argv[0]->ptr,"expire") ||
!strcasecmp(argv[0]->ptr,"pexpire")) {
tmpargv[0] = createStringObject("PEXPIREAT",9);
tmpargv[1] = getDecodedObject(argv[1]);
tmpargv[2] = createStringObjectFromLongLong(when);
replicationFeedSlaves(server.slaves,dbid,tmpargv,argc);
decrRefCount(tmpargv[0]);
decrRefCount(tmpargv[1]);
decrRefCount(tmpargv[2]);
}
/* Translate SETEX/PSETEX to SET and PEXPIREAT */
if (!strcasecmp(argv[0]->ptr,"setex") ||
!strcasecmp(argv[0]->ptr,"psetex")) {
argc = 3;
tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = getDecodedObject(argv[1]);
tmpargv[2] = getDecodedObject(argv[3]);
replicationFeedSlaves(server.slaves,dbid,tmpargv,argc);
tmpexpire[0] = createStringObject("PEXPIREAT",9);
tmpexpire[1] = getDecodedObject(argv[1]);
tmpexpire[2] = createStringObjectFromLongLong(when);
replicationFeedSlaves(server.slaves,dbid,tmpexpire,argc);
decrRefCount(tmpargv[0]);
decrRefCount(tmpargv[1]);
decrRefCount(tmpargv[2]);
decrRefCount(tmpexpire[0]);
decrRefCount(tmpexpire[1]);
decrRefCount(tmpexpire[2]);
}
} else {
replicationFeedSlaves(server.slaves,dbid,argv,argc);
}
}
}
Redis key数量不一致
Redis在做主从复制的时候,会对当前的存量数据做一个RDB快照(bgsave命令),然后将RDB快照传给从库,从库会解析RDB文件并且load到内存中。然儿在上述的两个步骤中Redis会忽略过期的key
1. 主库在做RDB快照文件的时候,发现key已经过期了,则此时不会将过期的key写到RDB文件中。
2. 从库在load RDB文件到内存中的时候,发现key已经过期了,则此时不会将过期的key load进去。
所以针对上述两个问题会造成Redis主从key不一致问题,这个对于我们做数据校验的时候会有些影响,因始终觉得key不一致,但是不影响业务逻辑。
针对上述问题,目前我们将以上两个步骤都改为不忽略过期key,过期key的删除统一由主库触发删除,然后将删除命令传送到从库中。这样key的数量就完全一致了。
最终在打上以上两个patch之后,再进行迁移测试的时候,验证key过期时间以及数量都是完全一致的。