技术

学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go 内存管理 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes垂直扩缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Prometheus 学习 容器日志采集 Kubernetes 控制器模型 Kubernetes监控 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

学习rpc 可观察性 生命周期管理 openkruise学习 BFF 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 kubebuilder 及controller-runtime学习 pv与pvc实现 csi学习 client-go学习 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 《推荐系统36式》笔记 资源调度泛谈 系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 《聊聊架构》 书评的笔记 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

招聘Java

招聘Java开发

标签


mysql 事务

2021年01月22日

前言

为什么要有事务隔离级别?

浅谈对数据库隔离级别的理解”隔离性”实质上就是指对数据操作的并发控制。

为什么需要并发控制?它解决了什么问题?在数据库中,如果对于同一数据项的所有事务操作都被串行化地执行,那么执行过程与结果是没有问题的。如果存在并发操作,即多个事务的生命周期(时间区间)之间存在交集,就可能产生操作上的冲突和依赖,进而引发异常现象。操作顺序类型可以分为 读-读、读-写、写-读、写-写 4种类型,其中后三种包含写的操作序列是有冲突的,它们可能会引发执行结果的异常(其实就是与串行执行结果不一样),或者叫异常现象(Anomaly),比如脏读等。PS:因为数据不是写入就算,而是commit 才算数,所以对另一个事务来说,“眼见不一定为实”,看见X=10,结果可能因为事务执行失败X后来不是10了。

并发控制就是要解决这些异常,但是并发控制需要达到什么程度呢?因为并发控制程度高对应着并发执行效率低,数据库用户并非时刻都需要最强的并发控制方式,这是一致性与并发度的权衡,隔离级别就是这一权衡的控制参数。从数据库开发者的角度来看,需要解决并发操作时的异常现象是根本需求,而隔离级别是把这些无穷多的现象级需求进行了分类、抽象、归纳,将规避异常现象之需求转化为了满足有限种类隔离级别之需求。即数据库开发者只需要实现定义好的几种隔离级别,就可以为数据库系统提供规避某些(可能是无穷多)异常现象的功能。如果一个事务系统在运行时能够规避某些问题集,那么该系统的事务将具有某种相应的隔离级别,即隔离级别抽象出待规避的最小异常现象集合。至于DBMS的某种隔离级别的实现,是否还可能规避对应级别的最小问题集以外的异常序列,并不做限制。

如何标准化、形式化地描述并发读写的(异常)现象?有论文“A Critique of ANSI SQL Isolation Levels”

InnoDB中隔离性实现——MVCC与锁

为了控制事务并发时的数据安全,在不同隔离级别下会通过不同的协同机制进行处理。传统隔离机制,完全由锁(LBCC)来处理,但是这样只能满足读读并发,会对性能造成很大影响,故而出现了支持读写并发的MVCC。

隔离组 = 锁 + MVCC 就好比 docker = cgroup + namespace 一样,mysql 揉和一系列复杂底层策略,对外提供几个不同级别的隔离性/并发控制强度保证。类似于分布式里面的线性一致性、最终一致性、因果一致性等。

事务和锁的关系

一文了解数据库隔离级别和锁的联系db 锁并不直接对 开发暴露,锁用于支持实现不同的事务的隔离性强度(有讨论价值的主要是RR和RC),加锁情况太多,容易晕。举个例子感受下事务和锁的关系

  1. 读未提交 (Read Uncommitted)。在事务开始的时候可以给要准备写操作的这一行数据加一个排它锁,如果是读操作,就给该行数据一个读锁。这样之后,在修改该行数据的时候,不让其他进程对该行数据有任何操作。而读该行数据的时候,其他进程不能更改,但可以读。读或写完成时,释放锁,最后commit提交。 这时候读写就分离开了,写和写也就分离开了。
  2. 读已提交 (Read Committed)。在一个进程的事务当中,我更改了其中的一行数据,但是我修改完之后就释放了锁,这时候另一个进程读取了该数据,此时先前的事务是还未提交的,直到我回滚了数据,另一个进程读的数据就变成了无用的或者是错误的数据。so,把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作。
  3. “事务提交前,其他进程是无法对该行数据进行读取的”未免有点重,mysql为了提高系统的并发量,在事务未提交前,虽然事务内操作的数据是锁定状态,但是另一个事务仍然可以读取数据的 快照版本(这就用到了mvcc)。
  4. 可重复读 (Repeatable-Read)。mysql虽然锁住了正在操作的数据行,但它仍然不会阻止另一个事务往表插入新行新的数据。如果一个事务的操作跟所有行有关系,则会出现幻读。so,在第一个事务中进行更新数据时(当前读),会使用 行锁 + 间隙锁 将表锁住,在第二个事务中插入操作被阻塞。PS:只有跟所有行有关系的sql 才会出现幻读,比如select count(*) from xx
  5. 序列化(serializable),该隔离级别会自动在锁住你要操作的整个表的数据,如果另一个进程事务想要操作表里的任何数据就需要等待获得锁的进程操作完成释放锁。

锁表/锁行/操作完释放锁/事务提交释放锁 etc

MVCC 为数据提供多个副本

《MySQL实战45讲》一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢?

  1. begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。
  2. 在 MySQL 里,有两个“视图”的概念
    1. 一个是 view。它是一个用查询语句定义的虚拟表,创建视图的语法是 create view … ,而它的查询方法与表一样。
    2. 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。
  3. 在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。如果一个库有 100G,那么我启动一个事务,MySQL 就要拷贝 100G 的数据出来,这个过程得多慢啊。实际上,我们并不需要拷贝出这 100G 的数据。
  4. InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id(trx_id 即操作 row 的事务id)。一行记录的 多个版本并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的

可重复读——可以读到什么数据

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

数据版本的可见性规则,就是基于数据的 row trx_id 和这个高低水位/事务视图(每个事务的高低水位都不同)对比结果得到的。对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据对当前事务是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是当前事务肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况:a 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,对当前事务不可见; b 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,对当前事务可见。

有了这个声明后,系统里面随后发生的更新,是不是就跟这个事务看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 2 或者 3(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了。InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力(PS:有点copy on write的feel)。一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  1. 版本未提交,不可见;
  2. 版本已提交,但是是在视图创建后提交的,不可见;
  3. 版本已提交,而且是在视图创建前提交的,可见。

可重复读——更新逻辑

当事务要去更新数据的时候,就不能再在历史版本上更新了,否则其它事务的更新就丢失了。update 更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read):必须要读最新版本,而且必须加锁。当前读的规则,就是要能读到所有已经提交的记录的最新值。

如果当前记录的行锁被其他事务占用的话,就需要进入锁等待。根据两阶段锁协议,其他事务提交才会释放锁,因此可以读到其他事务提交后的row。

除了 update 语句外,select 语句如果加锁,也是当前读

# 加读锁
mysql> select k from t where id=1 lock in share mode;
# 加写锁
mysql> select k from t where id=1 for update;

mvcc 实现

postgresql、mysql、tidb对 mvcc 有着不同的实现方案。

TiKV 事务模型概览,Google Spanner 开源实现MVCC层暴露给上层的接口行为定义:

MVCCGet(key, version), 返回某 key 小于等于 version 的最大版本的值
MVCCScan(startKey, endKey, limit, version), 返回 [startKey, endKey) 区间内的 key 小于等于 version 的最大版本的键和值,上限 limit 个
MVCCPut(key, value, version) 插入某个键值对,如果 version 已经存在,则覆盖它。上层事务系统有责任维护自增version来避免read-modify-write
MVCCDelete(key, version) 删除某个特定版本的键值对, 这个需要与上层的事务删除接口区分,只有 GC 模块可以调用这个接口

读提交

读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  1. 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图(高低水位),之后事务里的其他查询都共用这个一致性视图;对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  2. 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图(高低水位)。对于读提交,查询只承认在语句启动前就已经提交完成的数据;