percolator 逻辑代码分析

1 整个逻辑时间是作为版本号
2 单行事务有底层数据库保障
3 2PC的典型实现
4 外部需要Chubby做协调
总结: 用单行事务实现跨行、 跨表事务

场景分析过程(单行事务本身就存在由底层bigtable决定 )

1 初始状态, bob 账户有10美金, joe 有2个美金. write 列中的 6:data @ 5 表示 当前的数据是 version 为 5(一般是时间戳re)

key     bal:data            bal:lock             bal:write

Bob     6:                  6:                   6: data @ 5

        5: $10              5:                   5:



Joe     6:                  6:                   6: data @ 5

        5: $2               5:                   5:

2 事务的第一个阶段, bob的账户变成3美金了. 注意 lock 列被加锁, 并且标明自己是 primary. 每个事务中, 只有一个primary, 也正是这个primary的存在, 使得我们能够用行原子性来实现分布式事务.

Bob     7:$3                7: I am primary      7:

        6:                  6:                   6: data @ 5

        5: $10              5:                   5:



Joe     6:                  6:                   6: data @ 5

        5: $2               5:                   5:

3 现在给joe加上7美金, 所以joe是9美元了, 注意 joe 这一行的 lock 是指向 primary 的一个指针.

Bob     7: $3               7: I am primary      7:

        6:                  6:                   6: data @ 5

        5: $10              5:                   5:



Joe     7: $9               7: primary@Bob.bal   7:

        6:                  6:                   6: data @ 5

        5: $2               5:                   5:

4 事务提交的第一阶段, 提交 primary, 移除lock 列的内容 在 write 列写入最新数据的 version

Bob     8:                  8:                   8: data@7

        7: $3               7:                   7:

        6:                  6:                   6: data @ 5

        5: $10              5:                   5:



Joe     7: $9               7: primary @ Bob.bal 7:

        6:                  6:                   6:data @ 5

        5: $2               5:                   5:

5 事务提交的第二阶段, 提交除 primary 之外其它部分. 提交的方式也是移除 lock, 同时在 write 列写入新数据的 version

Bob     8:                  8:                   8: data @ 7

        7: $3               7:                   7:

        6:                  6:                   6: data @ 5

        5: $10              5:                   5:



Joe     8:                  8:                   8: data@7

        7: $9               7:                   7:

        6:                  6:                   6: data @ 5

        5:$2                5:                   5:

代码分析

class Transaction{
    struct Write { Row row; Column col; string value; };
    vector<Write> writes_;  

    // 每一个事务都有一个start timestamp
    // 读事务只关心[0, start_ts_]时间区间之内数据逻辑是否一致
    // 写事务则需要关心[0, infinate)时间区间之内数据逻辑是否一致
    int start_ts_;  

    // 初始化当前事务的timestamp,note: 此oracle非彼Oracle
    Transaction() : start_ts_(oracle.GetTimestamp())
    {
    }  

    void Set(Write w)
    {
        writes_.push_back(w);
    }  

    // 读事务
    void Get(Row row, Column c, string *value)
    {
        while (true)
        {
            // 利用了Google Bigtable的单行事务特性。
            // 单行事务的特征为:
            // 在单行数据的操作上保证事务性(ACID)
            bigtable::Txn T = bigtable::StartRowTransaction(row);
            // 检查在读操作的同时是否有并发的写操作,如果有并发写操作
            // (包括那些没有彻底完成写操作就挂掉的情况)则需要执行比较
            // 复杂的重试/清理操作 - BackoffAndMaybeCleanupLock()
            //
            // 这里需要注意时间区间为[0, start_ts],也就是说Get只关心
            // 在本事务发起前的数据快照是否具有一致性,对start_ts_之后
            // 发起的事务它并不关心。这反映了Percolator表现出来的
            // snapshot isolation特性,Get操作的是start_ts_之前的一个快照
            if (T.Read(row, c+"lock", [0, start_ts]))
            {
                 // 执行到这里的时候说明有尚未解开的锁
                  // (pending lock),可能来自:
                //  1. 在start_ts_之前发起的一个写事务正在进行中
                //  2. 在start_ts_之前发起的一个写事务没有完全commit就死掉了
                // ps. Back off的意思是后退,滚开,
                 // 貌似一群警察踢门的时候常喊?
                BackoffAndMaybeCleanupLock(row, c);
                continue;
            }  

            // 执行到这里的时候说明start_ts_之前的数据具有一个一致的snapshot
            last_write = T.Read(row, c+"write", [0, start_ts_]);
            // sanity check. 没有找到任何数据可读,返回。
            if (!latest_write.found())
            {
                return false;
            }
            // write列记录了data所在的timestamp,
              // 为了读到一条数据,需要先得到该数据所在的
             // timestamp,然后通过timestamp读到最终数据,
             // 有点间接寻址的味道
            int data_ts = latest_write.start_timestamp();
            *value = T.Read(row, c+"data", [data_ts, data_ts]);  

            return true;
        }
    }  

    bool Prewrite(Write w, Write primary)
    {
        Column c = w.col;
        bigtable::Txn T = bigtable::StartRowTransaction(w.row);  

        // 如果在本事务开始后([start_ts_, inf))也有其他事务执行写操作,
        // 并且已经完成了部分/全部数据写操作,则abort
        //
        // 对于start_ts_之前的写操作,分为两种情况
        //  1. 整个事务都提交完成了得写操作,这是正常情况,结果一致
        //  2. 只写了一半的事务,这由后面的lock检查来处理
        if (T.Read(w.row, c+"write", [start_ts_, inf])
        {
            return false;
        }  

        // 如果在当前操作的cell上还有锁的话,则abort
        // 这个检查比较狠,只要有锁,无论timestamp为多少均abort,这是
        // 因为只要有锁,就说明还有一个并发事务(dead or not)在写当前cell
        
        if (T.Read(w.row, c+"lock", [0, inf])) // 只要有锁, 全部则返回错误
        {
            return false;
        }  

        // 检查到这里就可以放心地预写入数据和锁了
        // 此时data对Get还不可见,因为write还没有写入
        // 时间相当于版本号,这个依赖于google的提供GPS和原子钟提供的严格递增的时间. 其他公司怎么做...找一个统一授时集群?? 
        T.Write(w.row, c+"data", start_ts_, w.value);                     //start_ts_@w.value 
        T.Write(w.row, c+"lock", start_ts_, {primary.row, primary.col}); // start_ts_@{primary.row, primary.col}

        // 提交bigtable单行事务
        return T.commit();
    }  

    bool Commit()
    {
        // 任选一个write作为primary,这里primary的作用类似于一个标志点primary行被提交后,整个事务必须提交
        Write primary = writes_[0];
        vector<Write> secondaries(writes_.begin() + 1, writes_end());  

        // 预提交
        // primary和secondarise的预提交如果失败,
        // 则说明还有别的并发事务在写当前cell,当前commit需要abort
        //
        // 我对并发事务的理解:在时间轴上有交集的事务。
        //  Timeline -------------------------------------------->
        //  Trans0:   ^-$
        //  Trans1:       ^-----$
        //  Trans2:       ^----------$
        //  Trans3:          ^------------$
        //  Trans4:                           ^----$
        //  Trans5:  ^----x
        // ^标志事务开始,$标志事务结束,x表示执行事务的进程中途死掉
        //  0,5; 1,2; 1,3; 1,5; 2,3; 2,5均为并发事务
        if (!Prewrite(primary, primary))
        {
            return false;
        }
        
        for (Write w : secondaries)
        {
            // 如果在这里挂了, 谁来清理
            if (!Prewrite(w, primary))
            {
                return false;
            }
        }  

        int commit_ts = oracle_.GetTimestamp();  

        Write p = primary;
        
 
        bigtable::Txn T = bigtable::StartRowTransaction(p.row);
        // 读取Prewrite阶段写入的lock,如果读取失败,则abort
        // 执行这一步的原因在于lock可能由于某种原因被Get操作清理掉了
        // 某种原因包括:
        //  1. 真死了
        //  2. 假死,等下可能活过来的
        //    1) 执行当前事务的线程被调度器调度出去了,执行优先级较低
        //    2) 系统中出现了一些工作特别繁重的线程,把系统暂时性压死
        //    3) 等待IO。等等
        // 另外,这里只读取primary lock,而没有读取其它lock,是Percolator
        // 的一个约定,它相对简化了检查过程,不需要检查secondaries的lock。
        if (!T.Read(p.row, p.col+"lock", [start_ts_, start_ts_])) //  注意整个时间就是版本
        {
            return false;
        }
        T.Write(p.row, p.col+"write", commit_ts, start_ts_); // commit_ts@start_ts_
        // 成功执行下面的Commit操作后,写操作对Get可见
        T.Erase(p.row, p.col+"lock", commit_ts);  

        // *NOTE* 提交点,T.Commit执行成功后一旦系统出现故障,恢复后
        // 只能rollforward,不能rollback
        if(!T.Commit())
        {
            return false;
        }  

        // 此时的写操作已经不需要用行事务来保证了,因为这里只有写操作
        // 并且也不可能有两个并发写操作都写同一个commit_ts下的cell
        for (Write w : secondaries)
        {
            bigtable.Write(w.row, w.col+"write", commit_ts, start_ts_);
            bigtable.Erase(w.row, w.col+"lock", commit_ts);
        }  

        return true;
    }  

    // 确认造成冲突的进程是否已经退出,如果退出则做清理,否则忽略
    void BackoffAndMaybeCleanupLock(Row row, Column c)
    {
         //------------分布式锁的常用用法-------------
        // 判断写这个锁的worker是否还活着(liveness)的方法:
        // 每个worker会写一个token到Chunbby lockservice中,并且定期
        // 更新这个token中的last_update_time,其它worker检查这个worker
        // 是否存活的方法就是去检查这个token是否存在,如果存在,其
        // last_update_time是否太旧,通过这两重检查才判定该worker活着,

        // -------------如果判定该worker已死,则根据primary lock的状态来决定动作---------
        //  1. primary lock不存在: roll-forward, 将所有未提交的secondary write都提交掉,相应的lock都擦除掉
        //  2. primary lock存在  : roll-back, 将primary的数据清除掉,write的值也擦除掉。
        
        
        // --------lock应该是被清除的, 只要读到任意版本的lock 都应该调用该函数----------- 

        // 如果worker还活着,则不进行数据操作,可能小睡眠一下,等待
        // worker主动将锁清除。
        // Get操作会因此等待较长一段时间,这是Percolator需要注意的一个特点。
    }  

} // class Transaction

锁冲突的处理

当一个client在事务提交阶段,crash掉了,那么锁还保留,这样后续的client访问就会被阻止,这种情况叫做锁冲突,Percolator提供了一种简单的机制来解决这个问题。

每个client定期向Chubby Server写入token,表明自己还活着,当某个client发现锁冲突,那么会检查持有锁的client是否还活着,如果client是working状态,那么client等待锁释放。否则client要清除掉当前锁。

Roll forward or roll back :

Client先检查primary lock是否存在,因为事务提交先从primary开始,如果primary不存在,那么说明前面的client已经提交了数据,所以client执行roll forward操作:把non-primary对应的数据提交,并且清除non-primary lock;如果primary存在,说明前面的client还没有提交数据就crash了,此时client执行roll back操作:把primary和non-primary的数据清除掉,并且清除lock。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 关键字:分布式事务、数据库 背景: 首先先说下Percolator出现的背景:众所周知Google是一家以技术著称...
    奔跑的番茄酱阅读 3,736评论 2 9
  • MySQL技术内幕:InnoDB存储引擎(第2版) 姜承尧 第1章 MySQL体系结构和存储引擎 >> 在上述例子...
    沉默剑士阅读 7,398评论 0 16
  • 当一个系统访问量上来的时候,不只是数据库性能瓶颈问题了,数据库数据安全也会浮现,这时候合理使用数据库锁机制就显得异...
    初来的雨天阅读 3,560评论 0 22
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 什么是事务 事务是一条或多条数据库操作语句的组合,具备ACID,4个特点。 原子性:要不全部成功,要不全部撤销 隔...
    jiangmo阅读 1,080评论 0 3