三天的元旦节,导致大量的面试积压,从早上来公司就开始疯狂补作业。自身本来就对面试没有太多激情,比较排斥八股文,刷题怪的这种套路。
临近下班前,和一位候选人聊到了他的项目(本人比较乐意从项目的设计来考察技术的深度和广度)。

项目的背景大致是教育直播相关的,对于直播的一些必要且时效性不高的数据,通过Redis做主动缓存,应对课堂上大流量的请求。
一般这种回答,我都愿意继续问一问,看看有没有更加全面的考虑和一些必要的兜底预案,算是讨论交流。这位同学回答说,他们线上的 Redis 使用
读写分离,采用1主多从的架构部署。

按照经验来看,redis 单实例应对 10W 以内的读写qps,没有什么特别压力。但是,存储组件在应对主从分离时,面临最大的是数据一致性。所以,顺口问
了候选人有没有在项目中碰到过因为读写分离导致的一致性问题,候选人谈到因为redis的过期key,导致在读取时会读到过期的key。

我突然有些吃惊。根据自己的知识面和记忆: Redis在主从架构下,如果当前key已经过期,

  • 在通过master获取过期key时,会执行异步懒删除,并将删除指令传播到slave节点,同时返回给clint不存在;
  • 如果通过slave获取过期key时,由于主从同步之间网络耗时一定存在,会依据logical clock处理,同时返回key不存在

持着怀疑的态度向他确认了为什么会读到过期的key。在追问下,他仅告诉我在主节点读取时会将删除命令同步到从节点,而从节点的逻辑却没有提到,
而且信心满满的告诉我他昨天刚看完书,肯定是这样的………

fix_slave_read_expire
在github中可以找到,redis作者在这个 commit 中就修复了这一问题。

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val;

    if (expireIfNeeded(db,key) == 1) {
        /* If we are in the context of a master, expireIfNeeded() returns 1
         * when the key is no longer valid, so we can return NULL ASAP. */
        if (server.masterhost == NULL)
            goto keymiss;

        /* However if we are in the context of a slave, expireIfNeeded() will
         * not really try to expire the key, it only returns information
         * about the "logical" status of the key: key expiring is up to the
         * master in order to have a consistent view of master's data set.
         *
         * However, if the command caller is not the master, and as additional
         * safety measure, the command invoked is a read-only command, we can
         * safely return NULL here, and provide a more consistent behavior
         * to clients accessing expired values in a read-only fashion, that
         * will say the key as non existing.
         *
         * Notably this covers GETs when slaves are used to scale reads. */
        if (server.current_client &&
            server.current_client != server.master &&
            server.current_client->cmd &&
            server.current_client->cmd->flags & CMD_READONLY)
        {
            goto keymiss;
        }
    }
    val = lookupKey(db,key,flags);
    if (val == NULL)
        goto keymiss;
    server.stat_keyspace_hits++;
    return val;

keymiss:
    if (!(flags & LOOKUP_NONOTIFY)) {
        notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
    }
    server.stat_keyspace_misses++;
    return NULL;
}

在源码中可以看到,如果是过期 key 并且 当前实例不是master,直接返回null。

replica_del
这一段摘自Redis官方文档中的描述,解释了Redis在处理复制时对过期key的处理策略。其中第一条,说明了slave节点删除key的指令是来由master节点
同步而来。第二条便是说,在slave节点上通过 逻辑时钟 来提示key不存在,这个操作是不违反数据一致性的读取,因为master节点的del命令会到达。

最后,问一个开放性的问题,为什么这里没有直接删除呢?因为系统时钟是不可靠的,如果slave的时钟比master快,
key可能会在master之前过期,这时client在slave上读取并删除了实例上的key, 后续master可能会发送一个命令来设置key的新过期时间,
但由于key在slave上过期而失败。作者这个修改还是依赖master的命令来对slave上的key做写操作,只是对slave上存在
读请求时发生的不一致行为做了兼容。

所以,大家在学习”八股文”的时候,还是要结合代码或者官方文档推敲细节哟,哈哈哈~