精华内容
下载资源
问答
  • 如何解决Redis 主从数据不一致问题
    千次阅读
    2018-02-05 17:17:09

    线上问题

    近期我们在对Redis做大规模迁移升级的时候,采用模拟复制协议的方式进行数据传输同步。

    在此期间,我们遇到如下两个问题:

    1. 迁移前后Redis过期时间不一致。
    2. 迁移前后Redis key 数量不一致。

    迁移前后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会存储计算后这个值在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过期时间不一致问题解决,目前迁移前后的过期时间是严格保持一致的。

    迁移前后Redis key 数量不一致

    针对于第二个问题,Redis key 迁移前后数量不一致问题,其实在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过期时间以及数量都是完全一致的。
    最后贴上以上修改的代码(针对于社区版本Redis 3.0.7):

    以下代码修改均在我标记注释的下面

    1、做bgsave的时候不忽略过期的key

    rdb.c

    /* Save a key-value pair, with expire time, type, key, value.
     * On error -1 is returned.
     * On success if the key was actually saved 1 is returned, otherwise 0
     * is returned (the key was already expired). */
    int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                            long long expiretime, long long now)
    {
        /* Save the expire time */
        if (expiretime != -1) {
            /* If this key is already expired skip it */
            /* 注释下面这一行 */
            /* if (expiretime < now) return 0; */                                                                                                                                                
            if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
            if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
        }   
    
        /* Save type, key, value */
        if (rdbSaveObjectType(rdb,val) == -1) return -1;
        if (rdbSaveStringObject(rdb,key) == -1) return -1;
        if (rdbSaveObject(rdb,val) == -1) return -1;
        return 1;
    }

    2、做bgrewirteaof 的时候不忽略过期的key

    aof.c

    int rewriteAppendOnlyFile(char *filename) {
        dictIterator *di = NULL;
        dictEntry *de;
        rio aof;
        FILE *fp;
        char tmpfile[256];
        int j;
        long long now = mstime();
        char byte;
        size_t processed = 0;
    
        /* Note that we have to use a different temp name here compared to the
         * one used by rewriteAppendOnlyFileBackground() function. */
        snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
        fp = fopen(tmpfile,"w");
        if (!fp) {
            redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
            return REDIS_ERR;
        }   
    
        server.aof_child_diff = sdsempty();
        rioInitWithFile(&aof,fp);
        if (server.aof_rewrite_incremental_fsync)
            rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);
        for (j = 0; j < server.dbnum; j++) {
            char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
            redisDb *db = server.db+j;
            dict *d = db->dict;
            if (dictSize(d) == 0) continue;
            di = dictGetSafeIterator(d);
            if (!di) {
                fclose(fp);
                return REDIS_ERR;
            }   
    
            /* SELECT the new DB */
            if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
            if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
    
            /* Iterate this DB writing every entry */  
            while((de = dictNext(di)) != NULL) {
                sds keystr;
                robj key, *o;
                long long expiretime;
     
                keystr = dictGetKey(de);
                o = dictGetVal(de);
                initStaticStringObject(key,keystr);
     
                expiretime = getExpire(db,&key);
     
                /* If this key is already expired skip it */
                /* 注释下面这一行 */
                /* if (expiretime != -1 && expiretime < now) continue; */

    3、在load rdb 的时候不忽略过期key

    rdb.c

    int rdbLoad(char *filename) {
        uint32_t dbid;
        int type, rdbver;
        redisDb *db = server.db+0;
        char buf[1024];
        long long expiretime, now = mstime();
        FILE *fp;
        rio rdb;
    
        if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR;
    
        rioInitWithFile(&rdb,fp);
        rdb.update_cksum = rdbLoadProgressCallback;
        rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
        if (rioRead(&rdb,buf,9) == 0) goto eoferr;
        buf[9] = '\0';
        if (memcmp(buf,"REDIS",5) != 0) {
            fclose(fp);
            redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");
            errno = EINVAL;
            return REDIS_ERR;
        }
        rdbver = atoi(buf+5);
        if (rdbver < 1 || rdbver > REDIS_RDB_VERSION) {
            fclose(fp);
            redisLog(REDIS_WARNING,"Can't handle RDB format version %d",rdbver);
            errno = EINVAL;
            return REDIS_ERR;
        }
    
        startLoading(fp);
        while(1) {
            robj *key, *val;
            expiretime = -1;
    
            /* Read type. */
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
            if (type == REDIS_RDB_OPCODE_EXPIRETIME) {
                if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr;
                /* We read the time so we need to read the object type again. */
                if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
                /* the EXPIRETIME opcode specifies time in seconds, so convert
                 * into milliseconds. */
                expiretime *= 1000;
            } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) {
                /* Milliseconds precision expire times introduced with RDB
                 * version 3. */
                if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr;
                /* We read the time so we need to read the object type again. */
                if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
            }
     
            if (type == REDIS_RDB_OPCODE_EOF)
                break;
     
            /* Handle SELECT DB opcode as a special case */
            if (type == REDIS_RDB_OPCODE_SELECTDB) {
                if ((dbid = rdbLoadLen(&rdb,NULL)) == REDIS_RDB_LENERR)
                    goto eoferr;
                if (dbid >= (unsigned)server.dbnum) {
                    redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum);
                    exit(1);
                }
                db = server.db+dbid;
                continue;
            }
            /* Read key */
            if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
            /* Read value */
            if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
            /* Check if the key already expired. This function is used when loading
             * an RDB file from disk, either at startup, or when an RDB was
             * received from the master. In the latter case, the master is
             * responsible for key expiry. If we would expire keys here, the
             * snapshot taken by the master may not be reflected on the slave. */
             /* 注释下面5行 */
            /* if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
                decrRefCount(key);
                decrRefCount(val);
                 continue;
            } */
            /* Add the new object in the hash table */
            dbAdd(db,key,val);
     
            /* Set the expire time if needed */
            if (expiretime != -1) setExpire(db,key,expiretime);
     
            decrRefCount(key);
        }

    总结

    注意上述修改在内存策略为noeviction一直有效,但是其他内存策略只能在Redis 使用内存小于最大内存的时候才会有效,因为从库在使用内存超过最大内存的时候也会触发淘汰,这个时候也没法完全保证数据一致性了。

    更多相关内容
  • Redis是一种高性能的内存数据库;而MySQL是基于磁盘文件的关系型数据库,相比于Redis来说,读取速度会慢一些,但是功能强大,可以用于存储持久化的数据。在实际工作中,我们常常将Redis作为缓存与 ...

    Redis是一种高性能的内存数据库;而MySQL是基于磁盘文件的关系型数据库,相比于Redis来说,读取速度会慢一些,但是功能强大,可以用于存储持久化的数据。在实际工作中,我们常常将Redis作为缓存与MySQL配合来使用,当有数据访问请求的时候,首先会从缓存中进行查找,如果存在就直接取出,如果不存在再访问数据库,这样就提升了读取的效率,也减少了堆后端数据库的访问压力。可以说使用Redis这种缓存架构是高并发架构中非常重要的一环。

    在这里插入图片描述

    当然我们也可以对MySQL做主从架构并且进行读写分离,让主服务器(Master)处理写请求,从服务器(Slave)处理读请求,这样同样可以提升数据库的并发处理能力。不过主从架构的作用不止如此,我们今天就从下面几个方面了解一下它:

    1、为什么需要主从同步,设置主从同步有什么样的作用?
    2、主从同步的原理是怎样的?在进行主从同步的同时会引入哪些问题?
    3、为了保证主从同步的数据一致性,都有哪些方案?

    为什么需要主从同步

    首先不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的,如果我们的目的在于提升数据库高并发访问的效率,那么首先需要考虑的应该是如何优化你的SQL和索引,这种方式简单有效,其次才是采用缓存的策略,比如使用Redis,通过Redis高性能的优势把热点数据保存在内存数据库中,提升读取的效率,最后才是对数据库采用主从架构,进行读写分离。

    按照上面的方式进行优化,使用和维护的成本是由小到大的。

    主从同步设计不仅可以提升数据库的吞吐量,还有以下三个方面的作用。

    首先是可以读写分离,我们可以通过主从复制的方式来同步数据,然后通过读写分离提升数据库的并发处理能力。

    简单来说就是同一份数据被放在了多个数据库中,其中一个数据库是Master主库,其余的多个数据库是Slave从库。当主库进行更新的时候,会自动将数据复制到从库中,而我们在客户端读取数据的时候,会从从库进行读取,也就是采用读写分离的方式。互联网的应用往往是“读多写少”的需求,采用读写分离的方式,可以实现更高的并发访问。原本所有的读写压力都由一台服务器承担,现在有多个“兄弟”帮忙处理读请求,这样就减少了对后端大哥(Maste)的压力。同时,我们还能对从服务器进行负载均衡,让不同的读请求按照策略均匀的分配到不同的从服务器中,让读取更加顺畅。读取顺畅的另一个原因,就是减少了锁表的影响,比如我们让主库负责写,当主库出现写锁的时候,不会影响到从库进行SELECT操作。

    第二个作用就是数据备份。我们通过主从复制将主库上的数据复制到了从库上,相当于是一种热备份机制,也就是在主库正常运行下进行备份,不会影响到服务。

    第三个作用是具有高可用性。我刚才讲的数据备份实际上是一种冗余的机制,通过这种冗余的方式可以换取数据库的高可用性,也就是当服务器出现故障或者宕机的情况下,可以切换到从服务器上,让从服务器充当主服务器,保证服务的正常运行。

    关于高可用性的程度,我们可以用一个指标衡量,既正常可用时间 / 全年时间。比如要达到全年99.999%的时间都可用,就意味着系统在一年中的不可用时间不得超过5.256分钟,其他时间都需要保持可用的状态。需要注意的是,这5.256分钟包括了系统崩溃的时间,也包括了日常维护操作导致的停机时间。

    事实上,更高的高可用性,意味着需要付出更高的成本代价,在现实中我们需要结合业务需求和成本来进行选择。

    主从同步的原理是怎样的

    提到主从同步的原理,我们就需要了解在数据库中的一个重要日志文件,那就是Binlog二进制文件,它记录了对数据库进行更新的事件,事实上主从同步的原理就是基于Binlog进行数据同步的。在主从复制过程中,会基于三个线程来操作,一个主库线程,两个从库线程。

    二进制日志转储线程是一个主库线程。当从库线程连接的时候,主库可以将二进制日志发送到从库,当主库读取事件的时候,会在Binglog上加锁,读取完成之后,再将锁释放掉。

    从库IO线程会连接到主库,向主库发送请求更新Binlog,这时从库的IO线程就可以读取到主库的二进制日志转储线程发送的Binlog更新部分,并且拷贝到本地形成中继日志(Relay log)。

    从库SQL线程会读取从库中的中继日志,并且执行日志中的事件,从而将从库中的数据与主库保持同步。

    在这里插入图片描述

    你能看到主从同步的内容就是二进制日志(Binlog),它虽然叫二进制日志,实际上存储的是一个又一个的事件(Event),这些事件分别对应着数据库的更新操作,比如INSERT、UPDATE、DELETE等。另外我们还需要注意的是,不是所有版本的MySQL都默认开启了服务器的二进制日志,在进行主从同步的时候,我们需要先检查服务器是否已经开启了二进制日志。

    进行主从同步的内容是二进制日志,它是一个文件,在进行网络传输的过程中就一定会存在延迟,比如500ms,这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的数据不一致问题。比如我们对一条记录进行更新,这个操作是在主库上完成的,而在很短的时间内,比如100ms,又对同一个记录进行读取,这时候从库还没有完成数据的读取,那么,我们通过从库读取到的数据就是一条旧的数据。

    这种情况下该怎么办呢?

    如何解决主从同步的数据一致性问题

    可以想象下,如果我们想要操作的数据都存储在同一个数据库中,那么对数据进行更新的时候,可以对记录进行加写锁,这样在读取的时候就不会发生数据不一致的情况了。但这时从库的作用就是备份数据,没有做到读写分离,分担主库的压力。

    在这里插入图片描述

    因此我们还需要想办法,在进行读写分离的时候,解决主从同步中数据不一致的问题,也就是解决主从之间数据复制方式的问题,如果按照数据一致性从弱到强来进行划分,有以下三种复制方式。

    方式1:异步复制

    异步模式就是客户端提交COMMIT之后不需要等从库返回任何结果,而是直接将结果返回给客户端,这样做的好处就是不会影响主库的执行效率,但是可能会存在主库宕机,而Binlog还没有同步到从库的情况,也就是主库和从库的数据不一致问题,这时候从从库中选一个作为新的主库,那么,新的主库则可能会缺少原来主服务器中已经提交的事务。所以,这种复制模式下的数据一致性是最弱的。

    在这里插入图片描述

    方式2:半同步复制

    MySQL5.5版本之后开始支持半同步复制的方式。原理是在客户端提交COMMIT之后不直接将结果返回给客户端,而是等待至少有一个从库收到了Binlog,并且写入到中继日志中,再返回给客户端。这样做的好处就是提高了数据的一致性,当然相比于异步复制来说,至少多增加了一个网络连接的延迟,降低了主库写的效率。

    在MySQL5.7版本中还增加了一个rpl_semi_sync_master_wait_for_slave_count参数,我们可以对应搭的从库数量进行设置,默认为1,也就是说只要有一个从库进行了响应,就可以返回给客户端。如果将这个参数调大,可以提升数据一致性的强度,但也会增加主库等待从库响应的时间。

    在这里插入图片描述

    方式3:组复制

    组复制技术,简称MGR。是MySQL在5.7.17版本中推出的一种新的数据复制技术,这种复制技术是基于paxos协议的状态机复制。

    我刚才介绍的异步复制和半同步复制都无法最终保证数据的一致性问题,半同步复制是通过判断从库响应的个数来决定是否返回给客户端,虽然数据一致性相比于异步复制有提升,但仍然无法满足对数据一致性要求高的场景,比如金融领域。MGR 很好地弥补了这两种复制模式的不足。

    下面我们来看下 MGR 是如何工作的(如下图所示)。

    首先我们将多个节点共同组成一个复制组,在执行读写(RW)事务的时候,需要通过一致性协议层(Consensus)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应Node节点)的同意,大多数指的是同意的节点数量要大于N/2+1,这样才可以进行提交,而不是一方说了算。而针对只读(RO)事务则不需要经过组内同意,直接COMMIT即可。

    在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了源自消息和全局有序消息,从而保证组内数据的一致性。(具体原理点击这里可以参考)。

    在这里插入图片描述

    MGR将MySQL带入了数据强一致性的时代,是一个划时代的创新,其中一个重要原因是MGR是基于paxos协议的。PAxos算法是由2013 年的图灵奖获得者 Leslie Lamport 于 1990 年提出的,有关这个算法的决策机制你可以去网上搜下。

    事实上,Paxos算法提出来之后就作为分布式一致性算法被广泛使用,比如Apache的Zookeeper也是基于paxos算法实现的。

    总结

    我今天讲解了数据库的主从同步,如果你的目标仅仅是数据库的高并发,那么可以先从 SQL 优化,索引以及 Redis 缓存数据库这些方面来考虑优化,然后再考虑是否采用主从架构的方式。

    在主从架构的配置中,如果想要采取读写分离的策略,我们可以自己编写程序,也可以通过第三方的中间件来实现。

    自己编写程序的好处就在于比较自主,我们可以自己判断哪些查询在从库上来执行,针对实时性要求高的需求,我们还可以考虑哪些查询可以在主库上执行。同时,程序直接连接数据库,减少了中间件层,相当于减少了性能损耗。

    采用中间件的方法有很明显的优势,功能强大,使用简单。但因为在客户端和数据库之间增加了中间件层会有一些性能损耗,同时商业中间件也是有使用成本的。我们也可以考虑采取一些优秀的开源工具,比如 MaxScale。它是 MariaDB 开发的 MySySQL 数据中间件。比如在下图中,使用 MaxScale作为数据库的代理,通过路由转发完成了读写分离。同时我们也可以使用 MHA 工具作为强一致的主从切换工具,从而完成 MySQL的高可用架构。

    在这里插入图片描述

    在这里插入图片描述

    展开全文
  • 在聊数据库与缓存一致问题之前,先聊聊数据库主库与从库的一致问题。 问:常见的数据库集群架构如何? 答:一主多从,主从同步,读写分离。 如上图: (1)一个主库提供写服务 (2)多个从库提供读服务...

    在聊数据库与缓存一致性问题之前,先聊聊数据库主库与从库的一致性问题。

     

    问:常见的数据库集群架构如何?

    :一主多从,主从同步,读写分离。

    如上图:

    (1)一个主库提供写服务

    (2)多个从库提供读服务,可以增加从库提升读性能

    (3)主从之间同步数据

    画外音:任何方案不要忘了本心,加从库的本心,是提升读性能。

     

    问:为什么会出现不一致?

    :主从同步有时延,这个时延期间读从库,可能读到不一致的数据。

    如上图:

    (1)服务发起了一个写请求

    (2)服务又发起了一个读请求,此时同步未完成,读到一个不一致的脏数据

    (3)数据库主从同步最后才完成

    画外音:任何数据冗余,必将引发一致性问题。

     

    问:如何避免这种主从延时导致的不一致?

    :常见的方法有这么几种。

     

    方案一:忽略

    任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。

    画外音:如果业务能接受,最推崇此法。

     

    如果业务能够接受,别把系统架构搞得太复杂。

     

    方案二:强制读主

    如上图:

    (1)使用一个高可用主库提供数据库服务

    (2)读和写都落到主库上

    (3)采用缓存来提升系统读性能

    这是很常见的微服务架构,可以避免数据库主从一致性问题。

     

    方案三:选择性读主

    强制读主过于粗暴,毕竟只有少量写请求,很短时间,可能读取到脏数据。

     

    有没有可能实现,只有这一段时间,可能读到从库脏数据的读请求读主,平时读从呢?

     

    可以利用一个缓存记录必须读主的数据。

    如上图,当写请求发生时:

    (1)写主库

    (2)将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”

    画外音:key的格式为“db:table:PK”,假设主从延时为1s,这个key的cache超时时间也为1s。

     

    如上图,当读请求发生时:

    这是要读哪个库,哪个表,哪个主键的数据呢,也将这三个信息拼装一个key,到cache里去查询,如果,

    (1)cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询

    (2)cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询

    以此,保证读到的一定不是不一致的脏数据。

     

    总结

    数据库主库和从库不一致,常见有这么几种优化方案:

    (1)业务可以接受,系统不优化

    (2)强制读主,高可用主库,用缓存提高读性能

    (3)在cache里记录哪些记录发生过写请求,来路由读主还是读从

     

    文字很短,不能解决所有问题,但希望能给大家一些启示。

    展开全文
  • 这次准备开启一个新的系列来写了,聊聊分布式系统中的关注点。节奏不会排的太紧凑,计划两周一更吧。  本文是本系列的第二篇。是前一篇《知道是不是最通俗易懂的《数据... 已经对数据一致问题做了一次剖析...

    这次准备开启一个新的系列来写了,聊聊分布式系统中的关注点。节奏不会排的太紧凑,计划两周一更吧。

      本文是本系列的第二篇。是前一篇《不知道是不是最通俗易懂的《数据一致性》剖析了》的后续内容。

      前一篇可能讲的过于通俗,逼格不高,不太受大家待见。。本篇会继续坚持尽量讲的通俗易懂,坚信让更多的人看懂才有更大的价值。不过相对来说内容的专业度有所上升。

      已经对数据一致性问题做了一次剖析,那么怎么解决由于故障导致的不一致问题呢?本文会围绕“共识”这个点展开。

     

    01 “共识”是什么?为什么会产生?

    一致性问题其实是一个「结果」,本质是由于数据冗余导致的,如果没有冗余,也就不会有一致性问题了。

    分布式系统里的各个子系统之间之所以能够相互协作,就是因为其之间冗余了相同的数据作为“信物”,要不然我都不认识你的话,为什么要配合你干活呢。所以这个“信物”变了,你得通知我,要不然我又不认识你了。这个“信物”变更达成一致性的过程称作达成「共识」。所以:

    一致性问题是结果,共识是为达到这个结果所要经过的过程,或者说一种手段。

    在分布式系统中,冗余数据的场景不限于此,因为规模越大的系统,越不能容忍某一个子系统出问题后产生蝴蝶效应,所以往往会做高可用。小明1号倒下了还有千千万万个小明X号在坚守岗位,理想中的全天候24小时提供服务~。高可用的本质是通过相同数据存储多个副本,并都可对外提供服务。比如每个小明X号都有一本《按摩指法白皮书》,谁请假了都可以由其它小明X号提供相同的按摩服务。但是这个本《按摩指法白皮书》改了,就得通知到每个人,因为这是服务的全部和来源,所以在做了高可用的集群中数据冗余的问题更为突出。

     

    实际上,如果分布式系统中各个节点都能保证瞬时响应、无故障运行,则达成共识很容易。就好像我们人一样,在一定范围内只要吼一嗓子,通过稳定的空气传播,相关人是否接收到这个消息,并且给出响应几乎可以是“瞬时”的。但是正如〖上篇,←点我〗文中提到,这样的系统只停留在想象中,响应请求往往存在延时,网络会发生中断,节点发生故障,甚至存在恶意节点故意要破坏系统。这就衍生出了经典的「拜占庭将军问题」[1]。

     

    02 拜占庭将军问题

    我们一般把「拜占庭将军问题」分为2种情况来看待:

    1. 拜占庭错误。表示通过伪造信息进行恶意响应产生的错误。

    2. 非拜占庭错误。没有进行响应产生的错误。

    这个问题的核心在于:

    如何解决某个变更在分布式网络中得到一致的执行结果,是被参与多方都承认的,同时这个信息是被确定的,不可推翻的。

    好比如何让所有的小明X号收到的都是《按摩指法白皮书Ⅱ》,而不是其它的,并且把原来的那本销毁掉。这个问题衍生出了很多“共识”算法,解决「拜占庭错误」的称作Byzantine Fault Tolerance(BFT)类算法,解决「非拜占庭错误」的称作Crash Fault Tolerance(CFT)类算法。从这个2个名字中也可以看出,本质的工作就是「容错」。有的小伙伴在平时的工作中可能对「容错」的重要性感知没那么强烈,不就产生一个BUG或者异常数据么,但是在航天领域,一个小错误可能导致整个发射的失败,代价非常巨大。

    对「拜占庭将军问题」想深入的了解的,可以自行查阅相关资料,这里就不展开了,文末附上提出时的论文。

     

    我们常见的软件开发中一般不会考虑「拜占庭错误」,但它是区块链项目的必需品。不过在主流的分布式数据库中,皆能看到「非拜占庭错误」的身影,诸如Tidb的Paxos算法,CockroachDB的Raft算法。虽然我们大家在日常的coding中,对数据库底层原理的了解并不是必须项。但是只要当我们涉及到应用程序级别的高可用时,那么至少「非拜占庭错误」是必须要面临的一道坎。

     

    03 BFT类算法

    BFT类型算法又有2个分支。「基于确定性的」和「基于概率的」。

     

    先聊聊「基于确定性的」,此类算法表示一旦对某个结果达成共识就不可逆转,即共识是最终结果。它的代表作是PBFT(Practical Byzantine Fault Tolerance)算法[2],自从有了央行背书(区块链数字票据交易平台),名声更大了。算法的原理,如下图:

    ▲图片来源于网络,版权归原作者所有

     

    拿军队来比喻,这里的直线C可以认为是“总司令”,直线0是“军长”,直线1、直线2、直线3都是“师长”,值得注意的是3号师长叛变了。整个过程这样解释:

    1. 「request」:总司令给军长下了一个命令,“干!”。

    2. 「pre-prepare」:军长把命令又广播给3个师长。

    3. 「prepare」:每个师长收到并同意之后将发送“收到”给军长和其他两个师长。

    4. 「commit」:每个师长收到2f个师长(军长不做prepare)的“收到”请求后发送“随时开干”给军长和其他两个师长。(f为可容忍的拜占庭节点数)

    5. 「reply」:每个师长收到2f+1条“随时开干”消息之后,就能认为总司令的命令在相关的师长中都到达了“随时开干”的状态,那么他就直接开炮了!

    真正深入了解PBFT的话还有很多内容,这里就不继续展开了,有兴趣的小伙伴自行查阅文末论文地址或者关注公众号后直接后台回复“一致性”打包下载

     

    再聊聊「基于概率的」,此类算法的共识结果则是临时的,随着时间推移或某种强化,共识结果被推翻的概率越来越小,成为事实上的最终结果。它的代表作是PoW(Proof of Work)算法,曾经高达2W美元/个的比特币就是基于这个算法来实现的。算法的原理拿“修仙”来做个简单的比喻(实际比特中的算法比这更复杂):

    • 自己努力修炼,并让神仙中大于一半的人认可你的修为,同意你成仙。

    • 随之你就成为了神仙。并且参与到评判后续其他人是否可以成为“神仙”的事情中去。

    • 这个事情如果想通过贿赂来达到的话,随着这个团队的人数越多,贿赂的成本越大,就可以认为去做贿赂的人越少,那么导致被误判的概率就越低,最终就越可信。

    被误判的概率公式是: 0.5 ^ 个数,如果个数=6的话,误判的概率是1.5625%。如果个数=10的话,就已经是0.09765625%了,指数级下降。

     

    值得注意的是,「基于确定性的」和「基于概率的」对于不合作节点的标准是不同的,前者至多能容忍1/3,后者是小于1/2

     

    04 CFT类算法

    正如上面所说CFT类算法解决的是分布式系统中存在故障,但不存在恶意节点的场景(即可能消息丢失或重复,但无错误消息)下的共识达成问题。「拜占庭将军问题」的提出者Leslie Lamport也在他另外的论文[3]中提出过「Paxos问题」,与这相似。在论文中通过一个故事类比了这个问题,如下:

    希腊岛屿Paxon 上的「执法者」在「议会大厅」中表决通过『法律』,并通过「服务员」传递纸条的方式交流信息,每个「执法者」会将通过的『法律』记录在自己的「账目」上。问题在于「执法者」和「服务员」都不可靠,他们随时会因为各种事情离开「议会大厅」,并随时可能有新的「执法者」进入「议会大厅」进行法律表决。

     

    使用何种方式能够使得这个表决过程正常进行,且通过的『法律』不发生矛盾。

            —— 百度百科

    这里的关键对象在我们的系统中,可以类比为:

    • 议会大厅 = 分布式系统

    • 执法者 = 某个程序

    • 服务员 = RPC通道

    • 账目 = 数据库

    • 法律 = 一次变更操作

     

    Leslie Lamport自己也提出了解决这个问题的算法,「Paxos」算法[4]。这个算法的关键由以下3个定义来体现:

    • 每次“变更”都有个唯一的序号,并且能够通过它识别新旧

    • 「执法者」只能接受比已知的“变更”更新的变更

    • 任意两次“变更”必须有相同的「执法者」参与

    这3点仅仅是保证一致性的最关键部分,全部内容还有很多。有兴趣的小伙伴自行查阅文末论文地址或者关注公众号后直接后台回复“一致性”打包下载

     

    「Paxos」算法是一种无领导人(Leaderless)算法,实现比较复杂,所以产生了很多变种来简化它,其中名气最大的应该是「Raft」,2013年才问世。「Raft」算法是一种领导人(Leadership)的算法。由以下2个过程保证达成共识:

    • 只会存在一个活着的领导人,领导人负责跟随者的数据同步。

    • 如果领导人“失联”了,那么每个跟随者都可成为候选人,最终比较谁的term最新,谁就是新的领导人。这个term是每个节点内部维护的一个自增数。

    虽然跟随者的投票秉承先到先得,但是还是会遇到多个term相同的候选人获得了相同票数(简称「分割投票问题」),那么进行新一轮投票,直到决出胜负为止。由于Raft用随机定时器来自增term,加上网络是不稳定的,所以再次遇到相同票数的概率就大大降低了。

    完整的过程更复杂一些,有一个Raft算法的动画推荐给大家,有兴趣的可以了解一下:http://thesecretlivesofdata.com/raft/。

     

    题外话,大家经常用的Zookeeper里的「ZAB」(ZooKeeper Atomic Broadcast)算法也是CFT类算法,是以Fast Paxos算法为基础实现的。

     

    05 结语

    回过头来看,我们发现,想要更严谨的一致性,那么就需要增加相互通讯确认的次数,但是这会导致性能低下,正如PBFT和Paxos一样。但是分布式系统就是这样,到处都需要Balance,找到最适合的才是最重要的

    聊完了数据层面的「共识」问题,我们下回再聊聊「分布式事务」的问题,围绕着常见的CAP、BASE理论展开。

    最后如果说想成为数据一致性专家,问有没有捷径的话。去阅读老爷子Leslie Lamport的论文就是捷径,他的个人主页:http://www.lamport.org/ 

     

    ▶ 微信后台回复“一致性”关键字,可打包下载哟~

    [1]《The Byzantine Generals Problem, ACM Transactions on Programming Languages and Systems》,Leslie Lamport,1982。

    链接:https://www.microsoft.com/en-us/research/uploads/prod/2016/12/The-Byzantine-Generals-Problem.pdf

    [2]《Practical Byzantine Fault Tolerance》,Miguel Castro&Barbara Liskov,1999。

    链接:http://101.96.10.63/pmg.csail.mit.edu/papers/osdi99.pdf

    [3]《The Part-Time Parliament》,Leslie Lamport,1998。

    链接:https://www.microsoft.com/en-us/research/uploads/prod/2016/12/The-Part-Time-Parliament.pdf

    [4]《In Search of an Understandable Consensus Algorithm》,Diego Ongaro&John Ousterhout,2013

    链接:https://raft.github.io/raft.pdf

     

    ▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码~。

    定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

    如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

    如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

    展开全文
  • 数据库集群架构 目前流行的数据库集群架构包括以下三点: 一主多从:高可用方案,主库挂掉,从库会变成...忽略:在业务保证数据一致性的情况下,可以选择忽略(技术永远是为业务提供服务的!); 强制性读写主库:
  • springcloud session不一致问题解决方法

    千次阅读 2018-11-19 16:40:45
    我们在分布式应用的时候通常会使用负载均衡或路由网关zuul实现负载,但是使用负载均衡必定会遇到session不一致问题解决办法: 使用spring-session和redis解决spring cloud中session不一致问题,具体如下: 1,...
  • 数据一致解决方案

    千次阅读 2022-03-12 21:21:17
    数据一致解决方案 CAP理论 C:一致性、A:可用性、P:分区容错性 CAP只能满足两个 CA:两阶段提交的严格选举协议 CP弱A:RAFT协议等多数派选举协议 AP:GOSSIP等冲突解决协议 数据一致性 时间一致性:所有相关数据...
  • RocketMQ解决数据一致性的问题

    千次阅读 2019-08-12 10:32:35
    整个系统架构的演变过程: 串行---->并行------>MQ解耦执行
  • 面试官:谈谈Redis缓存和MySQL数据一致问题

    千次阅读 多人点赞 2020-11-10 12:24:11
    重在穿透吧,也就是访问透过redis直接经过mysql,通常是一个存在的key,在数据库查询为null。每次请求落在数据库、并且高并发。数据库扛不住会挂掉。 解决方案 可以将查到的null设成该key的缓存对象。 当然,也...
  • 二、解决Redis 和数据库数据不一致的方法总结 前言 Redis 缓存经常会遇到有 4 个方面: 缓存中的数据和数据库中的不一致; 缓存雪崩; 缓存击穿; 缓存穿透。 只要我们使用 Redis 缓存,就会面对缓存和数据库间...
  • 解决Redis缓存和数据库值不一致问题

    千次阅读 2020-10-23 16:12:56
    同步写回策略:就是写入缓存时,必须要写入数据库,保证数据一致性。 异步写回策略:想写入缓存,等到缓存失效或者淘汰的时候,再写入数据库,此时如果还没有写入数据库,缓存就挂了,这样数据
  • 这个业务场景,主要是解决数据从Redis缓存,一般都是按照下图的流程来进行业务操作。 读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MyS...
  • 解决Shape数据形状数与表记录数不一致问题

    万次阅读 热门讨论 2018-06-14 19:46:36
    我们在用ArcGIS编辑Shape数据的时候,有时候会遇到编辑的过程中崩溃或者点断电后再打开Shape数据,提示打开要素类时出错,形状数与表记录数不一致问题问题提示如下: , (1)原因分析:出现这个问题,用...
  • 在互联网中大型项目中,读写分离应该是我们小伙伴经常听说的,这个主要解决大流量请求时,提高系统的吞吐量。因为绝大部分互联网产品...为什么产生数据不一致 上图中业务流程 1)写请求A进行数据更新,但写库...
  • 通常在高性能要求的场景,我们的系统设计会把数据存储到DB,然后冗余一份数据在缓存中。读请求优先从缓存读取数据,未命中缓存再从DB读取,如下...因为数据在DB跟缓存中各自保存了一份,如何保证它们之间的数据一致...
  • 缓存数据一致性-解决方案

    千次阅读 2022-03-10 16:21:31
    缓存数据一致性-解决方案
  • 更新数据时redis缓存与数据库数据不一致问题

    万次阅读 多人点赞 2019-01-17 10:52:35
    最初级的缓存不一致问题解决方案 问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据数据就出现了不一致解决思路:先删除缓存,再修改数据库。如果数据库...
  • pr导出视频结束时,提示: Pr文件导入器检测到的文件结构不一致已禁止读取和写入此文件的元数据无法将XMP数据写入输出文件。 报错原因:电脑硬盘存储空间不足。 解决办法:清理释放存储空间。 ...
  • 数据一致问题 写作背景:看了些视频,网上也看了很多篇文章,感觉写得都很片面,不是很全,所以我整体总结了一些,我喜欢重复造轮子,对于网上很多篇复制机类型的文章(很多文章所有字体相同)且很多正确严重...
  • 解决步骤如下: 1.先进入主库,进行锁表,防止数据写入 使用命令: mysql&gt; flush tables with read lock; 注意:该处是锁定为只读状态,语句区分大小写 2.进行数据备份 把数据备份到mysql.bak.sql...
  • 在高并发场景下,肯定会发生这个问题,这里简单谈谈解决思路 1.常规简单的解决方案  先删除缓存,在更新数据库,如果删除缓存成功,修改数据库失败了,那么数据库中依然是旧数据,如果去读取数据的时候,发现缓存...
  • 后端返回数据preview和response不一致 (翻译成专业术语就是:JS处理后台返回的Long型数据精度丢失) 问题分析: JS在处理返回数据类型是Long的时候,精度会丢失一部分!!! 问题原因: JS内置有32位整数,...
  • Redis与MySQL数据一致问题详解

    千次阅读 2019-02-25 18:07:03
    要求强一致性的读请求,走redis,要求强一致性的直接从mysql读取。 写请求: 数据首先都写到数据库,之后更新redis(先写redis再写mysql,如果写入失败事务回滚会造成redis中存在脏数据)。 【2】MySQL和Redis...
  • Redis和DB数据不一致解决方案

    千次阅读 2018-02-21 00:28:37
    还是先删缓存,再写库,都有可能出现数据不一致的情况 1、在并发中是无法保证读写的先后顺序的,如果删掉了缓存还没来得及写库,另一个线程就过来读取发现缓存为空就去数据库读取并写入缓存,此时缓存中为脏数据。 ...
  • axios.post跨域请求...①axios设置标题和转换请求数据 var HTTP = axios.create({ baseURL:'http://localhost:8080/', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, transformRequest:...
  • MySQL主从不一致情形与解决方法

    万次阅读 多人点赞 2018-07-14 18:50:55
    一、MySQL主从不同步情况 1.1 网络的延迟 由于mysql主从复制是基于binlog的一种异步复制 ...1.2 主从两台机器的负载不一致 由于mysql主从复制是主数据库上面启动1个io线程,而从上面启动1个sq...
  • java中的long能表示的范围比js中number大,也就意味着部分数值在js中存下(变成准确的值). 详情参考这里http://stackoverflow.com/questions/17320706/javascript-long-integer rest接口返回的json字符串中, ...
  • 情况时这样的 ...要解决问题就是怎么对比不一致,然后在影响业务的情况下,修复数据不一致问题,把从库缺少的数据补上 下面是能想到和找到的几个方案 1 从新从0开始同步,虽然对主库的使用没有影响,
  • 数据一致问题

    万次阅读 2018-09-18 10:44:36
    1.数据库数据的正确性 事务的ACID四个特性保证了一个事务的正确性。 1.原子性,简单来说就是一件事...防止多个事务交叉执行而导致数据不一致。事务隔离分为读未提交、读提交、可重复读和串行化。 4.持久性,事务...
  • 数据库事务特性包括原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily)。以下单为例,在单体的电商系统中,调用下单服务,整个服务操作在同一事务中完成。下单成功以后,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,106,072
精华内容 442,428
关键字:

如何解决数据不一致问题