mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1312 lines
60 KiB
HTML
1312 lines
60 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>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>MVCC(Multiversion 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> create table tab1(id decimal,name varchar(10),address varchar(10),status decimal,primary key(id));
|
||
mysql> 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> update tab1 set status=0 where name='a';
|
||
mysql> 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 -> 要执行 update address</li>
|
||
<li>T2 -> 要执行 update address</li>
|
||
<li>T3 -> 要执行 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> create table t_account(name varchar(10),balance decimal);
|
||
mysql> insert into t_account values('A',100);
|
||
mysql> insert into t_account values('B',0);
|
||
</code></pre>
|
||
<h4>读未提交</h4>
|
||
<p>设置事务隔离级别:</p>
|
||
<pre><code>mysql> set global tx_isolation='read-uncommitted';
|
||
</code></pre>
|
||
<p>查询事务隔离级别:</p>
|
||
<pre><code>mysql> 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> 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> set global tx_isolation='read-committed';
|
||
</code></pre>
|
||
<p>查询事务隔离级别:</p>
|
||
<pre><code>mysql> 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> set global tx_isolation='repeatable-read';
|
||
</code></pre>
|
||
<p>查询事务隔离级别:</p>
|
||
<pre><code>mysql> 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> set global tx_isolation='serializable';
|
||
</code></pre>
|
||
<p>查询事务隔离级别:</p>
|
||
<pre><code>mysql> 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>另外,结合前面说的 MVCC,Read-Committed 和 Repeatable-Read,支持 MVCC;Read-Uncommitted 由于可以读取未提交的数据,不支持 MVCC;Serializable 会对所有读取的数据加行锁,不支持 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>场景 1:insert 死锁</h4>
|
||
<p>创建一个测试表:</p>
|
||
<pre><code>mysql> create table t_insert(id decimal,no decimal,primary key(id),unique key(no));
|
||
</code></pre>
|
||
<p>session1:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_insert values(1,101);
|
||
</code></pre>
|
||
<p>session2:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_insert values(2,101);
|
||
</code></pre>
|
||
<p>此时会话一直等待无响应。</p>
|
||
<p>session1:</p>
|
||
<pre><code>mysql> 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>场景 3:rollback 死锁</h4>
|
||
<p>创建一个测试表:</p>
|
||
<pre><code>mysql> create table t_rollback(id decimal,no decimal,primary key(id),unique key(no));
|
||
</code></pre>
|
||
<p>session1:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_rollback values(1,100);
|
||
</code></pre>
|
||
<p>session2:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_rollback values(2,100);
|
||
</code></pre>
|
||
<p>此时会话一直等待无响应。</p>
|
||
<p>session3</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_rollback values(3,100);
|
||
</code></pre>
|
||
<p>此时会话一直等待无响应。</p>
|
||
<p>session1</p>
|
||
<pre><code>mysql> 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>场景 4:commit 死锁</h4>
|
||
<p>创建一个测试表:</p>
|
||
<pre><code>mysql> create table t_commit(id decimal,no decimal,primary key(id),unique key(no));
|
||
mysql> insert into t_commit values(1,100);
|
||
</code></pre>
|
||
<p>session1:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> delete from t_commit where id=1;
|
||
</code></pre>
|
||
<p>session2:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_commit values(1,100);
|
||
</code></pre>
|
||
<p>此时会话一直等待无响应。</p>
|
||
<p>session3:</p>
|
||
<pre><code>mysql> begin;
|
||
mysql> insert into t_commit values(1,100);
|
||
</code></pre>
|
||
<p>此时会话一直等待无响应。</p>
|
||
<p>session1:</p>
|
||
<pre><code>mysql> 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> select balance from t_account where name='A';
|
||
</code></pre>
|
||
<p>转账 100 元给用户 B:</p>
|
||
<pre><code>mysql> update t_account set balance=balance-100 where name='A';
|
||
mysql> update t_account set balance=balance+100 where name='B';
|
||
</code></pre>
|
||
<p><strong>设置事务保存点</strong></p>
|
||
<pre><code>mysql> savepoint T_A_TO_B;
|
||
</code></pre>
|
||
<p>转账 200 元给用户 C:</p>
|
||
<pre><code>mysql> update t_account set balance=balance-200 where name='A';
|
||
mysql> 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> rollback to T_A_TO_B;
|
||
mysql> commit;
|
||
</code></pre>
|
||
<p>根据提示 0 条受影响的行,也就是说用户 C 不存在呀,我们查询一下个用户信息:</p>
|
||
<pre><code>mysql> select * from t_account where name='A';
|
||
+------+---------+
|
||
| name | balance |
|
||
+------+---------+
|
||
| A | 900 |
|
||
+------+---------+
|
||
1 row in set (0.00 sec)
|
||
mysql> select * from t_account where name='B';
|
||
+------+---------+
|
||
| name | balance |
|
||
+------+---------+
|
||
| B | 200 |
|
||
+------+---------+
|
||
1 row in set (0.00 sec)
|
||
mysql> 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> 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>
|