一、什么是数据库的事务?
1.1 事务的定义
维基百科的定义:
事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,
由一个有限的数据库操作序列构成。
这里面有两个关键点,第一个,它是数据库最小的工作单元,是不可以再分的。
第二个,它可能包含了一个或者一系列的 DML 语句,包括 insert delete update。
(单条 DDL(create drop)和 DCL(grant revoke)也会有事务)
1.2 事务的典型场景
在项目里面,什么地方会开启事务,或者配置了事务?
无论是在方法上加注解,还是配置切面。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
1.3 哪些存储引擎支持事务?
1.4 事务的四大特性
事务的四大特性:ACID
1)原子性(Atomicity)
指我们对数据库一系列的操作,要么都是成功,要么都是失败,不可能有部分成功或部分失败的情况。
例如转账,一个账户余额减少,对应一个账户增加,这两个账户一定是同时成功或者同时失败的。
如果前面一个操作已经成功了,后面的操作失败了,怎么让它全部失败呢?这个时候我们必须要回滚。
原子性,在InnoDB中是通过undo log来实现的,它记录数据修改前的值,一旦异常,就通过undo log来实现回滚操作。
2)一致性(consistent)
用户自定义的完整性通常要在代码中控制。
3)隔离性(Isolation)
多个并发事物同时操作同一张表或同一行数据,必然会引发一些并发和干扰的问题。
隔离性就是指多个事物对表或行的并发操作,应该是透明的互不干扰的。
通过这种规则,也是为了保证数据的一致性。
4)持久性(Durable)
1.5 数据库事物的开启
1.6 事物并发会带来什么问题?
假如没有隔离性,并发事物会产生什么问题呢?
1)脏读
如图,有A,B两个事物,A事物读取一条记录name值是Jack,B事物修改这条数据,把name修改成Rose,没有提交。
此时A事物又进行了一次查询,查到这条记录中name变成了Rose。
这种在一个事物中,前后两次读到的数据不一致,读到了其他事物没有提交的数据的情况,就被称为脏读。
2)不可重复读
A,B两个事务,A事务读取到的name是Jack,B事务修改后进行了提交,A事务再次读取到的name是Rose。
由于A事务再次读取时,读到了其他事务提交后的数据,读取到了两个不一致的数据。name到底时Jack还是Rose呢?
3)幻读
在A事物中进行条件查询,满足条件的数据只有1条,在B事物中插入一条数据,并且提交。
在A事物再次查询时发现多了一条数据。
1.7 隔离级别
1)Read Uncommitted(未提交读)
一个事物可以读取到其他事物未提交的数据,会出现脏读,没有解决任何问题
2)Read Committed(已提交读)
一个事物智能读取到其他事物已提交的数据,不能读取到其他未提交的数据,
它解决了脏读的问题,但会出现不可重复读的问题。
3)Repeatable Read(可重复读)
解决了不可重复读的问题,但没有定义解决幻读的问题
4)Serializable(串行化)
所有事物都串行执行,也就是事物的操作需要排队,已经不存在事物的并发操作了,所以解决了所有的问题。
这是SQL92标准,但不同的数据库厂商或存储引擎的实现有差异。
如Oracle中就只有两种RC(已提交读)和Serializable(串行化)。
1.8 MySQL InnoDB 对隔离级别的支持
MySQL InnoDB 里面,不需要使用串行化的隔离级别去解决所有问题。
InnoDB 支持的四个隔离级别和 SQL92定义的唯一区别是
InnoDB 在 RR 的级别就解决了幻读的问题。
因此InnoDB默认的隔离级别就是Repeatable Read(可重复读),即保证了数据一致性,也支持较高的并发。
1.9 一致性的两大实现方案
LBCC
MVCC
下面是MVCC的演示:
Transaction 1
|
begin;
insert into product values(NULL,'电脑') ;
insert into product values(NULL,'空调') ;
commit;
|
id | name | 创建版本 | 删除版本 |
1 | 电脑 | 1 | undefined |
2 | 空调 | 1 | undefined |
Transaction 2
|
begin;
select * from product ;
|
Transaction 3 |
begin;
insert into product values(NULL,'电冰箱') ;
commit;
|
此时的数据,多了一条数据,它的创建版本号是当前事务编号,3:
id | name | 创建版本 | 删除版本 |
1 | 电脑 | 1 | undefined |
2 | 空调 | 1 | undefined |
3 | 电冰箱 | 3 | undefined |
Transaction 2
|
begin;
select * from product ;
|
查询结果,只查到了开始的两条数据,并没有查到事物三新创建的数据。
再来看看删除,
Transaction 4
|
begin;
delete from product where id=2;
commit;
|
此时的数据,id为2这条数据的删除版本被记录为当前事务 ID:4,其他数据不变:
id | name | 创建版本 | 删除版本 |
1 | 电脑 | 1 | undefined |
2 | 空调 | 1 | 4 |
3 | 电冰箱 | 3 | undefined |
Transaction 2
|
select * from product; (3) 第三次查询 |
查询结果,还是查到了开始的两条数据,事物四删除的数据仍能被事物二查到。
Transaction 4
|
begin;
update product set name ='洗衣机
' where id=1;
commit;
|
id | name | 创建版本 | 删除版本 |
1 | 电脑 | 1 | 5 |
2 | 空调 | 1 | 4 |
3 | 电冰箱 | 3 | undefined |
1 | 洗衣机 | 5 | undefined |
Transaction 2 |
select * from mvcctest ; (4) 第四次查询 |
查询结果,仍然可以查出更新前的"电脑",查不出更新后的“洗衣机”。这是为什么?
二、InnoDB的锁
2.1 锁的粒度
2.2 共享锁(Shared Locks)
行级别的锁有两种,第一种叫共享锁。
共享锁又称为读锁,简称S锁。
共享锁就是多个事物对同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
2.3 排它锁
第二种行锁叫排它锁。
又称为写锁,简称X锁,排它锁不能与其他锁并存。
如果一个事物获取了一个数据行的排它锁,其他事物事务就不能再获取该行的锁(包含共享锁),只有这个获取了排它锁的事物可以对数据行进行读写。
加锁释锁方式:
自动:delete/update/insert 默认加上排它锁
手动:select * form student where id=1 FOR UPDATE;
释放: commit;\rollback;
测试一下,一个会话修改数据,自动对该行施加写锁,另一个会话无法将获得共享锁和排它锁:
2.4 意向锁
三、行锁的原理
3.1 没有索引的表(假设锁住的是行)
3.2 有主键索引的表(假设锁住的是列)
3.3 有唯一索引的表(假设锁住的是字段)
还有两个问题没有解决:
四、锁的算法
4.1 记录锁(Record Locks)
第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询, 精准匹配到一条记录的时候,这个时候使用的就是记录锁。
Transcation1 | Transcation2 |
begin; select * form t2 where id=6 for update; //id=6不存在时,同时会对(4,7)这个不存在数据的区间施加间隙锁 |
|
INSERT INTO `t2` (`id`, `name`) VALUES (5, '5');
// 被阻塞
INSERT INTO `t2` (`id`, `name`) VALUES (6, '6');
// 被阻塞
select * from t2 where id =6 for update;
// OK
|
|
select * from t2 where id >20 for update;
//对大于最后一个存在的值的某值作为条件,会对(10,+∞)的区间施加间隙锁,因此并不是从20往后开始锁
|
|
INSERT INTO `t2` (`id`, `name`) VALUES (11, '11');
// 被阻塞
|
1)如例,当Transcation1中 执行查询where id=6 for update时,会对(4,7)之前的开区间施加间隙所,锁住(4,7)之间不存在数据的间隙。
当其他事物想对这个间隙(区间)内进行插入时,就会被阻塞,无法获取锁。
2)如例,对大于最后一个存在的值(10)的某值(20)作为条件,会对(10,+∞)的区间施加间隙锁,
因此并不是从20往后开始锁,因此插入id为11时也被阻塞。
临键锁解决了什么问题?
临建锁的这种设计正是解决了幻读的问题。
4.4 锁与隔离级别的实现
五、事物的隔离级别如何选择
六、死锁
6.1 锁的释放和阻塞
Q : 锁什么时候会被释放?
A : 事物结束(commit,rollback);客户端连接断开。
如果一个事物一直未释放锁,其他事物会被阻塞多久?会不会一直等待下去?
如果是,在并发访问高的情况下,打了的事物因无法立即获得需要的锁而挂起,会造成严重的性能问题,升值拖垮数据库。
MySQL中有一个参数来控制获取锁的等待时间,默认50 秒:
对于死锁,无论等多久都不能获取到锁,这种情况也会要等待50秒吗?
先看一下什么时候会发生死锁。
6.2 死锁的发生和检测
死锁演示:
Session 1
|
Session 2
|
begin;
select * from t2 where id =1 for update;
|
|
begin;
delete from t2 where id =4 ;
|
|
update t2 set name= '4d' where id =4 ;
|
|
delete from t2 where id =1 ;
|
第一个事物获取了id=1这行数据的排它锁,第二个事物获取了id=4这行数据的排它锁。
之后第一个事物想获取已经被第二个事物获取的id=4这行数据的排它锁,获取不到,保持阻塞。
第二个事物又想获取已经被第一个事物获取的id=1这行数据的排它锁,也后去不到,保持阻塞。
由于两个事物都获取不到向下执行所需要的锁,于是都无法向下执行结束事物,也就释放不了自身已经获得的锁,于是互相等待,形成死锁。
这是因为在发生死锁时,InnoDB 一般都能通过算法(wait-for graph)自动检测到。
那么产生死锁的条件是什么?
因为锁本身是互斥的,同一时刻只有一个事物持有这把锁,其他事物需要在这个事物释放锁之后才能获得锁,而不可以强行剥夺,
当多个事物形成等待环路的时候,就会发生死锁。
6.3 查看锁信息(日志)
show status命令中包含了一些行锁的信息:
innodb_row_lock_current_waits: 当前正在等待锁定的数量
innodb_row_lock_time: 从系统启动到现在锁定的总时长(ms)
innodb_row_lock_time_avg: 每次等待所化平均时间
innodb_row_lock_time_max: 从系统启动到现在等待最长的一次所花的时间
innodb_row_lock_waits: 从系统启动到现在总共等待的次数
InnoDB还提供了三张表来分析事物和锁的情况:
SELECT * FROM information_schema.INNODB_TRX;-- 当前运行的所有事物,还有具体的语句
select * from information_schema.INNODB_LOCKS; -- 当前出现的锁
select * from information_schema.INNODB_LOCK_WAITS; -- 锁等待的对应关系
6.4 死锁的避免
1.操作多张表的时候,尽量以相同的顺序来访问(避免形成等待环路);
2,批量操作单张表的时候,先对数据进行排序(避免形成等待环路);
3,申请足够级别的锁,如果要操作数据,就申请排它锁;
4,尽量使用索引访问数据,避免没有where条件的操作,避免锁表;
5,使用等值查询而不是范围查询,避免间隙锁对并发的影响;