[TOC]
# 丢失数据
旺财是数据库村的一个程序, 小强也是。
数据库村有个特点, 很多数据支持共享操作,多个程序可以同时读写,他们俩经常会为了读写同一个数据, 争夺的不可开交。
这一天,当旺财和小强对同一个银行账户A进行写操作时候, 出现了这么一个错误:
![](https://box.kancloud.cn/edb3a66651cb56d088833717f9385e75_233x292.png)
看看, 本来旺财要加上的20元就丢掉了。
同样的事情发生的多了, 他俩给这种情况起了一个名字,叫“丢失修改”, 其实说白了就是俩人都去写一个数据, 一个人的数据把另外一个给覆盖了。
村里的MySQL老头儿说: “你们两个小家伙,写数据的时候连加锁都不做,肯定会出大乱子!"
旺财说:“加什么锁?”
“来来来, 我教你们一个排他锁(Exclusive Lock) , 简称X锁, 旺财你要写数据了, 就把它用X锁锁住, 锁住后,除非你释放, 否则小强无法获得X锁。 这不就解决你们的问题了? ”
小强想了想, 就把上面的操作过程用X锁改了一下:
![](https://box.kancloud.cn/62df3ac1828fac5810adc50b5410c972_236x453.png)
旺财说:“果然不错, 确实可以解决两个人同时修改导致的问题。”
# 脏数据
小强说:“旺财, 我们约定,写数据的时候都用X锁吧?”
旺财说: “这没问题, 可是X锁只在写数据的时候用, 我们读数据是不用加锁的, 我想起了一种情况, 你看看怎么办?”
![](https://box.kancloud.cn/ac0a8fa1f09ca1ae089b9c1ecc6e230c_272x399.png)
小强在旺财执行的途中读了A的值, 但是旺财把对A的修改给回滚(Rollback)了, 这下小强尴尬了, 他读到了脏数据。
“要不我们在读取数据的时候也加个X锁 ? ” 小强说。
“那样太严格了, 就是读一个数据啊, 值得吗?”
“这样吧, 我们再搞一个新的锁出来, 专门用于共享数据的读取, 就叫共享锁(Share lock) ,简称S锁, 这个锁和之前的排他锁X锁有区别, 主要用于读取数据, 如果一个数据加了X锁, 就没法加S锁, 同样加了S锁, 就没法加X锁” 小强想出了一个点子。
“那如果我加了S锁, 你还能加S锁吗? ” 旺财问。
“应该可以吧, 咱们俩都是读数据, 互不影响啊。 还有为了防止长时间的锁住, 我们可以约定一下,不管我们要做的事情有多少, 读一个数据之前加S锁, 读完之后立刻释放该S锁 ! ”
![](https://box.kancloud.cn/03aa64e5d1299878da52bc8ae141cb86_264x475.png)
果然,这样一来“脏数据”的问题就解决了 !
# 没法重复读?
旺财和小强两个程序相安无事了很久, 但是S锁在读完数据后立刻释放的约定, 导致出了一个新问题。
旺财在一次数据处理中, 先读取了A和B的值, 相加得到了150 , 然后小强把B改成了30
旺财再次读取A和B, 发现求和以后是130 , 刚才的不一样了!
假定旺财的处理是在一个事务当中
![](https://box.kancloud.cn/3f2888a815eb695a0483920555ad3266_266x730.png)
旺财说: “小强, 我在读取数据的时候你不能改啊 , 要不然我这里会出现不一致, 你看刚开始是A+B是 150, 现在变成130了”
小强说: “我们之前的约定是读数据时加S锁, 读完立马释放, 问题就出现在这里了。”
“看来在读数据的时候, 也需要一直锁定了, 直到事务提交。”
![](https://box.kancloud.cn/48972d2f8d818a704862997a79159d6d_265x694.png)
# 幻觉出现
旺财和小强现在已经能灵活的使用X锁和S锁了。
他们俩总结了一下, 分为了这么几种情况:
1. 写数据时加上X锁,直到事务结束, 读的时候不加锁。
虽然能够避免丢失数据, 但是可以读到没有提交或者回滚的内容 (脏数据), 这其实就是数据库最低的事务隔离级别 --- Read uncommitted
2. 写数据的时候加上X锁, 直到事务结束, 读的时候加上S锁, 读完数据立刻释放。
这能避免“丢失数据”和“脏数据”, 但是会出现“不可重复读”的问题 , 这是第二级的事务隔离级别 -- Read committed
3. 写数据的时候加上X锁, 直到事务结束, 读数据的时候加S锁, 也是直到事务结束。
这能避免“丢失数据”和“脏数据”, “不可重复读”三个问题 , 这是数据库常用的隔离级别 --
Repeatable read
整个世界似乎清净了。
有一次旺财对一个“学生表”进行操作,选取了年龄是18岁的所有行, 用X锁锁住, 并且做了修改。
改完以后旺财再次选择所有年龄是18岁的行, 想做一个确认, 没想到有一行竟然没有修改!
这是怎么回事? 出了幻觉吗?
原来就在旺财查询并修改的的时候, 小强也对学生表进行操作, 他插入了一个新的行,其中的年龄也是18岁! 虽然两个人的修改都没有问题, 互不影响, 但从最终效果看, 还是出了事。
正是小强的操作, 让旺财出现了“幻读”
旺财说: “没辙了, 我们俩非得串行执行不可, 你必须得等我执行完。 ”
这就是数据库事务隔离级别的终极大招:**Serializable (串行化)**
最后, 为了方便记忆, 他们俩倒腾了半天, 整出了一张表, 用于记录各种情况:
![](https://box.kancloud.cn/e0119e2d110665f0febcda74c3068a17_443x93.png)
两个人看着这张表, 感慨的说:“唉, 这数据库村的事务隔离级别可真是不容易啊!”
# MVCC
旺财和小强使用了一段时间的“串行化”隔离级别,虽然不会出错,但是效率实在太低了。数据库村的人都笑话他俩干活太慢, 于是俩人商量着退到“可重复读”,虽然会出现幻读,但是也能忍受。
“可重复读”用了一段时间,他们又不满意了。
旺财唉声叹气地说:“为了实现可重复读, 我们需要在事务中对读操作加锁,并且得持续到整个事务结束,这实在是不爽啊!”
小强说:“是啊,我修改数据的时候,还得等待你读完成,效率就太低喽。”
许久不见的MySQL听到他俩的抱怨,插嘴道:“看来你们两个已经开始思考了啊,我有一个办法, 可以在读的时候不用加锁,也能实现可重复读。”
“你就吹吧!这怎么可能?” 旺财和小强根本不相信。
MySQL老头儿说: “你们两个太孤陋寡闻了,这个方法叫做MVCC(多版本并发控制)。”
顿了一下, MySQL老头儿故意激他们:“可是有点难啊,你们俩不一定能弄明白。”
旺财和小强很不服气:“说来听听!”
“假设啊,数据库中有一个叫做users的表,里边有这么一行数据:” MySQL老头儿开始画图:
![](https://box.kancloud.cn/99adf3e518756d9af7bcdbb6d4b234a0_194x99.png)
“现在,我要给他加两个隐藏的字段:”
![](https://box.kancloud.cn/a5ab275e581faae295ff7c8f6d0bad50_386x100.png)
“事务ID? 是不是每次开始事务的时候分配的? ”
“没错,这个事务ID就表明这一行数据是哪个事务操作的,注意啊,事务ID是一个递增的数字,每次开始新事务,这个数字就会增加。”
“这有什么用?”
“别急,马上就会讲到,” MySQL老头儿地说:“ 旺财,小强,假设你们俩的事务中SQL的执行次序如下: ”
![](https://box.kancloud.cn/370f814d1bedb9494d8139581fdf8ad0_467x640.png)
在标号为 (1) 的地方,数据是这样的:
![](https://box.kancloud.cn/6b0d937067543190ac7c4858547440ab_387x100.png)
与此同时,需要建立一个叫做Read View的数据结构,它有三个部分:
(1) 当前活跃的事务列表 ,即`[101,102]`
(2) Tmin ,就是活跃事务的最小值, 在这里 Tmin = 101
(3) Tmax, 是系统中最大事务ID(不管事务是否提交)加上1。 在这里例子中,Tmax = 103
(注: 在可重复读的隔离级别下,当第一个Read操作发生的时候,Read view就会建立。 在Read Committed隔离级别下,每次发出Read操作,都会建立新的Read view。)
旺财和小强还不知道有什么用处,只是死记硬背,生怕跟不上老头儿的思路。
MySQL老头儿接着说道: “在标号为(2)的地方,小强做了修改,数据是这样的:”
![](https://box.kancloud.cn/91af08342b766ec72029131bd2d2f78e_499x249.png)
“看到回滚指针没有? 它指向了上一条记录。”
“怪不得叫做多版本并发控制,你这里维护了数据的多个版本啊。” 小强感慨道。
“按照可重复读的要求,我开始了一个事务,无论我读多少次,我总是能读到age=20的那行记录,即使小强修改了age,我也不受影响。你这个结构该怎么实现啊? ” 旺财问道。
“关键部分要到了,我这里有个算法,用来判断这些数据版本记录中哪些对你来说是可见的(可读的)。 ”
![](https://box.kancloud.cn/91b3eb97fbf017d23b3646e4261b3e0f_546x772.png)
旺财只觉得觉得自己的头嗡地一下就大了:“这....怎么这么麻烦!”
MySQL老头说:“这还麻烦? 已经很简单的算法了,就是几个if else ,加上几个循环而已! 连这个都整不明白,别在我们数据库村混了! 对于上面的例子,ReadView 中事务列表是`[101,102]`, Tmin= 101, Tmax = 103,你们想想,第一次读和第二次读是什么样子。”
只听到小强嘴里嘟囔着:“ 当旺财第一次读的时候,只有一条记录, tid = 100 ,小于Tmin,所以是可以读的。 然后我做了修改, 当旺财第二次读的时候,tid=102,程序走到了‘tid是否在Read View中这一分支,由于102确实在Read View的活动事务列表中,那就顺着回滚指针找到下一行记录,即tid为100那一行,再次判断,这就和第一次读一样了。”
MySQL老头儿得意地说:“对喽,这不就实现了可保证可重复读嘛! 旺财你想想,你在读数据的时候,需不需要加锁操作?”
旺财摇头:“不用加锁, 我只要找到正确的版本就可以了。 ”
(注: 但是在写数据的时候,MySQL还是要加锁的,防止写-写冲突)
“这就是MVCC的好处啊,读写不互相等待,能极大地提高数据库的并发能力啊。”
旺财还是有点不放心,觉得这种方式太复杂了,但是转念一想,读的时候不用加锁,这个诱惑实在太大, 他说:“这样吧,我和小强再合计合计。”
MySQL老头儿自信地说:“没问题,你们来再想想,有问题再找我吧。”
(注:本文讲解了可重复读的情况,对于Read Committed 这个可以适用同样的算法,只是每次读操作的时候,都要建立新的Read view,朋友们可自行分析下。)
- SQL
- 名词
- mysql
- 初识mysql
- 备份和恢复
- 存储引擎
- 数据表损坏和修复
- mysql工具
- 数据库操作
- 增
- 删
- 改
- 查
- 数据类型
- 整数类型
- 小数类型
- 日期时间类型
- 字符和文本型
- enum类型
- set类型
- 时间类型
- null与not null和null与空值''的区别
- 数据表操作
- 创建
- 索引
- 约束
- 表选项列表
- 表的其他语句
- 视图
- sql增删改查
- sql增
- sql删
- sql改
- sql查
- sql语句练习
- 连接查询和更新
- 常用sql语句集锦
- 函数
- 字符函数
- 数值运算符
- 比较运算符与函数
- 日期时间函数
- 信息函数
- 聚合函数
- 加密函数
- null函数
- 用户权限管理
- 用户管理
- 权限管理
- pdo
- 与pdo相关的几个类
- 连接数据库
- 使用
- pdo的错误处理
- pdo结果集对象
- pdo结果集对象常用方法
- pdo预处理
- 常用属性
- mysql编程
- 事务
- 语句块
- mysql中的变量
- 存储函数
- 存储过程
- 触发器
- mysql优化
- 存储引擎
- 字段类型
- 三范式和逆范式
- 索引
- 查询缓存
- limit分页优化
- 分区
- 介绍
- 分区算法
- list分区
- range范围
- Hash哈希
- key键值
- 分区管理
- 特别注意
- 分表
- 数据碎片与维护
- innodb表压缩
- 慢查询
- explain执行计划
- count和max,groupby优化
- 子查询优化
- mysql锁机制
- 介绍
- 演示
- 总结
- 乐观锁和悲观锁
- 扛得住的mysql
- 实例和故事
- 系统参数优化
- mysql体系结构
- mysql基准测试
- 索引
- mysql的复制
- win配置MySQL主从
- mysql5.7新特性
- 常见问题
- general log
- 忘记密码
- uodo log与redo log
- 事务隔离级别
- mysql8密码登录
- explain
- 高效的Tree表
- on delete cascade 总结
- mongod
- 简介
- 集合文档操作语句
- 增删改查
- 索引
- 数据导入和导出
- 主从复制
- php7操作mongod
- 权限管理
- redis
- redis简介
- 3.2版本配置文件
- 3.0版本配置文件
- 2.8版本配置文件
- 配置文件总结
- 外网连接
- 持久化
- RDB备份方式保存数据
- AOF备份方式保存数据
- 总结
- win安装redis和sentinel部署
- 事务
- Sentinel模式配置
- 分布式锁
- 管道
- php中redis代码
- 发布订阅
- slowlog
- Redis4.0
- scan和keys
- elasticsearch
- 配置说明
- 启动
- kibana
- kibana下载
- kibana配置文件
- kibana常用功能
- 常用术语
- Beats
- Beats简介
- Filebeat
- Packetbeat
- Logstash
- 配置
- elasticsearch架构
- es1.7
- head和bigdesk插件
- 插件大全
- 倒排索引
- 单模式下API增删改查
- mget获取多个文档
- 批量操作bulk
- 版本控制
- Mapping映射
- 基本查询
- Filter过滤
- 组合查询
- es配置文件
- es集群优化和管理
- logstash
- kibana
- es5.2
- 安装
- 冲突处理
- 数据备份
- 缺陷不足
- 集群管理api
- 分布式事务
- CAP理论
- BASE模型
- 两阶段提交(2PC)
- TCC (Try-Confirm-Cancle)
- 异步确保型
- 最大努力通知型
- 总结