learn.lianglianglee.com/专栏/MySQL实战宝典/27 分布式事务:我们到底要不要使用 2PC?.md.html
2022-09-06 22:30:37 +08:00

300 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>AAtomicity 原子性:事务内的操作,要么都做,要么都不做;</li>
<li>CConsistency 一致性:事务开始之前和事务结束以后,数据的完整性没有被破坏;如唯一性约束,外键约束等;</li>
<li>IIsolation隔离性一个事务所做的操作对另一个事务不可见好似是串行执行</li>
<li>DDurability持久性事务提交后数据的修改是永久的。即使发生宕机数据也能修复</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>常见的分布式事务的实现就是通过 2PCtwo 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>