一 怎么考虑数据一致性问题
1.单体应用的数据一致性
想象一下如果我们经营着一家大型企业,下属有航空公司、租车公司、和连锁酒 店。我们为客户提供一站式的旅游行程规划服务,这样客户只需要提供出行目的 地,我们帮助客户预订机票、租车、以及预订酒店。从业务的角度,我们必须保 证上述三个服务的预订都完成才能满足一个成功的旅游行程,否则不能成行。
我们的单体应用要满足这个需求非常简单,只需将这个三个服务请求放到同一个 数据库事务中,数据库会帮我们保证全部成功或者全部回滚。
2.微服务场景下的数据一致性
这几年中,我们的行程规划服务非常成功,企业蒸蒸日上,用户量也翻了数十
倍。企业的下属航空公司、租车公司、和连锁酒店也相继推出了更多服务以满足 客户需求,我们的应用和开发团队也因此日渐庞大。如今我们的单体应用已变得 如此复杂,以至于没人了解整个应用是怎么运作的。更糟的是新功能的上线现在 需要所有研发团队合作,日夜奋战数周才能完成。看着市场占有率每况愈下,公 司高层对研发部门越来越不满意。
经过数轮讨论,我们最终决定将庞大的单体应用一分为四:机票预订服务、租车 服务、酒店预订服务、和支付服务。服务各自使用自己的数据库,并通过HTTP 协议通信。负责各服务的团队根据市场需求按照自己的开发节奏发版上线。如今 我们面临新的挑战:如何保证最初三个服务的预订都完成才能满足一个成功的旅 游行程,否则不能成行的业务规则?现在服务有各自的边界,而且数据库选型也 不尽相同,通过数据库保证数据一致性的方案已不可行。
二 分布式锁的场景与实现
1.使用场景
首先,我们看这样一个场景:客户下单的时候,我们调用库存中心进行减库存 那我们一般的操作都是:
update store set num = $num where id = $id
这种通过设置库存的修改方式,我们知道在并发量高的时候会存在数据库的丢失 更新,比如a, b当前两个事务,查询出来的库存都是5, a买了 3个单子要把库 存设置为2,而b买了 1个单子要把库存设置为4,那这个时候就会出现a会覆 盖b的更新,所以我们更多的都是会加个条件:
update store set num = $num where id = $id and num = $query_num
即乐观锁的方式来处理,当然也可以通过版本号来处理乐观锁,都是一样的,但 是这是更新一个表,如果我们牵扯到多个表呢,我们希望和这个单子关联的所有 的表同一时间只能被一个线程来处理更新,多个线程按照不同的顺序去更新同一 个单子关联的不同数据,出现死锁的概率比较大。对于非敏感的数据,我们也没 有必要去都加乐观锁处理,我们的服务都是多机器部署的,要保证多进程多线程 同时只能有一个进程的一个线程去处理,这个时候我们就需要用到分布式锁。分 布式锁的实现方式有很多,我们今天分别通过数据库,Zookeeper, Redis以及 Tair的实现逻辑
2.Zookeeper实现
1.获取锁
1.先有一个锁跟节点,lockRootNode,这可以是一个永久的节点
2.客户端获取锁,先在lockRootNode下创建一个顺序的瞬时节点,保证客户 端断开连接,节点也自动删除
3.调用lockRootNode父节点的getChildren。方法,获取所有的节点,并从小 到大排序,如果创建的最小的节点是当前节点,则返回true,获取锁成功,否则,关注比自己序号小的节点的释放动作(exist watch),这样可以保证每一 个客户端只需要关注一个节点,不需要关注所有的节点,避免羊群效应。
4.如果有节点释放操作,重复步骤3
2.释放锁
只需要删除步骤2中创建的节点即可
使用Zookeeper的分布式锁存在什么样的优缺点呢?
3.优点
•客户端如果出现宕机故障的话,锁可以马上释放
•可以实现阻塞式锁,通过watcher监听,实现起来也比较简单
•集群模式,稳定性比较高
4.缺点
一 旦网络有任何的抖动,Zookeeper就会认为客户端已经宕机,就会断掉连 接,其他客户端就可以获取到锁。当然Zookeeper有重试机制,这个就比较 依赖于其重试机制的策略了
•性能上不如缓存
3.Redis实现
我们先举个例子,比如现在我要更新产品的信息,产品的唯一键就是productIdpublic boolean lock(String key, V v, int expireTime)(
int retry = 0;
//获取锁失败最多尝试10次
while (retry < failRetryTimes)(
//获取锁
Boolean result = redis.setNx(key, v, expireTime);
if (result)(
return true;
}
try (
〃获取锁失败间隔一段时间重试
TimeUnit.MILLISECONDS.sleep(sleepInterval);
} catch (InterruptedException e) (
Thread,currentThread()・interrupt(); return false;
}
}
return false;
}
public boolean unlock(String key)(
return redis.delete(key);
}
public static void main(String[] args) (
Integer productId = 324324;
RedisLock<Integer> redisLock = new RedisLock<Integer>(); redisLock.lock(productId+"", productId, 1000);
}
}
三.分布式事务
1.分布式一致性
在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本 (replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的 CRUD等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。
为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程 中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase
Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和 Paxos 算法
2.分布式事务
分布式事务是指会涉及到操作多个数据库的事务.其实就是将对同一库事务的 概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致 性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做 的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部 回滚)
在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。 由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但 是,相互独立的节点之间无法准确的知道其他节点中的事务执行情况。所以从理 论上讲,两台机器理论上无法达到一致的状态。如果想让分布式部署的多台机器 中的数据保持一致性,那么就要保证在所有节点的数据写操作,要不全部都执 行,要么全部的都不执行。但是,一台机器在执行本地事务的时候无法知道其他 机器中的本地事务的执行结果。所以他也就不知道本次事务到底应该commit还 是rollback。所以,常规的解决办法就是引入一个“协调者”的组件来统一调度所 有分布式节点的执行。数据库事务必须具备ACID特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。
1.2PC
二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为 了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议
(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或 者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节 点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一 掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结 果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交 的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所 有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段 (执行阶段)。
2.准备阶段
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与 者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和 undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
可以进一步将准备阶段分为以下三个步骤:
1.协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等 待各参与者节点的响应。
2.参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信 息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
3.各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际 执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行 失败,则它返回一个”中止”消息。
3.提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚( Rollback )消息;否则,发送提交(Commit )消息;参与者根据协调者的指令执行 提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后 阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
当协调者节点从所有参与者节点获得的相应消息都为”同意”时:
1.协调者节点向所有参与者节点发出”正式提交(commit )”的请求。
2.参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
3.参与者节点向协调者节点发送”完成”消息。
4.协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
如果任一参与者节点在第一阶段返回的响应消息为"中止”,或者协调者节点在第 —阶段的询问超时之前无法获取所有参与者节点的响应消息时:
1.协调者节点向所有参与者节点发出”回滚操作(rollback广的请求。
2.参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内 占用的资源。
3.参与者节点向协调者节点发送”回滚完成”消息。
4.协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。
有参与者节点发出”回滚操作(rollback广的请求。
2.参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内 占用的资源。
3.参与者节点向协调者节点发送”回滚完成”消息。
4.协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。