mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 16:56:40 +08:00
1063 lines
32 KiB
HTML
1063 lines
32 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>42 电商系统的分布式事务调优.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="/专栏/Java并发编程实战/00 开篇词你为什么需要学习并发编程?.md.html">00 开篇词你为什么需要学习并发编程?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/01 如何制定性能调优标准?.md.html">01 如何制定性能调优标准?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/02 如何制定性能调优策略?.md.html">02 如何制定性能调优策略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/03 字符串性能优化不容小觑,百M内存轻松存储几十G数据.md.html">03 字符串性能优化不容小觑,百M内存轻松存储几十G数据.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/04 慎重使用正则表达式.md.html">04 慎重使用正则表达式.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/05 ArrayList还是LinkedList?使用不当性能差千倍.md.html">05 ArrayList还是LinkedList?使用不当性能差千倍.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/06 Stream如何提高遍历集合效率?.md.html">06 Stream如何提高遍历集合效率?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/07 深入浅出HashMap的设计与优化.md.html">07 深入浅出HashMap的设计与优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/08 网络通信优化之IO模型:如何解决高并发下IO瓶颈?.md.html">08 网络通信优化之IO模型:如何解决高并发下IO瓶颈?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/09 网络通信优化之序列化:避免使用Java序列化.md.html">09 网络通信优化之序列化:避免使用Java序列化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/10 网络通信优化之通信协议:如何优化RPC网络通信?.md.html">10 网络通信优化之通信协议:如何优化RPC网络通信?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/11 答疑课堂:深入了解NIO的优化实现原理.md.html">11 答疑课堂:深入了解NIO的优化实现原理.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/12 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法.md.html">12 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/13 多线程之锁优化(中):深入了解Lock同步锁的优化方法.md.html">13 多线程之锁优化(中):深入了解Lock同步锁的优化方法.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html">14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/15 多线程调优(上):哪些操作导致了上下文切换?.md.html">15 多线程调优(上):哪些操作导致了上下文切换?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/16 多线程调优(下):如何优化多线程上下文切换?.md.html">16 多线程调优(下):如何优化多线程上下文切换?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/17 并发容器的使用:识别不同场景下最优容器.md.html">17 并发容器的使用:识别不同场景下最优容器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/18 如何设置线程池大小?.md.html">18 如何设置线程池大小?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/19 如何用协程来优化多线程业务?.md.html">19 如何用协程来优化多线程业务?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/20 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型.md.html">20 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/21 深入JVM即时编译器JIT,优化Java编译.md.html">21 深入JVM即时编译器JIT,优化Java编译.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/22 如何优化垃圾回收机制?.md.html">22 如何优化垃圾回收机制?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/23 如何优化JVM内存分配?.md.html">23 如何优化JVM内存分配?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html">24 内存持续上升,我该如何排查问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.md.html">25 答疑课堂:模块四热点问题解答.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/26 单例模式:如何创建单一对象优化系统性能?.md.html">26 单例模式:如何创建单一对象优化系统性能?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/27 原型模式与享元模式:提升系统性能的利器.md.html">27 原型模式与享元模式:提升系统性能的利器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/28 如何使用设计模式优化并发编程?.md.html">28 如何使用设计模式优化并发编程?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/29 生产者消费者模式:电商库存设计优化.md.html">29 生产者消费者模式:电商库存设计优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html">30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/31 答疑课堂:模块五思考题集锦.md.html">31 答疑课堂:模块五思考题集锦.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/32 MySQL调优之SQL语句:如何写出高性能SQL语句?.md.html">32 MySQL调优之SQL语句:如何写出高性能SQL语句?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/33 MySQL调优之事务:高并发场景下的数据库事务调优.md.html">33 MySQL调优之事务:高并发场景下的数据库事务调优.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/34 MySQL调优之索引:索引的失效与优化.md.html">34 MySQL调优之索引:索引的失效与优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/35 记一次线上SQL死锁事故:如何避免死锁?.md.html">35 记一次线上SQL死锁事故:如何避免死锁?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/36 什么时候需要分表分库?.md.html">36 什么时候需要分表分库?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/37 电商系统表设计优化案例分析.md.html">37 电商系统表设计优化案例分析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/38 数据库参数设置优化,失之毫厘差之千里.md.html">38 数据库参数设置优化,失之毫厘差之千里.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/39 答疑课堂:MySQL中InnoDB的知识点串讲.md.html">39 答疑课堂:MySQL中InnoDB的知识点串讲.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/41 如何设计更优的分布式锁?.md.html">41 如何设计更优的分布式锁?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Java并发编程实战/42 电商系统的分布式事务调优.md.html">42 电商系统的分布式事务调优.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/43 如何使用缓存优化系统性能?.md.html">43 如何使用缓存优化系统性能?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/44 记一次双十一抢购性能瓶颈调优.md.html">44 记一次双十一抢购性能瓶颈调优.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/加餐 什么是数据的强、弱一致性?.md.html">加餐 什么是数据的强、弱一致性?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/加餐 推荐几款常用的性能测试工具.md.html">加餐 推荐几款常用的性能测试工具.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/答疑课堂:模块三热点问题解答.md.html">答疑课堂:模块三热点问题解答.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/结束语 栉风沐雨,砥砺前行!.md.html">结束语 栉风沐雨,砥砺前行!.md.html</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>42 电商系统的分布式事务调优</h1>
|
||
|
||
<p>你好,我是刘超。</p>
|
||
|
||
<p>今天的分享也是从案例开始。我们团队曾经遇到过一个非常严重的线上事故,在一次 DBA 完成单台数据库线上补丁后,系统偶尔会出现异常报警,我们的开发工程师很快就定位到了数据库异常问题。</p>
|
||
|
||
<p>具体情况是这样的,当玩家购买道具之后,扣除通宝时出现了异常。这种异常在正常情况下发生之后,应该是整个购买操作都需要撤销,然而这次异常的严重性就是在于玩家购买道具成功后,没有扣除通宝。</p>
|
||
|
||
<p>究其原因是由于购买的道具更新的是游戏数据库,而通宝是在用户账户中心数据库,在一次购买道具时,存在同时操作两个数据库的情况,属于一种分布式事务。而我们的工程师在完成玩家获得道具和扣除余额的操作时,没有做到事务的一致性,即在扣除通宝失败时,应该回滚已经购买的游戏道具。</p>
|
||
|
||
<p><strong>从这个案例中,我想你应该意识到了分布式事务的重要性。</strong></p>
|
||
|
||
<p>如今,大部分公司的服务基本都实现了微服务化,首先是业务需求,为了解耦业务;其次是为了减少业务与业务之间的相互影响。</p>
|
||
|
||
<p>电商系统亦是如此,大部分公司的电商系统都是分为了不同服务模块,例如商品模块、订单模块、库存模块等等。事实上,分解服务是一把双刃剑,可以带来一些开发、性能以及运维上的优势,但同时也会增加业务开发的逻辑复杂度。其中最为突出的就是分布式事务了。</p>
|
||
|
||
<p>通常,存在分布式事务的服务架构部署有以下两种:同服务不同数据库,不同服务不同数据库。我们以商城为例,用图示说明下这两种部署:</p>
|
||
|
||
<p><img src="assets/111f44892deb9919a1310d636a538f5a.jpg" alt="img" /></p>
|
||
|
||
<p><img src="assets/48d448543aeac5eba4b9edd24e1bcf6c.jpg" alt="img" /></p>
|
||
|
||
<p>通常,我们都是基于第二种架构部署实现的,那我们应该如何实现在这种服务架构下,有关订单提交业务的分布式事务呢?</p>
|
||
|
||
<h2>分布式事务解决方案</h2>
|
||
|
||
<p>我们讲过,在单个数据库的情况下,数据事务操作具有 ACID 四个特性,但如果在一个事务中操作多个数据库,则无法使用数据库事务来保证一致性。</p>
|
||
|
||
<p>也就是说,当两个数据库操作数据时,可能存在一个数据库操作成功,而另一个数据库操作失败的情况,我们无法通过单个数据库事务来回滚两个数据操作。</p>
|
||
|
||
<p>而分布式事务就是为了解决在同一个事务下,不同节点的数据库操作数据不一致的问题。在一个事务操作请求多个服务或多个数据库节点时,要么所有请求成功,要么所有请求都失败回滚回去。通常,分布式事务的实现有多种方式,例如 XA 协议实现的二阶提交(2PC)、三阶提交 (3PC),以及 TCC 补偿性事务。</p>
|
||
|
||
<p>在了解 2PC 和 3PC 之前,我们有必要先来了解下 XA 协议。XA 协议是由 X/Open 组织提出的一个分布式事务处理规范,目前 MySQL 中只有 InnoDB 存储引擎支持 XA 协议。</p>
|
||
|
||
<h3>1. XA 规范</h3>
|
||
|
||
<p>在 XA 规范之前,存在着一个 DTP 模型,该模型规范了分布式事务的模型设计。</p>
|
||
|
||
<p>DTP 规范中主要包含了 AP、RM、TM 三个部分,其中 AP 是应用程序,是事务发起和结束的地方;RM 是资源管理器,主要负责管理每个数据库的连接数据源;TM 是事务管理器,负责事务的全局管理,包括事务的生命周期管理和资源的分配协调等。</p>
|
||
|
||
<p><img src="assets/dcbb483b62b1e0a51d03c7edfcf89767.jpg" alt="img" /></p>
|
||
|
||
<p>XA 则规范了 TM 与 RM 之间的通信接口,在 TM 与多个 RM 之间形成一个双向通信桥梁,从而在多个数据库资源下保证 ACID 四个特性。</p>
|
||
|
||
<p>这里强调一下,JTA 是基于 XA 规范实现的一套 Java 事务编程接口,是一种两阶段提交事务。我们可以通过<a href="https://github.com/nickliuchao/jta">源码</a>简单了解下 JTA 实现的多数据源事务提交。</p>
|
||
|
||
<h3>2. 二阶提交和三阶提交</h3>
|
||
|
||
<p>XA 规范实现的分布式事务属于二阶提交事务,顾名思义就是通过两个阶段来实现事务的提交。</p>
|
||
|
||
<p>在第一阶段,应用程序向事务管理器(TM)发起事务请求,而事务管理器则会分别向参与的各个资源管理器(RM)发送事务预处理请求(Prepare),此时这些资源管理器会打开本地数据库事务,然后开始执行数据库事务,但执行完成后并不会立刻提交事务,而是向事务管理器返回已就绪(Ready)或未就绪(Not Ready)状态。如果各个参与节点都返回状态了,就会进入第二阶段。</p>
|
||
|
||
<p><img src="assets/2a1cf8f45675acac6fe07c172a36ec95.jpg" alt="img" /></p>
|
||
|
||
<p>到了第二阶段,如果资源管理器返回的都是就绪状态,事务管理器则会向各个资源管理器发送提交(Commit)通知,资源管理器则会完成本地数据库的事务提交,最终返回提交结果给事务管理器。</p>
|
||
|
||
<p><img src="assets/59734e1a229ceee9df4295d0901ce2d5.jpg" alt="img" /></p>
|
||
|
||
<p>在第二阶段中,如果任意资源管理器返回了未就绪状态,此时事务管理器会向所有资源管理器发送事务回滚(Rollback)通知,此时各个资源管理器就会回滚本地数据库事务,释放资源,并返回结果通知。</p>
|
||
|
||
<p><img src="assets/8791dfe19fce916f77b6c5740bc32e2f.jpg" alt="img" /></p>
|
||
|
||
<p>但事实上,二阶事务提交也存在一些缺陷。</p>
|
||
|
||
<p>第一,在整个流程中,我们会发现各个资源管理器节点存在阻塞,只有当所有的节点都准备完成之后,事务管理器才会发出进行全局事务提交的通知,这个过程如果很长,则会有很多节点长时间占用资源,从而影响整个节点的性能。</p>
|
||
|
||
<p>一旦资源管理器挂了,就会出现一直阻塞等待的情况。类似问题,我们可以通过设置事务超时时间来解决。</p>
|
||
|
||
<p>第二,仍然存在数据不一致的可能性,例如,在最后通知提交全局事务时,由于网络故障,部分节点有可能收不到通知,由于这部分节点没有提交事务,就会导致数据不一致的情况出现。</p>
|
||
|
||
<p><strong>而三阶事务(3PC)的出现就是为了减少此类问题的发生。</strong></p>
|
||
|
||
<p>3PC 把 2PC 的准备阶段分为了准备阶段和预处理阶段,在第一阶段只是询问各个资源节点是否可以执行事务,而在第二阶段,所有的节点反馈可以执行事务,才开始执行事务操作,最后在第三阶段执行提交或回滚操作。并且在事务管理器和资源管理器中都引入了超时机制,如果在第三阶段,资源节点一直无法收到来自资源管理器的提交或回滚请求,它就会在超时之后,继续提交事务。</p>
|
||
|
||
<p>所以 3PC 可以通过超时机制,避免管理器挂掉所造成的长时间阻塞问题,但其实这样还是无法解决在最后提交全局事务时,由于网络故障无法通知到一些节点的问题,特别是回滚通知,这样会导致事务等待超时从而默认提交。</p>
|
||
|
||
<h3>3. 事务补偿机制(TCC)</h3>
|
||
|
||
<p>以上这种基于 XA 规范实现的事务提交,由于阻塞等性能问题,有着比较明显的低性能、低吞吐的特性。所以在抢购活动中使用该事务,很难满足系统的并发性能。</p>
|
||
|
||
<p>除了性能问题,JTA 只能解决同一服务下操作多数据源的分布式事务问题,换到微服务架构下,可能存在同一个事务操作,分别在不同服务上连接数据源,提交数据库操作。</p>
|
||
|
||
<p>而 TCC 正是为了解决以上问题而出现的一种分布式事务解决方案。TCC 采用最终一致性的方式实现了一种柔性分布式事务,与 XA 规范实现的二阶事务不同的是,TCC 的实现是基于服务层实现的一种二阶事务提交。</p>
|
||
|
||
<p>TCC 分为三个阶段,即 Try、Confirm、Cancel 三个阶段。</p>
|
||
|
||
<p><img src="assets/23f68980870465ba6c00c0f2619fcfa9.jpg" alt="img" /></p>
|
||
|
||
<ul>
|
||
|
||
<li>Try 阶段:主要尝试执行业务,执行各个服务中的 Try 方法,主要包括预留操作;</li>
|
||
|
||
<li>Confirm 阶段:确认 Try 中的各个方法执行成功,然后通过 TM 调用各个服务的 Confirm 方法,这个阶段是提交阶段;</li>
|
||
|
||
<li>Cancel 阶段:当在 Try 阶段发现其中一个 Try 方法失败,例如预留资源失败、代码异常等,则会触发 TM 调用各个服务的 Cancel 方法,对全局事务进行回滚,取消执行业务。</li>
|
||
|
||
</ul>
|
||
|
||
<p>以上执行只是保证 Try 阶段执行时成功或失败的提交和回滚操作,你肯定会想到,如果在 Confirm 和 Cancel 阶段出现异常情况,那 TCC 该如何处理呢?此时 TCC 会不停地重试调用失败的 Confirm 或 Cancel 方法,直到成功为止。</p>
|
||
|
||
<p>但 TCC 补偿性事务也有比较明显的缺点,那就是对业务的侵入性非常大。</p>
|
||
|
||
<p>首先,我们需要在业务设计的时候考虑预留资源;然后,我们需要编写大量业务性代码,例如 Try、Confirm、Cancel 方法;最后,我们还需要为每个方法考虑幂等性。这种事务的实现和维护成本非常高,但综合来看,这种实现是目前大家最常用的分布式事务解决方案。</p>
|
||
|
||
<h3>4. 业务无侵入方案——Seata(Fescar)</h3>
|
||
|
||
<p>Seata 是阿里去年开源的一套分布式事务解决方案,开源一年多已经有一万多 star 了,可见受欢迎程度非常之高。</p>
|
||
|
||
<p>Seata 的基础建模和 DTP 模型类似,只不过前者是将事务管理器分得更细了,抽出一个事务协调器(Transaction Coordinator 简称 TC),主要维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。而 TM 则负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。如下图所示:</p>
|
||
|
||
<p><img src="assets/6ac3de014819c54fe6904c938240b183.jpg" alt="img" /></p>
|
||
|
||
<p>按照<a href="https://github.com/seata/seata">Github</a>中的说明介绍,整个事务流程为:</p>
|
||
|
||
<ul>
|
||
|
||
<li>TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;</li>
|
||
|
||
<li>XID 在微服务调用链路的上下文中传播;</li>
|
||
|
||
<li>RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;</li>
|
||
|
||
<li>TM 向 TC 发起针对 XID 的全局提交或回滚决议;</li>
|
||
|
||
<li>TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。</li>
|
||
|
||
</ul>
|
||
|
||
<p>Seata 与其它分布式最大的区别在于,它在第一提交阶段就已经将各个事务操作 commit 了。Seata 认为在一个正常的业务下,各个服务提交事务的大概率是成功的,这种事务提交操作可以节约两个阶段持有锁的时间,从而提高整体的执行效率。</p>
|
||
|
||
<p>那如果在第一阶段就已经提交了事务,那我们还谈何回滚呢?</p>
|
||
|
||
<p>Seata 将 RM 提升到了服务层,通过 JDBC 数据源代理解析 SQL,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。</p>
|
||
|
||
<p>如果 RM 决议要全局回滚,会通知 RM 进行回滚操作,通过 XID 找到对应的回滚日志记录,通过回滚记录生成反向更新 SQL,进行更新回滚操作。</p>
|
||
|
||
<p>以上我们可以保证一个事务的原子性和一致性,但隔离性如何保证呢?</p>
|
||
|
||
<p>Seata 设计通过事务协调器维护的全局写排它锁,来保证事务间的写隔离,而读写隔离级别则默认为未提交读的隔离级别。</p>
|
||
|
||
<h2>总结</h2>
|
||
|
||
<p>在同服务多数据源操作不同数据库的情况下,我们可以使用基于 XA 规范实现的分布式事务,在 Spring 中有成熟的 JTA 框架实现了 XA 规范的二阶事务提交。事实上,二阶事务除了性能方面存在严重的阻塞问题之外,还有可能导致数据不一致,我们应该慎重考虑使用这种二阶事务提交。</p>
|
||
|
||
<p>在跨服务的分布式事务下,我们可以考虑基于 TCC 实现的分布式事务,常用的中间件有 TCC-Transaction。TCC 也是基于二阶事务提交原理实现的,但 TCC 的二阶事务提交是提到了服务层实现。TCC 方式虽然提高了分布式事务的整体性能,但也给业务层带来了非常大的工作量,对应用服务的侵入性非常强,但这是大多数公司目前所采用的分布式事务解决方案。</p>
|
||
|
||
<p>Seata 是一种高效的分布式事务解决方案,设计初衷就是解决分布式带来的性能问题以及侵入性问题。但目前 Seata 的稳定性有待验证,例如,在 TC 通知 RM 开始提交事务后,TC 与 RM 的连接断开了,或者 RM 与数据库的连接断开了,都不能保证事务的一致性。</p>
|
||
|
||
<h2>思考题</h2>
|
||
|
||
<p>Seata 在第一阶段已经提交了事务,那如果在第二阶段发生了异常要回滚到 Before 快照前,别的线程若是更新了数据,且业务走完了,那么恢复的这个快照不就是脏数据了吗?但事实上,Seata 是不会出现这种情况的,你知道它是怎么做到的吗?</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/Java并发编程实战/41 如何设计更优的分布式锁?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Java并发编程实战/43 如何使用缓存优化系统性能?.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":"709971b9fe8b3d60","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>
|
||
|