这个海量存储系列是淘宝核心系统团队分享的一篇文章,这里转帖第六节,单机事务面临的问题及解决办法,很值得我们借鉴
全部章节可以参考:http://blog.csdn.net/heiyeshuwu/article/details/9722443
在我开始做淘宝数据层的时候,被问得最多的无非也就是:如何做事务,如何做join.至今仍然如此,我一般都会简单而明确的跟对方说:没有高效的实现方法。
虽然没有高效的实现,但实现还是有的。作为引子,我们先来介绍一下这种实现的方式。
我们仍然以上一次讲到的bob和smith为例子来说明好了。
开始的时候。Bob要给smith100块,那么实际上事务中要做的事情是
事务开始时查询bob有多少钱。如果有足够多的钱让bob的账户 -100 ,然后给smith 的账户+100 。最后事务结束。
如果这个事情在单机,那么事情可以使用锁的方式加以解决。
但如果bob在一台机器,smith在另外一台机器,我们应该怎么做呢?
第一种最常被人想起的方法,就是两段提交协议。
两段提交协议从原理上来说是非常简单的一套协议。
Prepare(bob-100) at 机器A->prepare (smith+100) at 机器b ->commit(bob) ->commit(smith)
事务结束。
两段提交的核心,是在prepare的阶段,会对所有该操作所影响的数据加锁,这样就可以阻止其他人(或机器)对他的访问。题外话,问个问题: )如果这时有其他节点,用相反的方向,进行更新,也就是先更新smith,然后更新bob.会有可能发生什么事情呢?
两段提交协议是被我们在大部分场景下放弃的一个模型,原因主要是因为
1. Tm本身需要记录事务进行的过程,log要保证安全和可信,性能非常低。
2. 锁的利用率和并行性较低。
3. 网络开销较大
4. 可见性要求实际上就等于让快的操作等慢的。
所以从性能角度来说,这类需求不多也不常见。
既然这样的模型不行,有没有其他模型可以使用呢?
有的。
在事务的过程中,细心的读者不难发现,实际上事务中并不需要这么强的一致可见性。
Bob是需要强一致的,因为他的操作仰赖于他有多少钱,如果他的钱不够100,那么是不能让他的账户变为负数的。但smith却不需要,smith不需要判断他的账户有多少钱,只需要把钱加到他的账户里,不少给他,到账时间尽可能短就可以。
Smith不需要chech账户的钱数,这个前提非常重要,这也是我们能使用最终一致性的关键因素。
下面,我们来看一下另外的选择吧。
Bob的账号在机器A上,smith的账号在机器b上。
首先,我们在机器A上做以下操作:
1. 本地事务开始
2. 读取bob的账户
3. 判断是否有充足余额
4. 更新bob的账号,将bob的钱减少100
5. 将需要给smith加100块这个操作,以事务的形式插入到同机(A)的一张log表中,并自动生成一个唯一的transactionID。
6. 事务关闭
然后,异步的发送一个通知,给一个消费者。
消费者接到通知后,从bob的机器上读取到需要给smith+100这个操作,以及该操作所对应的transactionID。
然后,按照如下方法进行运作
1. 查看在去重表内是否有对应的transactionID.如果没有,则
2. 开启本地事务
3. 将smith的账户+100
4. 将transactionID 插入去重表
5. 事务结束
这样,我们也可以完成一个交易的核心流程了。在交易类过程中的大量事务操作,都是以这样的方式完成的。
下面,我们针对上面的这个流程的一些抉择的点进行一些探讨。
首先,是bob这个机器,这里涉及第一个抉择点。
如果bob是个消费大户,短时间内进行了大量购买,那么可能会造成的问题是,bob所在的那个机器会成为热点,如果在某个突发的情况下,某个账户突然成为热点,那么这些有状态的数据很难快速的反应并加以处理,会造成事务数在某个单节点大量堆积。造成挂掉。
可能的解决方法是:
1. 利用两段提交协议来让原来的” 将需要给smith加100块这个操作,以事务的形式插入到同机(A)的一张log表中,并自动生成一个唯一的transactionID”这步操作放在另外的一台机器上进行。
这样做的的好处是,无论bob怎么是热点,都可以通过水平的加log机器的方式来防止这种热点的产生。
坏处则有:
1方案复杂度高
2额外的网络开销
3消息基于网络发送后,会可能得到三个可能的反馈:1. 成功 2. 失败 3. 无反馈。最麻烦的就是这个无反馈,他可能成功,也可能失败。所以是不确定的状态,需要进行事务的两边进行第二次确认,来确保这个事务的参与方是否都做了该做的事情,如果有一方做了类似commit的操作,那么另外的一方应该commit.如果两方都没做commit操作,那么应该回滚。
2. 让bob的库余量更高,并按照访问压力进行数据的切分,按照热度进行数据划分,放弃原有的简单取mod的策略。来兼容这种不均匀特性。
其次,如果有80个系统都关注着smith加了100这个操作的log,要做对应的处理(比如一些人要针对这个加钱操作做个打款短信推送,有些要做个数据分析等等),那么这里就有另外一个问题,这些系统对bob所在的库的读取就会让该机器成为悲剧的存在。
所以,可以考虑的方式是,增加一个队列,使用,推,拉,或推拉结合的方式将smith加100这个操作加以分发。这样就可以减轻主机的压力。
坏处则是:
1方案进一步复杂
2如何保证log到数据分发服务器之间的数据同步是安全的和准确的?
3如何保证分发服务器的可靠和冗余?
4如何保证写入分发服务器的数据的安全和可靠?
再次,smith这边也有问题,为什么要使用一张去重表呢?其实是因为,在发送端,也就是队列将数据发送到目标机器后,也可能从目标机获取到三种不同的反馈,一类是成功(这个占了大多数)。一类是失败。还有一类是。。。没反馈。
当然,最麻烦的还是这个没反馈的情况,没人知道这时候到底对方是做成功了呢?还是没做成功,为了保证最大的吞吐量,又不能其他人都不做事儿了,就等对方的反馈。所以这里就有另外的权衡了。
一般的模型有两类,一类是用分布式事务来完成。
一类是使用努力送达的模型,说叫努力送达,顾名思义,就是只有得到成功的反馈,才停止投递,而其他时候则重复投递消息,直到对方反馈成功为止。
两种模型比较,显然应该追求速度而放弃方便性,于是我们主要来说说这个努力送达以后所带来的影响。
影响一 : 会有重复的投递,也就是说,这个消息可能会投多次,这对于update set version=version+1 这类的操作来说,是个比较毁灭性的打击。
影响二:如果需要重复投递的消息过多,会导致log分发的机器消耗大量资源来进行重复投递。这会影响server的稳定性
影响三:如果大量堆积消息,那么会造成消息的严重delay。smith发现自己在1个月后收到了bob的钱,你说他会不会去K咱一顿: ) .
最后,额外记的这两次log其实在某些场景下也是可以省去的。
以上,就是我在尝试还原淘宝的消息和事务系统时所能大概想到的一些非常需要权衡和注意的问题点。
小小总结一下,整个问题的核心其实是幂等,说白了就是要能够理解数据基于网络的同步过程中,无反馈是一个经常发生的现象,在这种现象中,重复投递比傻傻等待要有效率的多。所以,重复作为一个side affect也就被默认的存在于系统中,所有的工程师都需要认识到这个问题的客观存在,并采取方法去解决之。
在基于网络的数据同步过程中,如果需要最大化性能,那么,一致性是第一个被放弃的。然后数据和消息不会出现重复,是第二个被放弃指标。
使用这种模型,我们可以放弃原来快得等慢的的模式,让整体的吞吐量和性能不会受制于锁的限制,所以淘宝和支付宝才能够支持如此大的交易量。完成大量交易订单。