有这样的一个经典问题,用户A转账给用户B,是怎样保证数据的一致性的?这时就需要用到事务了。
1.什么是事务
1.1 定义
事务是数据库管理系统执行insert、update、delete
过程中的一个逻辑单位,由一个有限的数据库操作序列构成
1.2 事务的四大特性
- 原子性 undo log
- 隔离性 锁LCC+MVCC
- 持久性 redo log + double write buffer 双写缓冲
- 一致性
1.3 如何使用事务
- 增删改语句,默认会开启事务,查询变量语句
show variables like '%autocommit%'
- 命令
begin 、commit/rollback
事务持有的锁在事务结束时释放
2.事务隔离级别
2.1 事务并发的三大问题
1.脏读(可能会回滚)
- 一个事务读到另一个还未提交的数据
- A开启了事务查询id=10的数据,B开启了事务更新了id=10的数据但还未提交,A查询时能查到B已修改的数据
2.不可重复读(update/delete)
- 一个事务读到了已提交的数据,导致两次读取数据不一致
- A开启事务查询了id=10的数据,B开启事务更新了id=10的数据,A再次查询时能查询到已经修改的数据
3.幻读(insert)
- 一个事务读到了另一个事务插入并且已经提交的数据,造成两次读取数量不一致
- A开启事务查询数据10条,B开启事务插入了新的数据且已提交,A查询时能查询到新增的数据
2.2 事务的四种隔离级别
- Read Uncommitted(RU 未提交读) – 未解决任何问题
- Read Committed(RC已提交读) – 解决脏读问题,只能看到已提交的事务做的修改,会出现不可重复读
- Repeatable Read(RR可重复读) – 解决不可重复读问题
- 在同一个事务中多次读取的结果应该是一致的,但不解决幻读问题
- 在innoDB不可能出现幻读
- Serializable(串行化) – 解决所有问题,强制事务的串行化执行
查看数据库隔离级别sql show variables like 'tx_isolation'
2.3 事务隔离级别解决方案
如何在一个事务里面保证两次查询一致?
1.在读数据前对其加锁,阻止其他事务对该数据加锁(LBCC,基于锁并发),高并发情况下,很不友好
2.生成一个请求时间点的一致性快照数据,用这个快照来提供一致性读(MVCC)
3 MVCC
3.1 核心思想
建立一个快照,同一个事务无论查询多少次,都是相同的数据
一个事务能看到的数据版本:
- 第一次查询之前已经提交事务的修改
- 本事务的修改
一个事务不能看到的版本
- 在第一查询之后创建的事务(事务ID大于当前事务ID)
- 未提交的事务修改
3.2 MVCC模型
在InnoDB里面,会为每行记录新加三个字段
- ROW_ID 行标识,默认主键
- TRX_ID 创建版本
- ROLL_PTR 删除版本
接下来从一张图来分析MVCC是怎么工作的:
MVCC是怎么实现的呢?为什么每次查询都能查到第一次查询的内容?
依赖的是Read View一致性视图
3.3 Read View一致性视图
事务开启之后的第一次查询会建立这样的一个视图
- mid[]:列表,当前系统活跃(未提交)的事务ID
- min_trx_id 未提交的最小的事务id
- max_trx_id 系统分配给下一个事务的ID
- creator_trx_id 生成ReadView事务的事务ID
3.4 Read View判断规则
1.从数据的最早版本判断(undo log)
2.数据版本的trx_id = creator_trx_id,本事务修改,可见
3.数据版本的trx_id < min_trx_id,当前事务版本在生成ReadView已经提交,可见
4.数据版本的trx_id > max_trx_id,当前事务版本是生成ReadView新开的事务的,不可见
5.数据版本的trx_id在min和max之间,看看是否还在mid[]列表中,在的话说明是未提交的,不可见,反之可见。
6.如果当前版本不可见,就找undo log链中的下一个版本。
3.5 RC和RR Read View的区别
- Read Committed(RC已提交读) – 是事务每次查询时建立的
- Repeatable Read(RR可重复读) – 是事务第一次查询时建立的
4.Mysql InnoDB锁
在mysql中锁有表锁和行锁。
- Myisam:只支持表锁
- InnoDB:支持表锁和行锁
4.1 表锁
- 锁住整个表
lock tables xxx read;
lock tables xxx write;
unlock tables;
4.2 表锁之意向共享锁(IS)/意向排它锁(IX)
- 不需要自己实现,由存储引擎维护
- 一个事务在加行锁时会自动给表加上一个意向锁,如果再有事务来加表锁不会存在
-- 先加行锁
begin;
select * from student where id=1 FOR UPDATE;
-- 另一个窗口加表锁 不会成功
begin;
LOCK TABLES student WRITE;
4.3 行锁之共享锁(Share Locks)
- 读锁,加锁后只能读不能修改。例如在两张表的数据在事务没完成之前不希望其他事务修改
begin;
select * from student where id=1 LOCK IN SHARE MODE;
commit/rollback;
4.4 行锁只排它锁(X)
- 一个事务获取了行锁,那其他事务不能再获取该行的锁
delete/update/insert
默认加上排它锁- 其他事务获取锁等待超时时间
show variables like 'innodb_lock_wait_timeout'
, 默认50s
-- 锁住 4
begin;
select * from student where id=1 FOR UPDATE;
commit/rollback;
4.5 间隙锁(Gap Lock)
- 锁住临近记录的区间值,间隙锁只在RR中存在
-- 锁住区间 (4,7)
select * from t1 where id>4 and id<7 for update;
-- 假如一个表的索引记录只到10 锁住 (10,+无穷大)
select * from t1 where id>15 for update;
4.6 临键锁(Next-key Lock)
- 范围查询,包含记录和区间,位于两个区间的,会同时锁住两个区间(左开右闭)
-- 索引记录是 1,4,7,10 会锁住(4,7],(7,10] 那么插入这个范围内的数据是会阻塞 读加锁用这种解决幻读
select * from t1 where id>4 and id<9 for update;
5. 一个事务锁住了什么?
锁的作用:为解决资源竞争而存在
- 不使用索引的情况:表锁
- 有主键索引:行锁
- 有主键和唯一索引
- 行锁,且两个字段不能分开,因为非主键索引都是二级索引,指向都是主键索引,不管锁住哪个,都等于行锁
6.死锁的发生
- 互斥
- 不可剥夺
- 形成等待环路
查看死锁日志
//需要先开启 --transaction 里面有记录死锁信息
show engine innodb status;
7.死锁的避免
- 顺序访问
- 数据排序
- 申请足够级别的锁
- 一定要带上条件,否则就是表锁
- 大事务拆分
- 尽量少使用范围查询
以上就是本章的全部内容了。
上一篇:mysql第五话 - mysql索引原理分析
下一篇:mysql第七话 - mysql性能优化总结
一日难再晨,岁月不饶人