learn.lianglianglee.com/文章/MySQL 地基基础:事务和锁的面纱.md.html
2022-05-11 19:04:14 +08:00

1312 lines
60 KiB
HTML
Raw Permalink 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>MySQL 地基基础:事务和锁的面纱.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="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.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>MySQL 地基基础:事务和锁的面纱</h1>
<h3>什么是事务,为什么需要事务</h3>
<p>在 MySQL 中,事务是由一条或多条 SQL 组成的单位,在这个单位中所有的 SQL 共存亡,有点有福同享,有难同当的意思。要么全部成功,事务顺利完成;要么只要有一个 SQL 失败就会导致整个事务失败,所有已经做过的操作回退到原始数据状态。</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>在日常生活中有很多的事情就能体现为数据库中的事务。比如“转账”,下面我们就具体展开,你就可以很清晰的认识事务的四个特性。</p>
<ul>
<li>时间2020 年 1 月 1 日</li>
<li>地点:某银行 ATM</li>
<li>人物A 和 B</li>
<li>起因B 向 A 借 1000 元人民币</li>
<li>经过A 转账给 B</li>
<li>结果:转账成功或失败</li>
</ul>
<p>这么一个转账我们想一下底层基本的技术支撑与实现B 向 A 借钱A 要转账给 B首先 A 必须有大于 1000 元的余额,然后从 A 的账户减 1000 元,在 B 的账户里加 1000 元。</p>
<p>我们定义一个事务:</p>
<pre><code>获取 A 账户余额
select balance from account where username='A';
在 A 账户里减 1000 元
update account set balance=balance-1000 where username='A';
获取 B 账户余额
select balance from account where username='B';
在 B 账户里加 1000 元
update account set balance=balance+1000 where username='B';
</code></pre>
<p>好了,一个简单事务基本就这样,我们开始分析分析这个事务是如何保证事务的 ACID 的。</p>
<ul>
<li><strong>原子性</strong>:这个事务要么全成功,要么全失败。事务成功则 1000 元转账到了 B 账户,事务失败回滚则 1000 元还在 A 账户里,就是说 1000 元不能凭空消失。</li>
<li><strong>一致性</strong>:在这个事务中,所有的查询都是一致的,我们先查询 A 账户余额是否大于 1000如果小于 1000事务失败回滚如果获取不到 B 账户余额,事务失败回滚。</li>
<li><strong>隔离性</strong>在这个事务发生的同时发生了另一个事务A 通过手机银行将钱全部转移到另外的账户,比如一共有 1500 元),第一个事务转 1000 元,第二个事务转 1500 元,我们仔细想想,如果都成功,那岂不是凭空获取了 1000 元,这是不合理的,每个事务在执行前都应查一下余额是否够本次转账的。这两个事务应该是隔离的,不能有冲突。</li>
<li><strong>持久性</strong>:转账成功了(即事务完成),这代表钱已经发生了转移,这个时候发生 ATM 吞卡、ATM 断电、手机银行无法登陆等等一切故障,反正钱已经转走了,钱没有丢(即数据没有丢)</li>
</ul>
<h3>MySQL 并发控制技术</h3>
<p>并发控制技术可以说是数据库的底层基础技术,并发控制技术可以拆分来看,一是并发,一是控制。并发也就是说大量请求连接到数据库,控制就是数据库要控制好这些连接,保证资源的可用性、安全性,解决资源的挣用的问题。</p>
<p>那么如何实现并控制呢?主要通过两个方面:</p>
<ul>
<li>Lock</li>
<li>MVCC</li>
</ul>
<p>先分别简单说一下 Lock 和 MVCC具体的后面再聊。</p>
<ul>
<li>Lock并发连接到数据库操作有读和读、读和写、写和写锁来保证并发连接使得数据可以保持一致性。</li>
<li>MVCCMultiversion Concurrency Control多版本并发控制是数据库的多版本可以提高并发过程中的读和写操作有效的避免写请求阻塞读请求。</li>
</ul>
<h3>面试再也不怕被问到的 MVCC</h3>
<p>前面我们已经大致了解了 MVCC 是什么,以及他做什么事情,现在我们具体看看 MVCC 是如何工作的?</p>
<p>我们知道数据的一致性可以通过锁来保证在并发连接中锁机制在读和读的并发请求中不会锁数据但是在读和写的并发请求中写请求会加锁读请求会被写请求阻塞基于此MVCC 发挥其作用。</p>
<p>MVCC 控制两类操作:</p>
<ul>
<li>快照读:读取的是历史可见版本的数据,无锁</li>
<li>当前读:读取的是当前最新版本的数据,加锁</li>
</ul>
<p>我们举个例子说一下吧,比如:</p>
<pre><code>mysql&gt; create table tab1(id decimal,name varchar(10),address varchar(10),status decimal,primary key(id));
mysql&gt; insert into tab1 values(1,'a','beijing',1);
</code></pre>
<p>表中数据为:</p>
<table>
<thead>
<tr>
<th align="left">id</th>
<th align="left">name</th>
<th align="left">address</th>
<th align="left">status</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">1</td>
<td align="left">a</td>
<td align="left">beijing</td>
<td align="left">1</td>
</tr>
</tbody>
</table>
<p>现在有一个请求,将数据 a 的地址改为 shanghai这个数据更新的过程我们细化一下将历史数据置为失效将新的数据插入</p>
<pre><code>mysql&gt; update tab1 set status=0 where name='a';
mysql&gt; insert into tab1 value(2,'a','shanghai',1);
</code></pre>
<p>表中数据为:</p>
<table>
<thead>
<tr>
<th align="left">id</th>
<th align="left">name</th>
<th align="left">address</th>
<th align="left">status</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">1</td>
<td align="left">a</td>
<td align="left">beijing</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">2</td>
<td align="left">a</td>
<td align="left">shanghai</td>
<td align="left">1</td>
</tr>
</tbody>
</table>
<p>MVCC 的原理就类似是这样的,<code>address='beijing'</code> 就是历史数据,更新前保存了下来,<code>address='shanghai'</code> 就是当前数据,新插入数据,这样并发连接来了,既可以读取历史数据,也可以修改当前数据。比如,现在有三个事务:</p>
<ul>
<li>T1 -&gt; 要执行 update address</li>
<li>T2 -&gt; 要执行 update address</li>
<li>T3 -&gt; 要执行 update address</li>
</ul>
<p>T1 先获取了表中这一行数据,执行了 update未提交T2 获取表中这一行数据,由于 T1 未提交address='beijing',这个 beijing 就来源历史数据T3 也获取表中这一行数据,由于 T1 未提交,<code>address='beijing'</code>,这个 beijing 也来源历史数据。这样是不是好理解了。</p>
<p>以此类推,如果只对 <code>name='a'</code> 这一行数据有 N 个并发连接要做 M 个操作,这些历史数据都保存在表中,这个表的数据量无法预估,势必会造成压力与瓶颈。多版本数据到底如何保存,这就不是本节考虑的问题了,是数据库 undo 帮你做的工作。这里就不展开了。(后期可能会做 undo 相关的 chat大家可以关注我</p>
<h3>简单易懂的实例帮你理解 MySQL 事务隔离级别</h3>
<p>事务隔离级别,拆分来看,事务、隔离、级别,故是三个概念的集合,是保证事务之间相互隔离互不影响的,有多个级别。事务在执行过程中可能会出现脏读、不可重复读、幻读,那么 MySQL 的事务隔离级别到底有怎样的表现呢?</p>
<table>
<thead>
<tr>
<th align="left">事务隔离级别</th>
<th align="left">脏读</th>
<th align="left">不可重复读</th>
<th align="left">幻读</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">读未提交(Read-Uncommited)</td>
<td align="left">可能</td>
<td align="left">可能</td>
<td align="left">可能</td>
</tr>
<tr>
<td align="left">读提交(Read-Commited)</td>
<td align="left">不可能</td>
<td align="left">可能</td>
<td align="left">可能</td>
</tr>
<tr>
<td align="left">可重复读交(Repeatable-Read)</td>
<td align="left">不可能</td>
<td align="left">不可能</td>
<td align="left">可能</td>
</tr>
<tr>
<td align="left">序列化(Serializable)</td>
<td align="left">不可能</td>
<td align="left">不可能</td>
<td align="left">不可能</td>
</tr>
</tbody>
</table>
<p>那么到底什么是脏读、不可重复读、幻读呢?</p>
<ul>
<li><strong>脏读</strong>:一个事务读取了另一个未提交事务操作的数据。</li>
<li><strong>不可重复读</strong>:一个事务重新读取前面读取过的数据时,发现该数据已经被修改了或者不见了,其实已被另一个已提交的事务操作了。解决了脏读的问题。</li>
<li><strong>幻读</strong>:一个事务,需要更新数据,于是重新提交了一个查询,返回符合查询条件行,发现这些行因为其他提交的事务发生了改变,这些数据像“幻影”一样出现了。解决了不可重复读。</li>
</ul>
<p>接下来我们用具体实例分析各个事务隔离级别。</p>
<p>创建测试表 t_account</p>
<pre><code>mysql&gt; create table t_account(name varchar(10),balance decimal);
mysql&gt; insert into t_account values('A',100);
mysql&gt; insert into t_account values('B',0);
</code></pre>
<h4>读未提交</h4>
<p>设置事务隔离级别:</p>
<pre><code>mysql&gt; set global tx_isolation='read-uncommitted';
</code></pre>
<p>查询事务隔离级别:</p>
<pre><code>mysql&gt; SELECT @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
</code></pre>
<p><strong>当前事务可以读取另一个未提交事务操作的数据。</strong></p>
<p>环境:用户 A 有 100 元钱,给用户 A 增加 100 元,然后用户 A 转账给用户 B。</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">begin;</td>
<td align="left">begin;</td>
</tr>
<tr>
<td align="left">update t_account set balance=balance+100 where name='A'; #给用户 A 增加 100 元</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select balance from t_account where name='A'; #转账前查询用户 A 余额为 200 元</td>
</tr>
<tr>
<td align="left">rollback; #决定不给用户 A 增加 100 元了,事务回滚</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">update t_account set balance=balance-200 where name='A'; #用户 A 继续给用户 B 转账,用户 A 减 200 元</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">update t_account set balance=balance+200 where name='B'; #用户 A 继续给用户 B 转账,用户加加 200 元</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">commit; #提交事务</td>
</tr>
</tbody>
</table>
<p>现在我们查询一下用户 A 和用户 B 的余额:</p>
<pre><code>mysql&gt; select * from t_account;
+------+---------+
| name | balance |
+------+---------+
| A | -100 |
| B | 200 |
+------+---------+
2 rows in set (0.00 sec)
</code></pre>
<p>问题来了,这个结果不符合预期,用户 A 竟然是 -100 元,用户 B 增加了 200 元,这是因为事务 2 读取了事务 1 未提交的数据。</p>
<h4>读提交</h4>
<p>设置事务隔离级别:</p>
<pre><code>mysql&gt; set global tx_isolation='read-committed';
</code></pre>
<p>查询事务隔离级别:</p>
<pre><code>mysql&gt; SELECT @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-COMMITTED |
+------------------+
1 row in set (0.00 sec)
</code></pre>
<p><strong>当前事务只能读取另一个提交事务操作的数据。</strong></p>
<p>环境:用户 A 有 100 元钱,给用户 A 增加 100 元。</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">begin;</td>
<td align="left">begin;</td>
</tr>
<tr>
<td align="left">update t_account set balance=balance+100 where name='A'; #给用 A 增加 100 元</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account where name='A'; #事务 2 查用户的余额,因事务 1 未提交,仍为 100 元</td>
</tr>
<tr>
<td align="left">commit;</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account where name='A'; #事务 2 查用户的余额,事务 1 已提交,变为 200 元</td>
</tr>
</tbody>
</table>
<p>一个事务重新读取前面读取过的数据时,发现该数据已经被修改了,其实已被另一个已提交的事务操作了。</p>
<h4>可重复读</h4>
<p>设置事务隔离级别:</p>
<pre><code>mysql&gt; set global tx_isolation='repeatable-read';
</code></pre>
<p>查询事务隔离级别:</p>
<pre><code>mysql&gt; SELECT @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| REPEATABLE-READ |
+------------------+
1 row in set (0.00 sec)
</code></pre>
<p><strong>当前事务读取通过第一次读取建立的快照是一致的,即使另外一个事务提交了该数据。除非自己这个事务可以读取在自身事务中修改的数据。</strong></p>
<p>可重复读隔离级别是 MySQL 的默认隔离级别。</p>
<p>环境:用户 A 有 100 元钱,给用户 A 增加 100 元。</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">begin;</td>
<td align="left">begin;</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account where name='A'; #事务 2 查用户的余额,为 100 元</td>
</tr>
<tr>
<td align="left">update t_account set balance=balance+100 where name='A'; #给用 A 增加 100 元</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account where name='A'; #事务 2 查用户的余额,因事务 1 未提交,仍为 100 元</td>
</tr>
<tr>
<td align="left">commit;</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account where name='A'; #事务 2 查用户的余额,事务 1 已提交,仍为 100 元</td>
</tr>
</tbody>
</table>
<p>这就能看出来,事务 2 开启后读取了用户 A 的余额,即使事务 1 修改了数据,不管提交与否,事务 2 读取的数据一直是之前第一次读取的数据。继续操作。</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"></td>
<td align="left">commit;</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account where name='A'; ###事务 2 查用户的余额,为 200 元</td>
</tr>
</tbody>
</table>
<p>为什么现在变成了 200 元了,因为事务 2 已经 commit再次 select 是一个新的事务,读取数据当然又变为第一次获取数据(此时的数据是最新的数据)。</p>
<p>思考一下:上述这个举例是可重复读的 select 相关验证,如果是 DML 操作,会不会是同样的结果呢?</p>
<p>思考三分钟......</p>
<p>答案是其他事物即使查询不到的数据DML 操作也可能会影响那些提交的数据。好,让我验证一下。</p>
<p>update 操作:</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">begin;</td>
<td align="left">begin;</td>
</tr>
<tr>
<td align="left">select * from t_account; #有一行数据,用户 A余额 100 元</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">insert into t_account values('B',100); #增加用户 B余额 100 元</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">commit;</td>
</tr>
<tr>
<td align="left">select * from t_account where name='B'; #无返回行,查询不到用户 B</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">update t_account set balance=balance+100 where name='B'; #神奇,更新成功了</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">select * from t_account; #用户 A 余额 100用户 B 余额 200</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account; #用户 A 余额 100用户 B 余额 100</td>
</tr>
<tr>
<td align="left">commit;</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account; #用户 A 余额 100用户 B 余额 200</td>
</tr>
</tbody>
</table>
<p>delete 操作:</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">begin;</td>
<td align="left">begin;</td>
</tr>
<tr>
<td align="left">select * from t_account; #有 2 行数据,用户 A 余额 100 元,用户 B 余额 200</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">insert into t_account values('C',100); #增加用户 C余额 100 元</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">commit;</td>
</tr>
<tr>
<td align="left">select * from t_account where name='C'; #无返回行,查询不到用户 C</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">delete from t_account where name='C'; #神奇,删除成功了</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">select * from t_account; #用户 A 余额 100用户 B 余额 200</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account; #用户 A 余额 100用户 B 余额 200用户 C 余额 100</td>
</tr>
<tr>
<td align="left">commit;</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">select * from t_account; #户 A 余额 100用户 B 余额 200</td>
</tr>
</tbody>
</table>
<p>通过这两个例子你是不是了解了一个事务的 update 和 delete 操作了另外一个事务提交的数据,会使得这些数据在当前事务变得可见。就像幻影一下出现了!</p>
<h4>序列化</h4>
<p>设置事务隔离级别:</p>
<pre><code>mysql&gt; set global tx_isolation='serializable';
</code></pre>
<p>查询事务隔离级别:</p>
<pre><code>mysql&gt; SELECT @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| SERIALIZABLE |
+------------------+
1 row in set (0.00 sec)
</code></pre>
<p><strong>当前事务 select 和 DML 操作的数据都会加行锁,其他事务访问同样的数据需要等锁释放。</strong></p>
<p>环境:用户 A 有 100 元钱,给用户 A 增加 100 元。</p>
<table>
<thead>
<tr>
<th align="left">事务 1</th>
<th align="left">事务 2</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">begin;</td>
<td align="left">begin;</td>
</tr>
<tr>
<td align="left">select * from t_account where name='A'; #查询用户余额</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">update t_account set balance=balance+100 where name='A'; #给用户 A 增加 100 元,执行一直处于等待</td>
</tr>
<tr>
<td align="left">commit;</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">update 成功返回</td>
</tr>
<tr>
<td align="left">select * from t_account where name='A'; #用户 A 余额为 100因为事务 2 还未提交,获取的是 undo 中的历史版本数据</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">begin;</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">select * from t_account where name='A'; #新开一个事务,由于事务 2 还未提交,此查询锁等</td>
<td align="left"></td>
</tr>
<tr>
<td align="left"></td>
<td align="left">commit;</td>
</tr>
<tr>
<td align="left">select * from t_account where name='A'; #用户 A 余额 200</td>
<td align="left"></td>
</tr>
</tbody>
</table>
<p>好了,实例讲解到此结束,是否已经帮你理解了 MySQL 事务隔离级别。</p>
<p>另外,结合前面说的 MVCCRead-Committed 和 Repeatable-Read支持 MVCCRead-Uncommitted 由于可以读取未提交的数据,不支持 MVCCSerializable 会对所有读取的数据加行锁,不支持 MVCC。</p>
<h3>MySQL 锁机制(机智)</h3>
<p>锁是可以协调并发连接访问 MySQL 数据库资源的一种技术可以保证数据的一致性。锁有两个阶段加锁和解锁InnoDB 引擎的锁主要有两类。</p>
<p><strong>共享锁S</strong></p>
<p>允许一个事务读取数据,阻塞其他事务想要获取相同数据。共享锁之间不互斥,读和读操作可以并行。代码展示:</p>
<pre><code>select * from table where ... lock in share mode
</code></pre>
<p><strong>排它锁X</strong></p>
<p>持有排他锁的事务可以更新数据,阻塞其他事务获取数据的排他锁和共享锁。排它锁之间互斥,读和写、写和写操作不可以并行。代码展示:</p>
<pre><code>select * from table where ... for update;
</code></pre>
<p>从 MySQL 数据库的内外区分锁,有两种锁。</p>
<p><strong>内部锁</strong></p>
<p>MySQL 在数据库内部自动管理,协调并发连接的资源争用。内部锁再具体来看分为:</p>
<ul>
<li>行锁:会话事务将访问的行数据加锁</li>
<li>表锁:会话事务将访问的表整体加锁</li>
</ul>
<p><strong>外部锁</strong></p>
<p>会话层使用特殊的手段显示获取锁,阻塞其他会话对数据的操作。我们通过外部操作命令实现外部锁,比如使用 lock table 和 unlock tables。</p>
<p>我们举个例子来描述一下这个过程吧,比如有事务 1 和事务 2事务 1 锁定了一行数据,加了一个 S 锁;事务 2 想要对整个表加锁,需要判断这个表是否被加了表锁,表中的每一行是否有行锁。仔细想想这个过程是很快呢?还是非常的慢?如果表很小无所谓了,如果表是海量级数据,那糟了,事务 2 势必耗费很多资源。</p>
<p>如何解决事务 2 这种检索资源消耗的问题呢?事务意向锁帮你先获取意向,先一步问问情况,然后再获取我们想要的 S 和 X 锁,具体分为:</p>
<p><strong>意向共享锁IS</strong></p>
<p>事务 1 说:我要加一个行锁,我有这个意向,你们其他人有没有意见,如果没有我就先拿这个 IS 锁了。</p>
<p><strong>意向排它锁IX</strong></p>
<p>事务 2 说:我要加一个表锁,这个可是排他锁,我拿了你们就等我用完再说吧,我有这个意向,你们其他人有没有意见,如果没有我就先拿这个 IX 锁了。</p>
<p>前面这个举例,其过程升级优化为:</p>
<ul>
<li>事务 1 先申请获取 IS 锁,成功后,获取 S 锁</li>
<li>事务 2 发现表中有 IS 锁了,事务 2 获取表锁会被阻塞</li>
</ul>
<p>那么这四个锁之间兼容性如何呢?</p>
<table>
<thead>
<tr>
<th align="left"></th>
<th align="left">X</th>
<th align="left">S</th>
<th align="left">IX</th>
<th align="left">IS</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">X</td>
<td align="left">冲突</td>
<td align="left">冲突</td>
<td align="left">冲突</td>
<td align="left">冲突</td>
</tr>
<tr>
<td align="left">S</td>
<td align="left">冲突</td>
<td align="left">兼容</td>
<td align="left">冲突</td>
<td align="left">兼容</td>
</tr>
<tr>
<td align="left">IX</td>
<td align="left">冲突</td>
<td align="left">冲突</td>
<td align="left">兼容</td>
<td align="left">兼容</td>
</tr>
<tr>
<td align="left">IS</td>
<td align="left">冲突</td>
<td align="left">兼容</td>
<td align="left">兼容</td>
<td align="left">兼容</td>
</tr>
</tbody>
</table>
<h3>聊几个经典死锁案例</h3>
<p>在实际应用中经常发生数据库死锁的情况,那么什么是死锁呢?说白了就是事务 1 锁事务 2事务 2 锁事务 1这两个事务都在等着对方释放锁资源陷入了死循环。</p>
<p>接下来我们介绍几个经典死锁案例MySQL 默认级别使用的是 REPEATABLE-READ。</p>
<h4>场景 1insert 死锁</h4>
<p>创建一个测试表:</p>
<pre><code>mysql&gt; create table t_insert(id decimal,no decimal,primary key(id),unique key(no));
</code></pre>
<p>session1</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_insert values(1,101);
</code></pre>
<p>session2</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_insert values(2,101);
</code></pre>
<p>此时会话一直等待无响应。</p>
<p>session1</p>
<pre><code>mysql&gt; insert into t_insert values(3,100);
</code></pre>
<p>结果如下。</p>
<p>此时 session2 立马报出来死锁:</p>
<pre><code>ERROR 1213 (40001): ==Deadlock== found when trying to get lock; try restarting transaction
</code></pre>
<p>数据库中 insert 作为最简单的 SQL为什么会导致死锁呢</p>
<p>session1 在插入(1,101) 的时候会加一个 X 锁session2 插入(2,101)no 字段有着唯一性,故 session2 在插入时数据库会做 duplicate 冲突检测,由于事务冲突先加 S 锁;然后 session1 又插入了 (3,100),此时 session1 会加 insert intention X 锁(插入意向锁),之前 session1 已经有了 X 锁,故进入等待队列,结局就是 session1 和 session2 都在等待陷入了僵局MySQL 很机智,牺牲一方事务解决这个尴尬的局面,所以 session2 被干掉了,报错死锁。</p>
<h4>场景 2自增列死锁</h4>
<p>自增列死锁问题和场景 1 的类似,比如将场景 1 的主键属性改为自增长属性,主键自增仍唯一,场景模拟类似,加锁的过程也类似,产生死锁的过程也类似,这里就不详细模拟了。</p>
<h4>场景 3rollback 死锁</h4>
<p>创建一个测试表:</p>
<pre><code>mysql&gt; create table t_rollback(id decimal,no decimal,primary key(id),unique key(no));
</code></pre>
<p>session1</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_rollback values(1,100);
</code></pre>
<p>session2</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_rollback values(2,100);
</code></pre>
<p>此时会话一直等待无响应。</p>
<p>session3</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_rollback values(3,100);
</code></pre>
<p>此时会话一直等待无响应。</p>
<p>session1</p>
<pre><code>mysql&gt; rollback;
</code></pre>
<p>结果如下: 此时 session1 执行了 rollback 成功返回session2 的 insert 返回成功session3 立马报出来死锁。</p>
<pre><code>ERROR 1213 (40001): ==Deadlock== found when trying to get lock; try restarting transaction
</code></pre>
<p>为什么我回滚了事务,还要报死锁,难道我需要全部回滚吗?</p>
<p>session1 在插入 (1,100) 的时候会加一个 X 锁session2 插入 (2,100)no 字段有着唯一性,故 session2 在插入时数据库会做 duplicate 冲突检测,由于事务冲突先加 S 锁session3 插入 (3,100)no 字段有着唯一性,故 session3 在插入时数据库会做 duplicate 冲突检测,由于事务冲突先加 S 锁session1 回滚session2 申请 insert intention X 锁,等 session3;session3 申请 insert intention X 锁,等 session2结局就是 session2 和 session3 都在等待陷入了僵局MySQL 很机智,牺牲一方事务解决这个尴尬的局面,所以 session3 被干掉了,报错死锁。</p>
<h4>场景 4commit 死锁</h4>
<p>创建一个测试表:</p>
<pre><code>mysql&gt; create table t_commit(id decimal,no decimal,primary key(id),unique key(no));
mysql&gt; insert into t_commit values(1,100);
</code></pre>
<p>session1</p>
<pre><code>mysql&gt; begin;
mysql&gt; delete from t_commit where id=1;
</code></pre>
<p>session2</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_commit values(1,100);
</code></pre>
<p>此时会话一直等待无响应。</p>
<p>session3</p>
<pre><code>mysql&gt; begin;
mysql&gt; insert into t_commit values(1,100);
</code></pre>
<p>此时会话一直等待无响应。</p>
<p>session1</p>
<pre><code>mysql&gt; commit;
</code></pre>
<p>结果如下:此时 session1 执行了 commit 成功返回session3 的 insert 返回成功session2 立马报出来死锁。</p>
<pre><code>ERROR 1213 (40001): ==Deadlock== found when trying to get lock; try restarting transaction
</code></pre>
<p>为什么我提交了事务,还要报死锁,难道我需要全部提交吗?</p>
<p>这个产生死锁的过程和场景 3rollback 死锁类似,大家可以和之前的 rollback 死锁产生过程对应来看。</p>
<h3>小技巧——事务保存点帮你读档重闯关</h3>
<p>玩游戏你是不是有过存档、读档的经历过某一个比较难的关卡先存档过不了就读档重新过。数据库中我们也可以如此MySQL 事务保存点可以回滚到事务的某时间点,并且不用中止事务。下面举例说明一下。</p>
<p>用户 B 和用户 C 向用户 A 借钱,用户 A 转账给用户 B 和用户 C转账的过程中发生了用户 C 账户不存在,那么我们也要把转给用户 B 的钱也取消吗?我们可以不取消,使用一个保存点即可。</p>
<p>查询用户 A 有 1000 元:</p>
<pre><code>mysql&gt; select balance from t_account where name='A';
</code></pre>
<p>转账 100 元给用户 B</p>
<pre><code>mysql&gt; update t_account set balance=balance-100 where name='A';
mysql&gt; update t_account set balance=balance+100 where name='B';
</code></pre>
<p><strong>设置事务保存点</strong></p>
<pre><code>mysql&gt; savepoint T_A_TO_B;
</code></pre>
<p>转账 200 元给用户 C</p>
<pre><code>mysql&gt; update t_account set balance=balance-200 where name='A';
mysql&gt; update t_account set balance=balance+200 where name='C';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
</code></pre>
<p>发现转账给 C 返回有 0 条受影响的行,转账给 C 未成功,此时用户 A 已经少了 200 元了,先退 200 元再排查吧,转账给用户 B 的不需要重新操作了。</p>
<pre><code>mysql&gt; rollback to T_A_TO_B;
mysql&gt; commit
</code></pre>
<p>根据提示 0 条受影响的行,也就是说用户 C 不存在呀,我们查询一下个用户信息:</p>
<pre><code>mysql&gt; select * from t_account where name='A';
+------+---------+
| name | balance |
+------+---------+
| A | 900 |
+------+---------+
1 row in set (0.00 sec)
mysql&gt; select * from t_account where name='B';
+------+---------+
| name | balance |
+------+---------+
| B | 200 |
+------+---------+
1 row in set (0.00 sec)
mysql&gt; select * from t_account where name='C';
Empty set (0.00 sec)
</code></pre>
<p>结果:用户 A 成功转 100 元给用户 B用户 C 果然不存才,设置了保存点,帮我们省了很多工作,中途不用取消全部操作。</p>
<h3>小技巧——一个死锁的具体分析方法</h3>
<p>前面我们学习了事务、锁,以及介绍了几个经典死锁案例,当遇到死锁,我们怎样具体分析呢?</p>
<p>分析死锁,我们就需要看死锁的日志信息,通过日志具体找到死锁的原因及执行的语句。</p>
<p>首先,我们用前面的场景 1 模拟一个死锁。</p>
<p>然后,执行如下命令获取死锁信息:</p>
<pre><code>mysql&gt; show engine innodb status;
</code></pre>
<p>在打印的日志中,先看事务 1 的日志:</p>
<pre><code>*** (1) TRANSACTION:
TRANSACTION 2179, ACTIVE 8 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 32, OS thread handle 140317789804288, query id 823 localhost root update
insert into t_insert values(2,101)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 37 page no 4 n bits 72 index no of table `test`.`t_insert` trx id 2179 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 5; hex 8000000065; asc e;;
1: len 5; hex 8000000001; asc ;;
TRANSACTION 2179, ACTIVE ==8 sec== inserting
</code></pre>
<p>事务 1 持续了 8 秒:</p>
<pre><code>mysql ==tables in use 1==, locked 1 涉及一张表
LOCK WAIT 2 lock struct(s) 有两个锁
insert into t_insert values(2,101) 这是 SQL 语句
WAITING FOR THIS LOCK TO BE GRANTED 唯一行锁处于等待
RECORD LOCKS space id 37 page no 4 n bits 72 index no 加锁的是索引字段 no
lock mode S waiting 锁等待为 S 锁
</code></pre>
<p>事务 2 的日志:</p>
<pre><code>*** (2) TRANSACTION:
TRANSACTION 2178, ACTIVE 17 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 33, OS thread handle 140317663659776, query id 824 localhost root update
insert into t_insert values(3,100)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 37 page no 4 n bits 72 index no of table `test`.`t_insert` trx id 2178 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 5; hex 8000000065; asc e;;
1: len 5; hex 8000000001; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 37 page no 4 n bits 72 index no of table `test`.`t_insert` trx id 2178 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 5; hex 8000000065; asc e;;
1: len 5; hex 8000000001; asc ;;
</code></pre>
<ul>
<li><code>HOLDS THE LOCK(S)</code> 持有锁的内容</li>
<li><code>lock_mode X locks</code> 持有锁的锁等待内容是一个 x 锁</li>
<li><code>WAITING FOR THIS LOCK TO BE GRANTED</code> 等待锁的内容</li>
<li><code>lock_mode X locks gap before rec insert intention waiting</code> 等待锁的锁等待内容也是一个 x 锁</li>
</ul>
<p>通过这些日志,我们发现日志中的事务 1持有 S 锁S 锁的出现是因为需要检查数据唯一性,我们的 no 字段确实有唯一索引,这一点也正好验证了。日志中的事务 1持有一个 X 锁,又等待一个 X 锁。所以场景 1 中的两个事务都在锁等,造成了死锁。</p>
<h3>小技巧——换种思路提高事务能力</h3>
<p>在数据中如果是单一事务,那没的说,一个一个的事务来执行,毫无压力。现实是不允许这样的,肯定是有大量的并发连接,并发事务在所难免。如果高并发的环境中,事务处理效率肯定大幅下降,这个时候我们有没有方法提高并发事务能力呢?</p>
<p>我们解决技术处理问题的限制,这次我们换一种思路来提高事务能力。比如:</p>
<p><strong>合理的在线、离线数据库</strong></p>
<p>比如我们的系统数据量日益增加,还有一些业务需要查询大量的数据,我们可以改造系统为在线、离线数据库,在线表提供高效事务能力,离线表提供数据查询服务,互不影响。</p>
<p><strong>提高 delete 操作效率的思考</strong></p>
<p>如果你对表有大量数据的 delete 操作,比如定期的按日、月、年删除数据,可以设计表为日表、月表、年表亦或是相对应的分区表,这样清理数据会由大事务降低为小事务。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/MySQL 地基基础:数据字典.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":"70997ff15b218b66","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>