mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 12:46:41 +08:00
300 lines
20 KiB
HTML
300 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>27 分布式事务:我们到底要不要使用 2PC?.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/00 开篇词 从业务出发,开启海量 MySQL 架构设计.md.html">00 开篇词 从业务出发,开启海量 MySQL 架构设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/01 数字类型:避免自增踩坑.md.html">01 数字类型:避免自增踩坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/02 字符串类型:不能忽略的 COLLATION.md.html">02 字符串类型:不能忽略的 COLLATION</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/03 日期类型:TIMESTAMP 可能是巨坑.md.html">03 日期类型:TIMESTAMP 可能是巨坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/04 非结构存储:用好 JSON 这张牌.md.html">04 非结构存储:用好 JSON 这张牌</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/05 表结构设计:忘记范式准则.md.html">05 表结构设计:忘记范式准则</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/06 表压缩:不仅仅是空间压缩.md.html">06 表压缩:不仅仅是空间压缩</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/07 表的访问设计:你该选择 SQL 还是 NoSQL?.md.html">07 表的访问设计:你该选择 SQL 还是 NoSQL?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/08 索引:排序的艺术.md.html">08 索引:排序的艺术</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/09 索引组织表:万物皆索引.md.html">09 索引组织表:万物皆索引</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/10 组合索引:用好,性能提升 10 倍!.md.html">10 组合索引:用好,性能提升 10 倍!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/11 索引出错:请理解 CBO 的工作原理.md.html">11 索引出错:请理解 CBO 的工作原理</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/12 JOIN 连接:到底能不能写 JOIN?.md.html">12 JOIN 连接:到底能不能写 JOIN?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/13 子查询:放心地使用子查询功能吧!.md.html">13 子查询:放心地使用子查询功能吧!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/14 分区表:哪些场景我不建议用分区表?.md.html">14 分区表:哪些场景我不建议用分区表?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/15 MySQL 复制:最简单也最容易配置出错.md.html">15 MySQL 复制:最简单也最容易配置出错</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/16 读写分离设计:复制延迟?其实是你用错了.md.html">16 读写分离设计:复制延迟?其实是你用错了</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/17 高可用设计:你怎么活用三大架构方案?.md.html">17 高可用设计:你怎么活用三大架构方案?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/18 金融级高可用架构:必不可少的数据核对.md.html">18 金融级高可用架构:必不可少的数据核对</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/19 高可用套件:选择这么多,你该如何选?.md.html">19 高可用套件:选择这么多,你该如何选?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/20 InnoDB Cluster:改变历史的新产品.md.html">20 InnoDB Cluster:改变历史的新产品</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/21 数据库备份:备份文件也要检查!.md.html">21 数据库备份:备份文件也要检查!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/22 分布式数据库架构:彻底理解什么叫分布式数据库.md.html">22 分布式数据库架构:彻底理解什么叫分布式数据库</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/23 分布式数据库表结构设计:如何正确地将数据分片?.md.html">23 分布式数据库表结构设计:如何正确地将数据分片?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md.html">24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/25 分布式数据库架构选型:分库分表 or 中间件 ?.md.html">25 分布式数据库架构选型:分库分表 or 中间件 ?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/26 分布式设计之禅:全链路的条带化设计.md.html">26 分布式设计之禅:全链路的条带化设计</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/MySQL实战宝典/27 分布式事务:我们到底要不要使用 2PC?.md.html">27 分布式事务:我们到底要不要使用 2PC?</a>
|
||
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>27 分布式事务:我们到底要不要使用 2PC?</h1>
|
||
<p>你好,我是姜承尧,前面我们学习了分布式数据库中数据的分片设计、索引设计、中间件选型,全链路的条带化设计。但是我们一直在回避分布式数据库中最令人头疼的问题,那就是分布式事务。</p>
|
||
<p>今天,我们就来学习分布式事务的概念,以及如何在海量互联网业务中实现它。</p>
|
||
<h3>分布式事务概念</h3>
|
||
<p>事务的概念相信你已经非常熟悉了,事务就是要满足 ACID 的特性,总结来说。</p>
|
||
<ul>
|
||
<li>A(Atomicity) 原子性:事务内的操作,要么都做,要么都不做;</li>
|
||
<li>C(Consistency) 一致性:事务开始之前和事务结束以后,数据的完整性没有被破坏;如唯一性约束,外键约束等;</li>
|
||
<li>I(Isolation)隔离性:一个事务所做的操作对另一个事务不可见,好似是串行执行;</li>
|
||
<li>D(Durability)持久性:事务提交后,数据的修改是永久的。即使发生宕机,数据也能修复;</li>
|
||
</ul>
|
||
<p>特别需要注意的是,当前数据库的默认事务隔离级别都没有达到隔离性的要求,MySQL、Oracle、PostgreSQL等关系型数据库都是如此。大多数数据库事务隔离级别都默认设置为 READ-COMMITTED,这种事务隔离级别没有解决可重复度和幻读问题。</p>
|
||
<p>但由于在绝大部分业务中,都不会遇到这两种情况。若要达到完全隔离性的要求,性能往往又会比较低。因此在性能和绝对的隔离性前,大多数关系型数据库选择了一种折中。</p>
|
||
<p>那什么是分布式事务呢?简单来说,就是要在分布式数据库的架构下实现事务的ACID特性。</p>
|
||
<p>前面我们讲了分布式数据库架构设计的一个原则,即大部分的操作要能单元化。即在一个分片中完成。如对用户订单明细的查询,由于分片键都是客户ID,因此可以在一个分片中完成。那么他能满足事务的ACID特性。</p>
|
||
<p>但是,如果是下面的一个电商核心业务逻辑,那就无法实现在一个分片中完成,即用户购买商品,其大致逻辑如下所示:</p>
|
||
<pre><code>START TRANSATION;
|
||
INSERT INTO orders VALUES (......);
|
||
INSERT INTO lineitem VALUES (......);
|
||
UPDATE STOCK SET COUNT = COUNT - 1 WHERE sku_id = ?
|
||
COMMIT;
|
||
</code></pre>
|
||
<p>可以看到,在分布式数据库架构下,表orders、linitem的分片键是用户ID。但是表stock是库存品,是商品维度的数据,没有用户ID的信息。因此stock的分片规则肯定与表orders和lineitem不同。</p>
|
||
<p>所以,上述的事务操作大部分情况下并不能在一个分片中完成单元化,因此就是一个分布式事务,它要求用户维度的表 orders、lineitem 和商品维度的表 stock 的变更,要么都完成,要么都完成不了。</p>
|
||
<p>常见的分布式事务的实现就是通过 2PC(two phase commit 两阶段提交)实现,接着我们来看下 2PC。</p>
|
||
<h3>2PC的分布式事务实现</h3>
|
||
<p>2PC 是数据库层面实现分布式事务的一种强一致性实现。在 2PC 中,引入事务协调者的角色用于协调管理各参与者(也可称之为各本地资源)的提交和回滚。而 2PC 所谓的两阶段是指parepare(准备)阶段和 commit(提交)两个阶段。</p>
|
||
<p>在 2PC 的实现中,参与者就是分钟的 MySQL 数据库实例,那事务协调者是谁呢?这取决于分布式数据库的架构。若分布式数据库的架构采用业务通过分库分表规则直连分片的话,那么事务协调者就是业务程序本身。如下图所示:</p>
|
||
<p><img src="assets/Cgp9HWEVB0iAEvVdAAHugD2AY7I024.png" alt="图片1" /></p>
|
||
<p>若采用数据库中间件的模式,那么事务协调者就是数据库中间件。如下图所示:</p>
|
||
<p><img src="assets/CioPOWEVB3aAGla7AAGq-ZbCoGU801.png" alt="图片2" /></p>
|
||
<p>从上图可以发现,使用分布式数据库中间件后,可以对上层服务屏蔽分布式事务的实现,服务不需要关心下层的事务是本地事务还是分布式事务,就好像是单机事务本身一样。</p>
|
||
<p>2PC 要求第一段 prepare 的操作都成功,那么分布式事务才能提交,这样最终能够实现持久化,2PC 的代码逻辑如下所示:</p>
|
||
<p><img src="assets/Cgp9HWEVB9KAXvPMAAex-bcCE8c431.png" alt="图片3" /></p>
|
||
<p>上面就是 2PC 的 Java 代码实现,可以看到只有2个参与者第一阶段 prepare 都成功,那么分布式事务才能提交。</p>
|
||
<p>但是 2PC 的一个难点在于 prepare 都成功了,但是在进行第二阶段 commit 的时候,其中一个节点挂了。这时挂掉的那个节点在恢复后,或进行主从切换后,节点上之前执行成功的prepare 事务需要人为的接入处理,这个事务就称之为悬挂事务。</p>
|
||
<p>用户可以通过命令 XA_RECOVER 查看节点上事务有悬挂事务:</p>
|
||
<p><img src="assets/CioPOWEVB_GAScw3AALL2SV2QHs125.png" alt="图片4" /></p>
|
||
<p>如果有悬挂事务,则这个事务持有的锁资源都是没有释放的。可以通过命令SHOW ENGINE INNODB STATUS 进行查看:
|
||
<img src="assets/CioPOWEVCBWAUJX7AAXrOE_0ylE945.png" alt="png" /></p>
|
||
<p>从上图可以看到,事务 5136 处于 PREPARE状态,已经有 218 秒了,这就是一个悬挂事务,并且这个事务只有了两个行锁对象。</p>
|
||
<p>可以通过命令 XA RECOVER 人工的进行提交:
|
||
<img src="assets/CioPOWEVCCyAfJTjAAPcHT0OqKw598.png" alt="png" /></p>
|
||
<p>讲到这,同学们应该都了了分布式事务的 2PC 实现和使用方法。它是一种由数据库层实现强一致事务解决方案。其优点是使用简单,当前大部分的语言都支持 2PC 的实现。若使用中间件,业务完全就不用关心事务是不是分布式的。</p>
|
||
<p>然而,他的缺点是,事务的提交开销变大了,从 1 次 COMMIT 变成了两次 PREPARE 和COMMIT。而对于海量的互联网业务来说,2PC 的性能是无法接受。因此,这就有了业务级的分布式事务实现,即柔性事务。</p>
|
||
<h3>柔性事务</h3>
|
||
<p>柔性事务是指分布式事务由业务层实现,通过最终一致性完成分布式事务的工作。可以说,通过牺牲了一定的一致性,达到了分布式事务的性能要求。</p>
|
||
<p>业界常见的柔性事务有 TCC、SAGA、SEATA 这样的框架、也可以通过消息表实现。它们实现原理本身就是通过补偿机制,实现最终的一致性。柔性事务的难点就在于对于错误逻辑的处理。</p>
|
||
<p>为了讲述简单,这里用消息表作为柔性事务的案例分享。对于上述电商的核心电商下单逻辑,用消息表就拆分为 3 个阶段:</p>
|
||
<pre><code>阶段1:
|
||
START TRANSACTION;
|
||
# 订单号,订单状态
|
||
INSERT INTO orders VALUES (...)
|
||
INSERT INTO lineitem VALUES (...)
|
||
COMMIT;
|
||
阶段2:
|
||
START TRANSACTION;
|
||
UPDATE stock SET count = count -1 WHERE sku_id = ?
|
||
# o_orderkey是消息表中的主键,具有唯一约束
|
||
INSERT INTO stock_message VALUES (o_orderkey, ... )
|
||
COMMIT;
|
||
阶段3:
|
||
UPDATE orders SET o_orderststus = 'F' WHERE o_orderkey = ?
|
||
</code></pre>
|
||
<p>上面的柔性事务中,订单表中的列 o_orderstatus 用于记录柔性事务是否完成,初始状态都是未完成。表 stock_message 记录对应订单是否已经扣除过相应的库存。若阶段 2 完成,则柔性事务必须完成。阶段 3 就是将柔性事务设置为完成,最终一致性的确定。</p>
|
||
<p>接着我们来下,若阶段 2 执行失败,即执行过程中节点发生了宕机。则后台的补偿逻辑回去扫描订单表中 o_orderstatus 为未完成的超时订单有哪些,然后看一下是否在对应的表stock_message 有记录,若有,则执行阶段 3。若无,可选择告知用户下单失败。</p>
|
||
<p>若阶段 3 执行失败,处理逻辑与阶段 2 基本一致,只是这时 2 肯定是完成的,只需要接着执行阶段 3 即可。</p>
|
||
<p>所以,这里的补偿逻辑程序就是实时/定期扫描超时订单,通过消息表判断这个柔性事务是继续执行还是执行失败,执行失败又要做哪些业务处理。</p>
|
||
<p>上面介绍的框架实现的柔性事务原理大致如此,只不过对于补偿的逻辑处理有些不同,又或者使用上更为通用一些。</p>
|
||
<p>对于海量的互联网业务来说,柔性事务性能更好,因此支付宝、淘宝等互联网业务都是使用柔性事务完成分布式事务的实现。</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/MySQL实战宝典/26 分布式设计之禅:全链路的条带化设计.md.html">上一页</a>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997337fbce3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|