Nov 6
改 vimrc 没什么卵用,搜了一下,说是因为终端的兼容问题,只要在 ~/.bashrc 里面加上  "export TERM=linux" 就好。

refer: https://stackoverflow.com/questions/31783160/why-vim-is-changing-first-letter-to-g-after-opening-a-file
Sep 19
# 1. 什么是跳表

跳表(Skip List)是基于链表 + 随机化实现的一个有序数据结构,可以达到平均 O(logN) 的查找、插入、删除效率,在实际运行中的效率往往超过 AVL 等平衡二叉树,而且其实现相对更简单、内存消耗更低。

Redis 的 ZSET 底层实现就是用的 Skip List,这里是 [Antirez对此的说明](https://news.ycombinator.com/item?id=1171423)

这是一个典型的跳表:

[0] -> 0 -> 1 -> 3 -> 4 -> 5 -> 6 -> 7 -> 9 -> nil
[1] -> 0 ------> 3 ------> 5 ------> 7 ------> nil
[2]----------------------> 5-----------------> nil


解释一下:

1. SkipList 是一个多层的链表

2. 第[0]层的链表包含所有节点,其他层的链表包含部分节点,层次越高,节点越少

3. 每层链表之间会共享相同的节点(节省内存,但为了方便展示,每一层都输出了它的值)

4. 对于某个节点,在插入时通过概率判断它最高会出现在哪一层,并且也会出现在之下的每一层

通过这样的设计,当需要查找某个 key 时,可以从最高层的链表开始往前找,在这一层遇到末尾或者大于 key 的节点时往下走一个层,直到找到 key 节点。

例如:

引用
4 的查找路径为 [2] -> [1] -> 0 -> 3 -> 3@[0] -> 4
6 的查找路径为 [2] -> 5 -> 5@[1] -> 5@[0] -> 6
8 的查找路径为 [2] -> 5 -> 5@[1] -> 7 -> 7@[0] -> 9 (找不到)


# 2. 跳表的节点

从上面的描述,我们大概可以知道 (1) 每个节点需要保存一个 key; (2) 每个节点需要有多个next指针 (3) 其 next 指针的数量会在插入时确定

因此我们可以用下面这个 class 来表示节点:

class Node(object)
    def __init__(self, height, key):
        self.key = key
        self.next = [None] * height

    def height(self):
        return len(self.next)


# 3. 创建跳表

一个新创建的跳表是没有节点的。但为了实现的简单起见,可以添加一个头节点:

class SkipList(object):
    def __init__(self):
        self.head = Node(0, None) #头节点高度为0,不需要key


到目前为止都特别简单,但是还什么也干不了。

# 4. 创建节点

创建节点时,需要先按一定的概率分布确定其高度。

为了保证高层的节点比低层少,我们可以用这样的概率分布:

引用
Height(n) = p^n


实现其实非常简单:

import random

def randomHeight(self, p = 0.5):
    height = 1
    while random.uniform(0, 1) < p and self.head.height() >= height:
        height += 1
    return height


这样可以保证平均的路径长度是 log(n) 。

精确一点的话,实际上是 log(n-1, 1/p) / p,也就是说, p 的选择会影响跳表层数、平均路径长度。

具体的计算比较复杂,有兴趣可以参考跳表的原论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。(TL;DR)

然后我们就可以这样来创建一个新的节点:

    node = Node(self.randomHeight(), key)

# 5. 添加节点

如果只是为空跳表添加一个新的节点,只要更新头结点的每一个next指针:

def insertFirstNode(self, key):
    node = Node(self.randomHeight(), key)
    while node.height > self.head.height():
        self.head.next.append(None) #保证头节点的next数组覆盖所有层次的链表

    for level in range(node.height()):
        node.next[level] = self.head.next[level]
        self.head.next[level] = node


但很显然这个方法只能用一次。

如果跳表中已经有多个节点,那我们就必须找到每一层中适合插入的位置:

def getUpdateList(self, key):
    update = [None] * self.head.height()
    for level in range(len(update)):
        x = self.head
        while x.next[level] is not None and x.next[level].key < key:
            x = x.next[level]
        update[level] = x
    return update


这个函数返回一个 update 节点数组,其中的每个节点都是在这一层中小于 key 的最后一个节点。

也就是说,在 level = i 层,总是可以把新的节点插入 update[i] 之后:

def insert(self, key):
    node = Node(self.randomHeight(), key)
    while node.height > self.head.height():
        self.head.next.append(None) #保证头节点的next数组覆盖所有层次的链表

    update = self.getUpdateList(key)
    next0 = update[0].next[0]
    if next0 is not None and next0.key == key:
        return # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。

    for level in range(node.height()):
        node.next[level] = update[level].next[level]
        update[level].next[level] = node


但是由于这一版 getUpdateList 是 O(n) 的,插入效率并没有达到跳表的设计目标。

# 6. 添加节点++

考虑这一点:跳表的每一层都是有序的。

也就是说,我们在找到 update[n] = x 以后,其实可以从节点 x 的 n - 1 层继续查找 update[n-1] 应该是哪个节点。

由于查找路径的平均长度是 log(N) ,所以我们可以实现一个更快的 getUpdateList 方法

注意,需要从最高层开始查

def getUpdateList(self, key):
    update = [None] * self.head.height()
    x = self.head
    for level in reversed(range(len(update))):
        while x.next[level] is not None and x.next[level].key < key:
            x = x.next[level]
        update[level] = x
    return update


# 7. 里程碑1

把上面的代码整合起来,我们就可以得到第一版跳表代码:能够插入节点。

为了更好地展示我们的成果,我们可以用这样一个函数,把链表按第1节的例子样式输出:

def dump(self):
    for i in range(self.head.height()):
        sys.stdout.write('[H]')
        x = self.head.next[0]
        y = self.head.next[i]
        while x is not None:
            s = ' -> %s' % x.key
            if x is y:
                y = y.next[i]
            else:
                s = '-' * len(s)
            x = x.next[0]
            sys.stdout.write(s)
        print ' -> <nil>'
    print


试试看:

sl = SkipList()
for i in range(10):
    sl.insert(sl)
    s1.dump()



[H] -> 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> <nil>
[H]----- -> 1 -> 2 -> 3---------- -> 6 -> 7---------- -> <nil>
[H]---------- -> 2-------------------- -> 7---------- -> <nil>


多尝试几次,以及选择不同的 p 值,可以观察生成跳表的区别。

# 8. 查找节点

实际上查找节点的过程,已经包含在 insert 的实现里了:

def find(self, key):
    update = self.getUpdateList(key)
    if len(update) == 0:
        return None

    next0 = update[0].next[0]
    if next0 is not None and next0.key == key:
        return next0 # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。
    else:
        return None


# 9. 删除节点

既然已经能找出 update 节点数组,在 level = i 层,只要判断 update[i].next[i] 是否等于要删除的 key 就可以了:

def remove(self, key):
    update = self.getUpdateList(key)
    for i, node in enumerate(update):
        if node.next[i] is not None and node.next[i].key == key:
            node.next[i] = node.next[i].next[i]


# 10. 里程碑2

整合 find 和 update 数组,就可以实现跳表的基础操作了,试试看:

node = sl.find(3)
print node

for i in range(7, 14):
    sl.remove(i)
    sl.dump()


# 11. 其他

我们在 Node 中只添加了一个 key 属性,在具体的实现中,我们往往可能需要针对 key 存储一个 value,例如 Python 自带的 dict 实现。改造起来也很简单:

1. node 中添加一个 value 属性,并且添加相应的初始化逻辑(__init__方法)

2. 将 SkipList.insert 修改为 `insert(self, key, value)`,在新建 Node 时指定其 value

3. 再添加一个 `update(self, key, value)` API,方便调用方的使用

4. 可以考虑针对语言适配,例如实现 python 的 __getitem__ 、 __setitem__ 等魔术方法


# 12. 完整代码

#coding:utf-8

import random

class Node(object):
    def __init__(self, height, key=None):
        self.key = key
        self.next = [None] * height

    def height(self):
        return len(self.next)

class SkipList(object):
    def __init__(self):
        self.head = Node(0, None) #头节点高度为0,不需要key

    def randomHeight(self, p = 0.5):
        height = 1
        while random.uniform(0, 1) < p and self.head.height() >= height:
            height += 1
        return height

    def insert(self, key):
        node = Node(self.randomHeight(), key)
        print node.height(), node.key
        while node.height() > self.head.height():
            self.head.next.append(None) #保证头节点的next数组覆盖所有层次的链表

        update = self.getUpdateList(key)
        if update[0].next[0] is not None and update[0].next[0].key == key:
            return # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。

        for level in range(node.height()):
            node.next[level] = update[level].next[level]
            update[level].next[level] = node


    def getUpdateList(self, key):
        update = [None] * self.head.height()
        x = self.head
        for level in reversed(range(len(update))):
            while x.next[level] is not None and x.next[level].key < key:
                x = x.next[level]
            update[level] = x
        return update

    def dump(self):
        for i in range(self.head.height()):
            sys.stdout.write('[H]')
            x = self.head.next[0]
            y = self.head.next[i]
            while x is not None:
                s = ' -> %s' % x.key
                if x is y:
                    y = y.next[i]
                else:
                    s = '-' * len(s)
                x = x.next[0]
                sys.stdout.write(s)
            print ' -> <nil>'
        print

    def find(self, key):
        update = self.getUpdateList(key)
        if len(update) == 0:
            return None

        next0 = update[0].next[0]
        if next0 is not None and next0.key == key:
            return next0 # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。
        else:
            return None

    def remove(self, key):
        update = self.getUpdateList(key)
        for i, node in enumerate(update):
            if node.next[i] is not None and node.next[i].key == key:
                node.next[i] = node.next[i].next[i]


完。
Sep 6
excel很强大,但也有非常蠢的地方:比如今天遇到的,导出文档的日期列是“文本”格式,这时候用数据透视表,excel不能识别这是日期,于是无法根据月或者年对数据进行聚合。

即使选中整列,然后将格式全都修改为日期也不行。

即使再弄一列格式为日期的,然后用黏贴数值也不行。

按照过去的经验,只有逐个格子双击,然后回车,才能把格式应用到数据上,真是蠢到爆炸。

今天觉得实在不能忍了,放狗搜了下“excel apply format instead of double click on each column”,总算找到一个解决方案:

1. 选中该列

2. 在“数据”Tab里点击“分列”(按格式将单列文本拆分成多列,英文版是 Text To Columns)

3. 点击完成

搞定
Aug 3

方法论 不指定

felix021 @ 2018-8-3 03:49 [杂碎] 评论(2) , 引用(0) , 阅读(519) | Via 本站原创
1. 意志力

    有些人似乎天生就有很强的意志力,可以长时间连续做某件事情而不知疲倦,例如学霸可以长时间做题,计算机大神可以长时间编写代码(看看那些 Hackathon 比赛)。但我认为这里有一些事实被掩盖了——你回顾一下,是不是也有一些自己可以长时间做而不会疲倦的事情,例如王者荣耀、吃鸡,又或者刷各种美剧?你也许会觉得,这不一样啊,玩游戏和刷剧是这种有意思的事情,并不需要意志力的支撑。问题就在这里——可能很多人无法理解的是,学霸们和计算机大神们可以从做题、写码中获得快乐,所以才会觉得是强大的意志力在支撑着他们。

    在能得到快乐的事情上,持续的投入不需要强大意志力的支撑。反过来说,在不是特别感兴趣甚至令人反感的事情上,意志力就非常重要。不巧的是,意志力是一种有限的资源,合理、有效地利用这个资源,我认为是取得成绩的一个关键因素。

    那该怎么办呢?鸡汤地说,就是想办法让自己喜欢上要做的事情,如果做不到,至少也应该减少排斥感。当然,给汤不给勺是耍流氓,所以请继续往下看。

2. 反馈(奖励)

    很多人都对嗑瓜子这种带有魔性的娱乐活动深有体会,拿着一袋瓜子,不嗑完根本停不下来。如果不巧你不喜欢嗑瓜子,可以想想那些让你觉得非常爽的游戏,无论具体的游戏形式如何呈现,都会有一个共性,就是总会给你立即的回馈。游戏总会给完成挑战的玩家一些奖励,激励他们继续玩下去。好玩的游戏尤其在意回馈的呈现形式,比如施放一个技能产生的光影声音特效、对手被攻击掉血的数值化呈现,杀死一个对手后增加经验值、掉下来的装备,完成一个剧情任务以后的礼包奖励等等,都希望够给玩家创造充分的满足感,引导玩家继续完成下一个目标。

    人类本质上还是一种动物,大脑中对反馈的本能反应,相当于给游戏的设计者开了一个后门,用于操纵玩家的行为。利用好这个特点,即使在本来并不十分感兴趣的领域,也能够显著改善对意志力的消耗。举例而言,我总是会边写边运行我的代码,一方面可以确认写好的代码没有错误(避免把一大堆问题积攒到最后),另一方面运行结果本身是一个有效的反馈(每次运行结果都更完整、更接近目标),激励我继续改进;而且在学习数据结构时(例如一个二叉树),我总会写一个 dump 方法,用来把内存中抽象的数据可视化。

3. 目标

    游戏设计的“心流(flow)”理论给出的几个关键因素中,我认为反馈是最重要的,其次是目标。好的游戏总会设定合理的目标,引导玩家一步一步入坑。太简单的目标会让人觉得无聊,太难的目标则很容易让人因畏难而放弃。就像下棋/打球,和新手、或水平远高于自己的选手对战,都不是一件愉快的事情,而让人既觉得愉快又能提高自己水平的,正是与水平相当(或者略高)的人对战,获胜的可能性(不确定性)和对获胜的追求,会带来双方激烈的对抗,而这些对抗能给参与者淋漓酣畅的快意(反馈),从而形成有效的正反馈循环。

    同样地,在学习过程中,给自己设定合理的目标能够有效降低对意志力的消耗。如何设置合理的目标,可能没有通用的方法,但幸运的是,可以在实践中调整,找到最适合自己的阈值。

4. 启动成本

    俗话说万事开头难,如果能想办法减少开头的困难,那万事就不那么难了。所谓的困难,换种说法就是成本高。这里的成本是广义的,不仅仅包含金钱成本,也包含时间成本,以及其他为了达成目标所要付出的代价。而这里的目标还只是开始做某件事情,如果启动成本过高,在开始之前就会消耗大量意志力,那么结果便可想而知。

    一个典型的例子是健身。我曾经的一份工作,公司隔壁是一个专业大型健身房,提供浴巾、拖鞋等各种洗浴用品,公司发放免费的年卡,于是我每周都会有2~3次午休或下班时间“说走就走”地去健身,甚至还请了教练来辅导我,过程虽然有些辛苦,但回想起来确实很享受那段时光。换工作以后,我也曾经办过一次季度健身卡,但是启动成本陡增:我需要自掏腰包来办理健身卡,而且这家健身房离我有十几分钟脚程、不提供浴巾和拖鞋等,每次去健身房之前心里都一番挣扎,结果是,虽然我坚持了一周2次的频率,但季度卡到期后就没再续费。我做了另一个决定:在家里腾出一小块地方,买了一架椭圆机,只要有15分钟空隙就可以完成一次轻松的健身。如果我愿意,还可以开启刷剧模式,很愉快地在椭圆机上磨蹭大半个小时。

    降低启动成本,其本质就是想办法制定一个较容易达成的初始目标(启动)。硅谷创业家 Eric Rise 所著《精益创业》(Lean Startup)的核心思想“最小化可行产品”(Minimum Viable Product, MVP),我认为也是最小化启动成本的一个应用,通过最小代价开发出一个可用的产品(达成目标:启动),推向市场,验证其可行性(反馈),根据市场反馈持续不断地迭代改进(阶段性目标),形成正反馈循环。

通过降低启动成本,设置适合自己的目标,建立能够带来奖赏的反馈机制,形成正反馈的循环,从而最大化地利用好意志力资源,这就是我的方法论。

如果不够的话,还有一个开挂的技巧——研究表明,吃糖可以在一定程度上补充意志力。也对,要是在健身减肥的过程中可以吃甜品,应该就没那么难坚持下来了吧?
Jun 8
注:这篇是3月初在公司内部平台上发布的,搬一份到 blog 存档。
===


我注意到过去几个月有些同学还在踩一个简单的分布式事务Case的坑,而这个坑我们在两年以前就已经有同学踩过了,这里简单解析一下这个case和合适的处理方案,供各位参考。

# 1. 踩过的坑

这个case有很多变种,先说说我们在 X 业务踩过的坑开始,大约是16年9月,核心业务需求是很简单的:在用户发起支付请求的时候,从用户的银行卡扣一笔钱。负责这个需求的同学是这么写的代码(去除其他业务逻辑的简化版):
$dbTrans = $db->beginTransaction();
try {
    $order = PayRequest::model()->newPayRequest(...); #在数据库中插入一条支付请求记录,状态为待支付
    //其他业务改动
    $result = PaySvr::pay($order->id, $order->amount); #请求PaySvr(或第三方支付通道)扣款
    if ($result['code'] == PaySvr::E_SUCCESS) {
        $order->setAsSucceeded();
    } else {
        $order->setAsPending();
    }
    $dbTrans->commit();
} catch (Exception $e) {
    $dbTrans->rollback();
}

乍一看好像是没有什么毛病,测试的case都顺利通过,也没有人去仔细review这一小段代码,于是就这么上线了。但问题很快就暴露出来,PaySvr在支付成功以后尝试回调,业务系统报错”订单不存在”。查询线上日志发现,这笔订单在请求第三方支付通道时网络超时,Curl抛了timeout异常,导致支付记录被回滚。有心的同学可以自己复现一下这个问题,观察BUG的发生过程。

代码修复起来倒是很简单,在请求PaySvr之前提交事务,将支付请求安全落库即可。
$dbTrans = $db->beginTransaction();
try {
    $order = PayRequest::model()->newPayRequest(...);
    $order->setAsPending();
    //其他业务改动
    $dbTrans->commit(); #先将支付请求落地
} catch (Exception $e) {
    $dbTrans->rollback();
}

#再请求PaySvr
$result = PaySvr::pay($order->id, $order->amount);

#根据PaySvr结果修改支付请求和其他业务记录的状态
$dbTrans = $db->beginTransaction();
try {
    if ($result['code'] == PaySvr::E_SUCCESS) {
        $order->setAsSucceeded();
        //其他业务改动
    } elseif ($result['code'] == PaySvr::E_FAIL) {
        $order->setAsFailed();
        //其他业务改动
    } else {
        //等待后续cron补单
    }
    $dbTrans->commit();
} catch (Exception $e) {
    $dbTrans->rollback();
}

把这个实现代入多个不同的业务下,还会衍生出更多问题,比如被动代扣业务,就可能因为重试导致用户被多次扣款,引起投诉(支付通道对投诉率的要求非常严格,甚至可能导致通道被关停);更严重的是放款业务,可能出现重复放款,给公司造成直接损失。据说某友商就是因为重复放款倒闭的,所以请各位同学在实现类似业务时特别注意,考虑周全。

# 2. 归纳总结

我们往后退一步再审视这个case,这段简单的代码涉及了两个系统:X 业务系统(本地数据库)、PaySvr(外部系统)。可以看得出这段代码的本意,是期望将当前系统的业务和外部系统的业务,合并到一个事务里面,要么一起成功提交,要么一起失败回滚,从而保持两个系统的一致性。

之所以没能达到预期,直接原因是,在失败(异常)回滚的时候,只回滚了本地事务,而没有回滚远端系统的状态变化。按这个思路去考虑,似乎只要加一个 PaySvr::rollbackRequest($order->id) 好像就可以解决问题。但仔细想想就会发现远没这么简单,从业务上来说,如果已经给用户付款了,那实际上就是要给用户退款,而往往这时候是掉单(支付请求结果未知),我们又无法直接给用户退款;更极端一点,如果这个rollback请求也超时了呢,那本地可以rollback吗?

这就是分布式事务棘手的地方了,只靠这样的逻辑是无法保证跨系统的一致性的。解决这个问题的方法是引入两段式提交(2 Phase Commit,常常简写为2PC),其基本逻辑是,在两个系统分别完成业务,然后再分别提交。

例如我们上面的解决方案,实际上就是2PC的一个实现:我们把业务需求作为一整个事务,它可以拆成两个子事务(第三方支付通道完成代扣,在业务系统记录支付请求成功并修改相应业务状态),每个子事务又分成两个阶段:第一阶段,是在本地先记录支付请求(状态为待确认),并向第三方支付发出代扣请求(结果不确定);第二阶段,在确认第三方代扣成功以后,修改本地支付请求的状态修改为成功,或者是代扣结果为失败,本地支付请求状态记为失败。两个阶段都完成,这个事务才是真的完成了。

# 3. Case变种

仔细思考我们曾经实现过的需求,可能会在很多看似不起眼的地方发现分布式事务,例如我们在存管匹配系统里面,就有这样一个Case。

由于与银行存管系统交互的延迟比较大,所以我们的匹配系统实现是异步的,匹配系统在撮合了资金和资产以后,会生成一条债权关系记录在本地,随后再发送到银行系统执行资金的划拨。为了提高执行的效率,我们希望在债权关系生成以后,尽快执行资金的划拨,因此我们会把资金划拨的指令通过LPush放进Redis的list里;List的另一端,那些使用BLPOP监听数据的worker会立刻被激活去执行。

如果没有仔细思考,代码可能会这么写:
#匹配系统
function matcher() {
    $dbTrans = $db->beginTransaction();
    try {
        foreach (matchCapitalAndProject() as $match_result) {
            list($capital_id, $project_id, $amount) = $match_result;
            $relation = Relation::model()->create($capital_id, $project_id, $amount);
            $redis->lPush($relation->id);
        }
        $dbTrans->commit();
    } catch (Exception $e) {
        $dbTrans->rollback();
    }
}

#Worker
function Worker() {
    while (true) {
        $id = $redis->brPop();
        $relation = Relation::model()->findByPk($id);
        if ($relation) {
            HengfengApi::invest($relation->capital_id, $relation->project_id, $amount);
        }
    }
}

在实际执行这段代码的时候,如果没有仔细测试(尤其是在有补单逻辑,捞出未执行成功的划拨指令再发送给银行),可能就不会发现,实际上有很多指令并不是马上被执行的,因为relation_id被送进list以后,worker马上就会读出来执行,但这时事务可能还没有提交。但这只是影响了业务的效率,还没有对业务的正确性产生影响。


为了修复这个问题,似乎可以这么做:把 [capital_id, project_id, amount] 发送到redis,worker直接取出执行,这样就不用从数据库读取relation,保证尽快将请求发送到银行。但如果因为某些原因,事务最终没有被提交呢?找银行rollback这些指令的执行,那就麻烦多了。

正确的做法是,在事务提交了以后,再lPush到Redis里:
#匹配系统
function matcher() {
    $arr_relation = [];
    $dbTrans = $db->beginTransaction();
    try {
        foreach (matchCapitalAndProject() as $match_result) {
            list($capital_id, $project_id, $amount) = $match_result;
            $relation = Relation::model()->create($capital_id, $project_id, $amount);
            $arr_relation[] = $relation;
        }
        $dbTrans->commit();
    } catch (Exception $e) {
        $dbTrans->rollback();
        $arr_relation = []; #清空,避免被push到队列里
    }
    foreach ($arr_relation as $relation) {
        $redis->lPush($relation->id);
    }
}
注:foreach要放到try-catch后面。


最后想补充一点,相信有很多同学知道这个Case,或者就算不知道也不会犯这样的错误,因此也许会觉得没必要专门揪出来这样分享 —— 但“知识的诅咒”就是这样,“我会的东西都是简单的”,然而对于没有踩过坑的同学来说,其实都是宝贵的经验;另一方面,有些别人觉得简单的问题、踩过的坑,也许自己是不知道的。所以,希望大家都能分享自己在工作学习中踩过的坑、解决过的问题,互相交流,互相提高。
Mar 29

浅析嵌套数据库事务 不指定

felix021 @ 2018-3-29 22:00 [IT » 数据库] 评论(0) , 引用(0) , 阅读(1014) | Via 本站原创

大家都知道,数据库事务提供的强一致性,让我们只需要在业务开始之前执行begin、结束后执行commit,并在异常的情况下执行rollback,就能够保证业务数据的强一致性。

## 1. 转一笔账

以一个转账操作为例,从from账户往to账户转一笔钱,涉及到两个账户的操作,我们用事务来保证数据的一致性:
function actionTransfer(Account $from, Account $to, int $amount)
{
    $this->db->begin();
    try {
        $from->withdraw($amount);
        $to->deposit($amount);
        $this->db->commit();
    } catch (Exception $e){
        $this->db->rollback();
    }   
}

class Account extends Model
{
    protected function changeBalance($amount)
    {
        $this->db->exec(
            "update account set balance = balance + :amount where id = :id",
            ['amount' => $amount, 'id' => $this->id]
        );
        AccountLog::create($this->id, $amount); #创建一条账户流水
    }
    public function withdraw($amount)
    {
        $this->changeBalance(-$amount);
    }
   
    public function deposit($amount)
    {
        $this->changeBalance($amount);
    }
}

如上面的栗子所见,我们往往会把数据库操作封装到底层模块(常常是model)里,对上层提供API,这样既保证了上层业务的可读性,也使得代码的可复用性提高了。

## 2. 充一笔钱,踩一个坑

说到复用性,那就是说,可能会有另一个业务需要调用它,比如充值,这个业务很简单,只涉及到一个账户,似乎可以简单点:
function actionDeposit(Account $account, int $amount)
{
    $account->deposit($amount);
}

这样看起来好像也没啥问题,对吧?但细心的小伙伴可能注意到了,代码里的 `changeBalance` 函数挖了个坑:在修改完账户余额以后会创建一条流水记录。如果没有开启事务的话,极端情况下会出现账户余额改好了,却没记录流水的情况,这就尴尬了。

当然这里只要在 `actionDeposit` 里开启了事务就能解决这个问题,但是基于防御性编程的习惯,`changeBalance` 这个方法本身作为一个独立的业务,应当尽最大努力确保其内业务的一致性,更直白一点说,就是也开启一个事务:
    protected function changeBalance($amount)
    {
        $this->db->begin();
        try {
            $this->db->exec("update account set balance ...", ...);
            AccountLog::create($this->id, $amount);
            $this->db->commit();
        } catch (Exception $e) {
            $this->db->rollback()
            throw $e; //确保上层业务知晓发生了异常
        }
    }

我们期望每一段代码都像这样对自己的业务负责,那么程序员不管在哪一层写代码,都能写得放心。

## 3. 嵌套事务

但到目前为止还没有到到期望的这么美好:如果只是在同一个流程里连续使用  begin ,实际上只会启动一个事务,遇到第一个 commit 就会结束事务,后续如果出现异常,rollback 无法达到预期的效果:
引用

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance + 1 where id = 1;
Query OK, 1 row affected (0.01 sec)

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> update account set balance = balance - 1 where id = 2;
Query OK, 1 row affected (0.01 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+---------+
| id | balance |
+----+---------+
|  1 |    1001 |
|  2 |    999 |
+----+---------+

还好的是,MySQL提供了嵌套事务的支持,可以使用其 SavePoint 特性来达到这个效果:
引用

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance + 1 where id = 1;
Query OK, 1 row affected (0.00 sec)

mysql> savepoint level1;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance - 1 where id = 2;
Query OK, 1 row affected (0.00 sec)

mysql> release savepoint level1;
Query OK, 0 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
|  2 |    1000 |
+----+---------+

利用这一点,我们就可以写出一个支持嵌套事务的数据库模块:
class Database
{
    protected $level = 0;
    public function begin()
    {
        if ($this->level == 0) {
            $this->exec("begin");
        } else {
            $this->exec("savepoint level{$this->level}");
        }
        $this->level += 1;
    }

    public function commit()
    {
        $this->level -= 1;
        if ($this->level == 0) {
            $this->exec("commit");
        } else {
            $this->exec("release savepoint level{$this->level}");
        }
    }

    public function rollback()
    {
        $this->level -= 1;
        if ($this->level == 0) {
            $this->exec("rollback");
        } else {
            $this->exec("rollback to savepoint level{$this->level}");
        }
    }
}

当然,对于不支持 savepoint 特性的DBMS,也不是不能解决这个问题,只要在 `level != 0` 的时候不执行 commit 或者 rollback 就行,只是无法达到 savepoint 的效果。

加上这个改动的具体实现这里就不贴了,感兴趣的小伙伴建议自己动手写写看,实在太懒也可以查看 Yii2.0 中`yii\db\Transaction` 的实现 —— 其实上面这段代码就是对 Yii2.0 实现的一个简化版本。喜欢 Laravel 的小伙伴,在 `Illuminate\Database\Concerns\ManagesTransactions` 也能找到类似的应用。

## 4. 找坑和填坑

我们实现的这个嵌套事务方案,使得每一部分代码都能够健壮地实现其业务。但是,在具体编码过程中会常常还会遇到两个大坑:坑一是 begin 和 commit/rollback 不配对,坑二是出了错忘记反馈给上层。在上面的实现里,要避开这两个坑,完全依赖程序员的编码习惯。但墨菲定律告诉我们……说多了都是泪,总之就是“依赖人来保证正确性”这件事情永远是不靠谱的。

那怎么改变这一点呢?Laravel 的解决方案是由代码本身来填坑,所以实现了 `ManagesTransactions->transaction` 这个方法(以下略作简化,省略了失败重试机制及相应的死锁处理,这一部分建议详读 laravel 的相关代码):
    public function transaction(Closure $callback)
    {
        $this->begin();
        try {
            $value = $callback();
            $this->commit();
            return $value;
        } catch (Exception $e) {
            $this->rollback();
            throw $e;
        }
    }

于是程序员们就可以这么干了:
public function actionTransfer($from, $to, $amount)
{
    DB::transaction(function () use ($from, $to, $amount) {
        $from->withdraw($amount);
        $to->deposit($amount);   
    })
}

当然,这个方案也不是完全没代价的,其中最重要的是,程序员们要严格遵守一个基本约定:**遇到错误的时候抛出异常**,以便封装层自动执行rollback。虽然也是约定,但是不像前面那两个约定那么晦涩,应该更容易遵守吧。

另外,如果在rollback之前还希望做一些清理操作(包括打log之类的),需要在callback里面再包一层 try-catch ;但如果遇到 commit 失败这种极端情况,清理操作仍无法被执行。这可以通过一个更复杂的约定(比如返回一个包含清理操作的 $clean_callback),但鉴于实际上不太有这种需要,得不偿失,所以大概就这样吧。

这种方案,在 C++ 里面有很多类似的例子,甚至还有一个专有名词 RAII (Resource Acquisition Is Initialization),“资源获取即初始化”,念起来有点拗口,其逻辑是在获取一个资源的时候立即初始化,并且在资源生命周期结束的时候自动销毁,避免因为程序员遗忘导致非预期的异常。常见的应用场景,包括打开文件(自动关闭)、多线程并发获取互斥锁(自动解锁)、申请内存资源(自动释放)等等。

如果你觉得看明白了,不妨试着写一个文件锁(flock)的代码练练手?
Mar 26
If you find this article helpful, you may like to donate to my ETH address:

  0x84D5084a0142a26081a2d06F3505cfc2CDaE9009


Detailed guide to compile viabtc_exchange_server on Ubuntu 16.04

## DEPENDENCIES ##
引用
$ sudo apt install -y libev-dev libjansson-dev libmpdec-dev libmysqlclient-dev libcurl4-gnutls-dev libldap2-dev libgss-dev librtmp-dev libsasl2-dev

# librdkafka: 0.11.3+; DO NOT INSTALL BY APT: version too old (0.8.x);
# if you do, remove them by: sudo apt remove librdkafka1 librdkafka-dev
$ wget https://github.com/edenhill/librdkafka/archive/v0.11.3.tar.gz -O librdkafka-0.11.3.tar.gz
$ tar zxf librdkafka-0.11.3.tar.gz
$ cd librdkafka-0.11.3
$ ./configure
$ make
$ sudo make install


## COMPILATION ##

引用
$ git clone https://github.com/viabtc/viabtc_exchange_server.git
$ cd viabtc_exchange_server

$ make -C depends/hiredis
$ make -C network

$ vi utils/makefile #modify INCS
# INCS = -I ../network -I ../depends
$ make -C utils

$ vi accesshttp/makefile #modify INCS & LIBS
# INCS = -I ../network -I ../utils -I ../depends
# LIBS = -L ../utils -lutils -L ../network -lnetwork -L ../depends/hiredis -Wl,-Bstatic -lev -ljansson -lmpdec -lrdkafka -lz -lssl -lcrypto -lhiredis -lcurl -Wl,-Bdynamic -lm -lpthread -ldl -lssl -lldap -llber -lgss -lgnutls -lidn -lnettle -lrtmp -lsasl2 -lmysqlclient
$ make -C accesshttp

$ vi accessws/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C accessws

vi alertcenter/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C alertcenter

$ vi marketprice/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C marketprice

$ vi matchengine/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C matchengine

$ vi readhistory/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C readhistory

Mar 14
之前接触以太坊的时候,确实能搜到很多资料,还有一个看起来很丰富的 Homestead Documentation,但这些材料都太不接地气了,看完还是不知道以太坊区块链到底长什么样,因此整理了这篇说明,希望能够在一定程度上解决这个问题吧。

# 1. 安装 ethereum (@ubuntu)

参考 官方说明
引用
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum solc


# 2. 生成创世文件

将以下内容保存于 ~/.ethereum/genesis.json
引用
{
    "config": {
        "chainID": 1048576,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0,
        "ByzantiumBlock": 0
    },
    "alloc": { "0xc1de867b55fdb749be0c927ecf7b19451777042b" : { "balance" : "20000000000000000000"} },
    "coinbase": "0x0000000000000000000000000000000000000000",
    "difficulty": "0x0400",
    "extraData": "0x00",
    "gasLimit": "0x2fefd8",
    "nonce": "0xdeadbeefdeadbeef",
    "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "timestamp": "0x00"
}

说明:
1) 来源于 https://g2ex.github.io/2017/09/12/ethereum-guidance/
2) 以太坊公链的chainID是1,私有链需要使用不同的chainID
3) 指定 ByzantiumBlock 这个参数,getTransactionReceipt() 才有 status ,用于判断交易执行结果
4) alloc 中指定了预分配 20个eth 的地址,其私钥是 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449

# 3. 初始化私有链

$ cd ~/.ethereum
$ geth --datadir ~/.ethereum/ init ./genesis.json

# 4. 账户基本操作

关于以太坊的私钥、公钥、地址、钱包的说明,参见第8节。

## 4.1 导入账户

$ echo 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449 > ~/.ethereum/coinbase.key
$ geth account import ~/.ethereum/coinbase.key

输入密码后,会在 ~/.ethereum/keystore/ 目录下生成一个账户文件,对应的地址就是创世文件里预分配了 20 eth 的那个 0xc1de867b55fdb749be0c927ecf7b19451777042b

## 4.2 生成新账户

执行以下命令,输入密码后会随机生成一个新的账户

$ geth account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {a90bbc1870e06a84b3dcf92a98f55453e6404a32}

## 4.3 列出账户

$ geth account list
Account #0: {c1de867b55fdb749be0c927ecf7b19451777042b} keystore:///data/www/.ethereum/keystore/UTC--2018-02-09T13-11-19.026113731Z--c1de867b55fdb749be0c927ecf7b19451777042b
Account #1: {7aa99ede746e863e87cdd1e686abadb31325cb92} keystore:///data/www/.ethereum/keystore/UTC--2018-02-18T09-00-05.982653587Z--7aa99ede746e863e87cdd1e686abadb31325cb92

# 5. 启动私有链

$ geth --identity "TestNode" --rpc --rpcport 8545 --port 30303 --nodiscover console

说明:
1) nodiscover 表示不会去连接其他节点(单节点私有链)
2) console 表示启动 geth 的 javascript 控制台

# 6. 控制台操作

在上一步启动的控制台里执行以下命令;或者通过 geth attach 连接到已经启动的 geth 实例执行。

## 6.1 查看账户

引用
> eth.accounts
["0xc1de867b55fdb749be0c927ecf7b19451777042b", "0x7aa99ede746e863e87cdd1e686abadb31325cb92"]
> eth.coinbase
"0xc1de867b55fdb749be0c927ecf7b19451777042b"

一般来说 coinbase 账户(挖矿的收益账户)是第一个导入的账户,也就是 eth.accounts[0]。

## 6.2 查看余额

引用
> eth.getBalance(eth.coinbase)
20000000000000000000


## 6.3 转账

引用
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var ether = Math.pow(10, 18)
> var gwei = Math.pow(10, 9)
> tx_hash = eth.sendTransaction({from:eth.coinbase, to: eth.accounts[1], value: 1 * ether, gasPrice: 2 * gwei, gas: 21000, data: null})
"0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"
> eth.getTransaction(tx_hash)
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gas: 21000,
  gasPrice: 2000000000,
  hash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
  input: "0x",
  nonce: 0,
  r: "0xe99aa75fdb2d071632f7d7796229e3d84df60407007032f29b69296677df798c",
  s: "0x616be42ff2a41a153f953685dc497d58ab4cca6ace4eac70405c611f078fcf4b",
  to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
  transactionIndex: 0,
  v: "0x200024",
  value: 1000000000000000000
}


说明:
1) 以太币的最小单位 wei = 10^-18 ether
2) sendTransaction 的几个参数说明:
    from: 交易发起方地址,必填,需要先解锁
    to: 交易接收方地址,转账交易必填
    value: 转账金额,单位为 wei
    gasPrice: gas的价格,选填(geth会用估算值自动填充)
    gas: 交易消耗的最大gas数量,选填(转账交易正好消耗21000,少于这个数字geth会拒绝创建交易); 多出的部分会在交易成功以后返还
    data: 交易的输入数据,选填(转账交易不需要)
3) getTransaction 根据交易的hash获取交易详情,包括sendTransaction给出的参数,签名数据,和包含该交易的区块信息。由于当前还没有被确认,所以blockHash和blockNumber都是无效数据;特别提一下,nonce 其实也是 sendTransaction 的一个参数,相当于由该账户发起的交易的订单号,以太坊要求nonce严格有序递增(即下一笔订单的nonce一定是上一笔的nonce+1)。

关于gas和gasPrice,参见第9节。

由于交易还未被确认,所以暂不能得知交易的执行结果。

## 6.4 挖矿

引用
> eth.blockNumber
0
> miner.start();
> admin.sleepBlocks(1);
> miner.stop();
> eth.blockNumber
1
> eth.getBlock(1)
{
  difficulty: 131072,
  extraData: "0xd783010802846765746887676f312e392e34856c696e7578",
  gasLimit: 4076476,
  gasUsed: 21000,
  hash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
  logsBloom: "0x000(省略部分)000",
  miner: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  mixHash: "0x16639b8a9e2adaddbb9a48f1963b16ebdb997a528c4a1aeb4ac06e04501b56e9",
  nonce: "0x1cf42797e7b2c1d8",
  number: 1,
  parentHash: "0xc75321a9915b2a2c973528f713a2a587e28244a032e3fcd46a5915112269c6a4",
  receiptsRoot: "0xd95b673818fa493deec414e01e610d97ee287c9421c8eff4102b1647c1a184e4",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 764,
  stateRoot: "0x0de36a5d34f176a081fde3f26646b9368f28736ad01e96000c7ac05565213d53",
  timestamp: 1521027698,
  totalDifficulty: 35081353,
  transactions: ["0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"],
  transactionsRoot: "0x4ae833bcf2114a237b86755453117da68876888d8c4215da8fb750016d5470f6",
  uncles: []
}


## 6.5 查看交易执行结果

引用
> eth.getTransaction(tx_hash)
{
  blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
  blockNumber: 267,
  ...
}
> eth.getTransactionReceipt(tx_hash)
{
  blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
  blockNumber: 267,
  contractAddress: null,
  cumulativeGasUsed: 42000,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gasUsed: 21000,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  status: "0x1",
  to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
  transactionHash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
  transactionIndex: 1
}
> eth.getBalance(eth.accounts[1])
1000000000000000000


说明:

1) 交易已经被确认,所以 getTransaction 可以看到区块信息
2) getTransactionReceipt 得到交易回执,其中:
    gasUsed: 实际消耗的gas(转账交易总是21000,除非指定了额外的data)
    status: "0x1" 表示成功, "0x0" 表示失败
    contractAddress: 新部署的合约地址;这个交易只是普通转账交易,不会部署合约,所以是null
    logs: 记录发送给合约的交易触发的事件,普通转账交易不会产生事件,所以是 [] 。

# 7. 合约

以太坊的账户分成两种,一种是EOA账户(Externally Owned Account,由私钥控制),里面存储了账户的以太币余额信息和由该账户发起的交易数量(nonce),上面创建的都是EOA账户。另一种是合约账户(Contract Account),是由合约部署交易创建的,里面除了存储以太币余额,还存储了合约代码,以及代码的状态(存储)。通过创建并发送由某个EOA账户私钥签名的交易,可以从该账户给其他EOA账户转账,或者向某个合约账户发起一笔交易(调用合约账户里的某个方法);而合约账户只能被动触发,接受某个EOA账户(或其他被触发的合约账户)发起的交易,根据该交易的指令执行相应的动作,修改内部状态,或向其他账户发出交易。

所谓合约,并不是某个要被执行的合同,其实质是一段代码,可以类比为一个Class,其中定义了一些方法、状态、事件。将这段代码编译成以太坊虚拟机(Ethereum Virtual Machine,简称EVM)机器码,就可以作为 sendTransaction() 方法的 data 参数,被部署到以太坊区块链上(会自动分配一个地址),这样就创建了一个合约账户。后续按照EVM ABI约定发送到该合约账户的交易,可以指定触发这段代码的相应方法执行预定的操作。

这样的描述有点抽象,下面就用具体的栗子来说明合约账户。

## 7.1 ERC20

以太坊上线几年了,目前为止最成熟的应用就是发行 ERC20 Token ,圆了好多人的会所嫩模梦。所谓 ERC20 ,实际上就是一个 Interface ,凡是遵守这个 Interface 的合约,都可以被相同的方式触发。

具体的 ERC20 Interface 实现了多个方法和事件,详见Wiki:https://theethereum.wiki/w/index.php/ERC20_Token_Standard

下面的栗子里,只挑最重要的 balanceOf 和 transfer 这两个方法,以及 Transfer 这个事件:
引用
contract ERC20Interface {
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    function transfer(address to, uint tokens) public returns (bool success);
    event Transfer(address indexed from, address indexed to, uint tokens);
}

从interface的定义可以比较直接的看懂,balanceOf用于查询某个地址的余额,tranfer用于转账到某个地址;如果转账成功,transfer方法会触发一个 Transfer 事件,记录在区块链上。

下面看一个具体的代码 (保存为 token.sol ):

引用
pragma solidity ^0.4.0; //指定solidity编译器的最小版本

contract MyToken {
    string public name; //token的名称,例如 Tether USD
    string public symbol; //token的代码,例如 USDT
    uint8 public decimals; //token的精度,例如4表示该token精确到小数点后4位
    mapping (address => uint256) public balances; //该合约的账本,记录某地址的token余额

    event Transfer(address indexed from, address indexed to, uint256 value);

    /* 合约的初始化函数,在部署合约时被调用,指定供应量等信息 */
    function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) {
        balances[msg.sender] = initialSupply;
        name = tokenName;
        symbol = tokenSymbol;
        decimals = decimalUnits;
    }

    function transfer(address _to, uint256 _value) {
        /* 检查发送方余额充足,且不会溢出 */
        require(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value); //关注该合约的用户可以得知发生了一笔转账事件
    }

    function balanceOf(address tokenOwner) public constant returns (uint balance) {
        return balances[tokenOwner]; //查询某个地址的token余额
    }
}


## 7.2 编译合约

引用
$ solc token.sol --bin --abi
...(编译器warning等)...
======= token.sol:MyToken =======
Binary:
6060604052341{合约被编译后产生的EVM字节码}fd000029
Contract JSON ABI
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"tokenOwner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"tokenSymbol","type":"string"},{"name":"decimalUnits","type":"uint8"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]


## 7.3 部署合约

从上面的编译结果 copy 字节码 和 abi json,执行下面的命令:

引用
$ geth attach #连接到 geth 的 console
> var code = '0x6060604052341{EVM字节码}fd000029'; //这里以字符串形式保存字节码
> var abi = [{"constant":true,....:"event"}]; //注意这里不是字符串,而是json数组
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var contract = eth.contract(abi).new(1000000, 'Test Token', 'TEST', 2, {from: eth.coinbase, data: code, gas: 1000000})
> contract.transactionHash
"0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c"

创建合约交易:初始化参数、合约部署方、gas limit;因为没有接收方,也不需要发送以太币,所以省略to和value

挖矿确认交易,然后看看部署的结果:
引用
> miner.start(); admin.sleepBlocks(1); miner.stop();
true
> eth.getTransaction(contract.transactionHash)
{
  blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
  blockNumber: 2,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gas: 1000000,
  gasPrice: 1000000000,
  hash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
  input: "0x606060{EVM字节码}fd000029{合约初始化参数的ABI编码}",
  nonce: 1,
  r: "0x32c5cb0c44b03dd273b6973f59f85c8e09d0d6212a38263b3c199329dbbc9ef",
  s: "0x758efe028c494fc8e637792aa3e09adceb1e7cdafb8567d4a7eecf95f54accc4",
  to: null,
  transactionIndex: 0,
  v: "0x200024",
  value: 0
}
> eth.getTransactionReceipt(contract.transactionHash)
{
  blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
  blockNumber: 2,
  contractAddress: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
  cumulativeGasUsed: 594886,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gasUsed: 594886,
  logs: [],
  logsBloom: "0x0000{省略一些0}0000",
  status: "0x1",
  to: null,
  transactionHash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
  transactionIndex: 0
}
> contract.address
"0x52ce1b10f214abb88517ceb3de2fbfbbfc305791"


可以看到,发送的交易在被挖矿确认以后,部署到了 0x52ce1b10f214abb88517ceb3de2fbfbbfc305791 这个地址,消耗了 594886 个 gas 。这样就“发行”了一个ERC20 Token,名字是 Test Token,代码 TEST ,一共发行 10000 个,每个 token 精确到小数点后 2 位。

## 7.4 调用合约

上面的 contract 变量就是合约对象了,但不能每次想调用一个合约都得创建它,通常我们是需要直接调用某个合约地址里的方法,那就需要用这种方式创建合约对象:
引用
> var abi = [{"contract"...}];
> var contract = eth.contract(abi).at("0x52ce1b10f214abb88517ceb3de2fbfbbfc305791")

余额查询:
引用
> contract.balanceOf.call(eth.coinbase)
1000000

可以看到,由于我们是用 coinbase 帐号发起的交易,合约在初始化的时候,根据合约代码的实现,把初始发行的 10000 个 TEST 都记到了 coinbase 帐号名下。要特别注意的是,这个账,不是记在 coinbase 这个EOA帐号的存储里,而是记在这个合约帐号的存储(代码里balances这个map)里面。

转账(误):
引用
> contract.transfer.call(eth.accounts[1], 100)
[]
> contract.balanceOf(eth.accounts[1])
0

可以看到,虽然调用了transfer方法,但是 eth.accounts[1] 的余额并没变化。这是因为transfer方法是要修改存储的,而以太坊上的状态,只能由被区块链确认了的交易出发的动作修改。

那就试试往区块链上发送一笔转账交易:
引用
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> tx_hash = contract.transfer.sendTransaction(eth.accounts[1], 100, {from:eth.coinbase, gas:100000})
"0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad"


挖矿确认这笔交易,然后查看执行结果:
引用
> miner.start(); admin.sleepBlocks(1); miner.stop();
true
> contract.balanceOf(eth.accounts[1])
100
> contract.balanceOf(eth.coinbase)
999900
> eth.getTransaction(tx_hash)
{
  blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
  blockNumber: 3,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gas: 100000,
  gasPrice: 1000000000,
  hash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
  input: "0xa9059cbb00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd0000000000000000000000000000000000000000000000000000000000000064",
  nonce: 2,
  r: "0x53b6312127838fe8cdca5209b51d33175f370b2a0921215d07f2df9a572c120d",
  s: "0xd18471632a066386c42a42226f441442f73e0ba39b5a9b6db58a283d957579a",
  to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
  transactionIndex: 0,
  v: "0x200024",
  value: 0
}
> eth.getTransactionReceipt(tx_hash)
{
  blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
  blockNumber: 3,
  contractAddress: null,
  cumulativeGasUsed: 51595,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gasUsed: 51595,
  logs: [{
      address: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
      blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
      blockNumber: 271,
      data: "0x0000000000000000000000000000000000000000000000000000000000000064",
      logIndex: 0,
      removed: false,
      topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b", "0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
      transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
      transactionIndex: 0
  }],
  logsBloom: "0x0000010{省略部分数据}0000000000000",
  status: "0x1",
  to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
  transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
  transactionIndex: 0
}

向区块链发送一个交易,等到交易被确认以后,再查询 eth.accounts[1] 的 token 余额,就可以看到发生了变化。

这里有几个比较重要的信息:

1) getTransaction 返回结果里的 input

之前的普通转账交易,input的内容(就是sendTransaction里指定的data)都是 "0x" ,也就是 0 字节,无数据。而这个交易,有了一个比较复杂的值。这个值可以被拆成3段:

    0xa9059cbb
        => 按照evm abi的约定,这是合约方法的签名的前4个字节。签名是用sha3对方法的定义进行hash计算出来的
        => 例如这个 0xa9059cbb 就来源于 sha3("transfer(address,uint256)")
    00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd
        => 这就是接收方的地址,但用32字节表示,前面用0补齐
    0000000000000000000000000000000000000000000000000000000000000064
        => 这是转账金额,32字节无符号整数,0x64 = 100

2) getTransactionReceipt 返回结果里的 logs
    logs是个数组,其中的每一项代表一个被触发的 event ,这个栗子里,logs[0].topics 包含3个元素:
   
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
        => 这是事件的签名,sha3("Transfer(address,address,uint256)")
    "0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b"
        => 这是发送方地址,也就是 eth.coinbase
    "0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
        => 这是接受方地址,也就是 eth.coinbase

    因为 Transfer 事件的定义里,from和to都带了一个 indexed 修饰符,所以会被记录到 topics 里,而没带 indexed 修饰符的 value ,则被记录在了 logs[0].data 里。

# 8. 以太坊的私钥、公钥、地址、钱包

以太坊和比特币一样,都是用的椭圆曲线加密算法 ECDSA-secp256k1。通常将私钥记为 k ,公钥记为 K 。下面使用node,安装bitcore-lib来演示。
引用
$ node
> var bitcore = require("bitcore-lib")


## 8.1 私钥

私钥 k 通常是随机选出的 32 字节数据(可以看作一个 uint256 大整数),但要特别注意,不能使用伪随机数生成器。更准确地说,私钥可以是1和n-1之间的任何数字,其中n是一个常数(n=1.158 * 10^77,略小于2^256)。从编程的角度来看,一般是通过在一个密码学安全的随机源中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。如果运算结果小于n-1,我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。

引用
> var k = bitcore.PrivateKey()
> k.toBuffer().hexSlice()
'3f4d9a11b162c61af91c63e601b0dadaec09c74abb7f6659364549011b0dcf15'


## 8.2 公钥

将私钥 k 与一个指定的生成点 G 运算,就能得到公钥 K = k * G。K是椭圆曲线上的一个点 (x, y)。其中x和y都是uint256大整数。在以太坊里,公钥的二进制存储格式为 x + y,一共64字节。

引用
> var K = k.toPublicKey()
> K.point.x.toBuffer().hexSlice()
'fbc1994297bd2bdd5a4464ae7f6c88338587a175faffeb414fb2717207638435'
> K.point.y.toBuffer().hexSlice()
'b5d1a6015fc9fc6cf947046213cfb7d3341491934a90a2e3880af6a940cdd1df'
> var K_buffer = Buffer.concat([K.point.x.toBuffer(), K.point.y.toBuffer()])


## 8.3 地址

通过对公钥K做sha3(keccak256)哈希,得到一个32字节的结果,取其中的后20字节,就是以太坊的地址了:

引用
> const SHA3 = require('keccakjs')
> addr = '0x' + SHA3(256).update(K_buffer).digest('hex').slice(-40)
'0x2864cdd53410820bc684f746871e9820fbb57243'


这样就得到了一个以太坊的地址。为了检测在地址传递过程中发生的错误,EIP55给出了一个校验码方案,其思路很简单,对地址再做一次sha3,根据哈希结果修改地址中对应位置的字符大小写,详细方案参见:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md

加上校验码后,地址长这样:0x2864Cdd53410820BC684F746871E9820fBb57243

## 8.4 钱包

由于以太坊和比特币都用的是 secp256k1 ,所以很多以太坊钱包是参考BIP32+BIP44实现的(包括KeepKey这样的硬件钱包),其中Coin Type用的是60H。

详情参考BIP44:https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

# 9. Gas/GasLimit/GasPrice

为了避免死循环导致的拒绝服务攻击,比特币区块链的目标是实现一个点对点的电子现金系统,因此其内置的脚本不是图灵完备的,不支持循环,只能完成一些条件判断。

以太坊有更大的野心,实现了一个图灵完备的EVM。为了解决这个拒绝服务攻击的问题,以太坊实现了这样一个机制:为了让EVM这台机器运转起来,你需要给它提供“加密燃料”——gas。gas是用以太币计价的,因此在发送交易的时候,需要指定购买gas的价格——gasPrice。由于停机问题不可解,为了避免过度消耗,交易发送者还需要指定gasLimit(对应sendTransaction方法的gas参数),表示他愿意为这笔交易的执行支付的最大gas数量。

当EVM收到开始执行一个交易的时候,会先冻结 value + gas * gasPrice 这些以太币(转账金额 + 为这笔交易愿意支付的最大金额),然后开始执行交易。以太坊的黄皮书里定义了不同操作需要消耗的gas数量,EVM在执行过程中,会根据定义好的规则累计消耗的gas总量。如果交易尚未执行完,但所有gas都消耗完,EVM会回滚所有改动,并将交易标记为失败(getTransactionReceipt返回的status='0x0')。无论交易最终是否成功,EVM累计消耗的gas都会被扣除,记录在 receipt 的 gasUsed 字段里,剩余的gas则按gasPrice换成以太币,退给交易发送者。特别地,普通转账交易消耗的gas数量正好是21000。作为以太坊网络的维护者和验证者,挖出包含这个交易的block的矿工可以获得这些被消耗的gas。

因此,如果有人要用死循环来恶意攻击以太坊网络,他就要付出无限的以太币。


# 10. 其他

## 10.1 以太币的总量

以太坊正式上线时,发行了72002454个以太币,每年会按这个数字的25%增发(即不超过1800万)。截止到目前(2018-03-17),流通总量为9823万(数据来源于 coinmarketcap.com)。

## 10.2 大整数:十六进制

由于以太币的最小单位 wei = 1/10^18 ether,所以连 UINT64 都无法保存所有的以太币。很多语言没有提供原生的大整数支持,所以以太坊的rpc接口传递大整数时都使用16进制编码,由语言自行处理。

## 10.3 版本

以太坊最早于是2015年5月上线 Olympic 测试网,7月30日上线 Frontier 版本。Frontier是beta版本,供开发者学习、体验,直到2016年3月14日(圆周率日)上线了正式的 Homestead 版本。

以太坊后续还规划了 Metropolis 和 Serenity 两个版本,上线时间尚未确定。

## 10.4 挖矿和共识机制

Homestead 的共识机制与比特币一样,都是PoW(Proof of Work),但使用了一个基于DAG的Ethash算法,对内存的要求较高,因此像ASIC矿机这种大规模嵌入运算单元的方式无法加速挖矿效率,从而尽量避免挖矿的中心化,增加网络的安全性。

在接下来的Metropolis版本里,以太坊计划切换为PoS机制,提高出块的效率。

## 10.5 还有啥?

想起来再补吧。
分页: 1/96 第一页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]