一涉及到数据库的事情,你总不会感到孤单,总是有人在和你同时操作相同的数据。那么允许多人同时操作相同的数据是为了增强数据库的并发性,而并发可以带给我们更大的吞吐量,更优的资源利用率,更好的性能。
但是并发还会给我们带来一些问题,例如当多个用户同时修改数据时,很容易破坏数据的一致性,而为了解决并发带来的资源征用和数据一致性的问题,数据库就引入了事务和基于锁的这种多版本的并发控制机制。
START TRANSACTION / BEGIN
SELECT ...
UPDATE ...
INSERT ...
COMMIT / ROLLBACK
事务开启后,可以执行一系列的DML操作,但是不能执行DLL(像create table之类的)操作,因为DLL操作在执行前都会执行一个COMMIT操作,会将事务结束掉。
特征 | 说明 |
---|---|
原子性(A) | 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。 |
一致性(C) | 在事务开始之前和结束之后,数据库的完整性没有被破坏。 |
隔离性(I) | 事务的隔离性要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事务提交前对其它事务都不可见。 |
持久性(D) | 事务一旦提交了,其结果就是永久性的,就算发生了宕机等事故,数据库也能将数据恢复。 |
我们知道,提高数据库的并发性是为了提高数据库的数据处理效率,如果所有的事务都是按我们设想的顺序来操作数据的话,那么是不会出现问题的。可是在并发的情况下,很难保证事务处理数据的顺序,那么如果事务按照不受控制的顺序来修改数据库的数据的话,会出现下面问题:
我们看一个示例:
事务1 | 事务2 |
---|---|
start transaction; |
|
select score from course where course_id=59; /* score=9.2 */ |
|
start transaction; |
|
update course set score=9.6 where course_id=59; |
|
select score from course where course_id=59; /* score=9.6*/ |
|
rollback; |
当第二个事务最终回滚了执行的操作,第一个事务就读取了错误的数据,这就是脏读
事务1 | 事务2 |
---|---|
start transaction; |
|
select course_id ,score from course where course_id=56; /* 56,9.6 */ |
|
start transaction; |
|
update course set score=9.7 where course_id=56;commit; |
|
select course_id ,score from course where course_id=56; /* 56,9.7 */ |
|
commit; |
在不可重复读的过程中,第二个事务提交了对数据的修改,在脏读的时候第二个数据是回滚了对数据的修改。
虽然在不可重复读中事务1是前后两次数据是不一致的,但是由于事务2是最终提交了对数据的修改,所以在事务1第二次读取到数据时候,它的数据实际上是有效的,而不像脏读时,事务2回滚了对数据的修改,事务1读取的数据是无效的数据。
事务1 | 事务2 |
---|---|
start transaction; |
|
select course_id ,score from course where score> and score<9.8; /* 56,9.6| 73,9.7 */ |
|
start transaction; |
|
update course set score=9.7 where course_id=43;commit; |
|
select course_id ,score from course where score> and score<9.8; /* 56,9.6|73,9.7|43,9.7 */ |
|
commit; |
幻读读取到的数据也是有效的数据
在工作中,我们一般并没有遇到上面说的并发带来的几种情况,是因为INNODB带来的事务隔离性起到了一些作用,所谓的隔离性就是一个事务A和另一个事务B在对同一份数据进行修改时,相互影响的程度。INNODB支持4种隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 隔离性 | 并发性 | 默认 |
---|---|---|---|---|---|---|
顺序读(SERIALIZABLE) | N | N | N | 最高(5) | 最低(1) | N |
可重复读(REPEATABLE READ) | N | N | Y(可以采用下一键锁避免) | 最高(4.5) | 最低(3) | Y |
读以提交(READ COMMITTED) | N | Y | Y | 很差(1.5) | 很高(4.5) | N |
读未提交(READ UNCOMMITTED) | Y | Y | Y | 最低(1) | 最高(5) | N |
对一个事务来说,在不同的事务隔离级别下所执行的结果是有可能不同的,隔离级别越高读取数据的完整性和一致性就越好,同时并发性就越差
SET [PERSIST|GLOBAL|SESSION]
TRANSACTION ISOLATION LEVEL
{
READ UNCOMMITTED
| READ COMMITED
| REPEATABLE READ
| SERIALIZABLE
}
在顺序读的隔离级别下会产生阻塞,在可重复读的隔离级别下我们对同一数据集进行修改,同样也会产生阻塞。我看下例子:
--设置隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查看当前事务隔离界别
SHOW VARIABLE LIKE '%iso%'
事务1 | 事务2 |
---|---|
start transaction; |
|
update imc_course SET score=score+0.1 WHERE score>9.6 AND score<9,8 |
|
start transaction; |
|
update imc_course set score=score-0.1 WHERE score>9.6 AND score<9,8; /*修改的条件要跟事务1是一样的 */ |
|
事务2没有执行结果,处于执行中 ... |
|
commit; |
|
事务2在事务1commit后,也执行成功了 |
上面的示例,事务1阻塞了事务2的执行,为什么会产生阻塞呢?其实阻塞是我们在平常工作中经常会遇到的一个问题,也是对数据库性能影响非常大的一个问题。
为什么产生阻塞,还要从Innodb实现事务隔离级别的方式说起
为了在各个事务之间实现隔离,MySQL引入了锁的概念,锁的主要作用就是保证一个事务不能对另一个事务正在读取或修改的数据进行修改,基本的锁呢有两种类型:
排它锁 | 共享锁 | |
---|---|---|
排它锁 | 不兼容 | 不兼容 |
共享锁 | 不兼容 | 兼容 |
排它锁之间和排它锁与共享锁之间是不能加在相同的对象上的,排它锁同共享锁是不兼容的。
对于共享锁来说,由于查询并不会修改数据,所以多个查询时可以并发查询相同的数据的,也就是共享锁和共享锁是兼容的。
刚才的示例中两个事物为什么在修改相同的数据时候,后面的事物会被阻塞这也就是原因了,因为第一个事物在数据上加了排它锁,第二个事物只要在第一个事务释放排它锁后,才能获取到数据上的锁,才能对数据进行修改,也就产生了阻塞。
阻塞是由不同锁之间的兼容关系问题,造成的一个事务需要等待另一个事务释放其所占用的资源后才能再继续执行的一种现象
举个生活中的例子,阻塞就像是在高速上并排行驶的多辆汽车,当这个车道突然变窄,所有车全挤在同一车道的时候,并且这条车道在前面的车开的还很慢的情况下,是不是后面的车就会排成很长的队伍从而造成堵车呢,这时每一辆汽车都可以看成一个事务,而车道就是这个每个事务都要占用的资源。其实数据库中的阻塞也是这样产生的,通常是前面执行的线程执行缓慢,从而阻塞了后面线程的执行。
在MySQL8.0中可以使用下面的SQL来发现数据库中的阻塞
SELECT waiting_pid AS '被阻塞的线程'
,waiting_query AS '被阻塞的SQL'
,blocking_pid AS '阻塞线程'
,blocking_query AS '阻塞的SQL'
,wait_age AS '阻塞时间'
,sql_kill_blocking_query AS '建议操作
-- 记录了所有innodb锁等待的事件
FROM sys.innodb_lock_waits
-- 只有当阻塞时间超过30秒之后才会被查询出来
WHERE
(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(wait_started)>30
我们针对上面的示例看下:
被阻塞的线程 | 被阻塞的SQL | 阻塞线程 | 阻塞的SQL | 阻塞时间 | 建议操作 |
---|---|---|---|---|---|
27 | update imc_course set score=score-0.1 WHERE score>9.6 AND score<9,8; |
20 | (NULL) | 00:00:39 | KILL QUERY 20 |
可看到阻塞的SQL并没有值,这是因为事务1中的SQL虽然没有提交但是已经执行完了。
建议的操作是说,将阻塞的线程给kill掉
-- 查看连接的线程
SELECT CONNECTION_ID();
-- 杀死当前线程
KILL 20
我们为什么这么关心阻塞呢?因为阻塞往往意味着存在性能问题,如果大量的阻塞很可能会把数据库服务器的所有可用资源占满,导致服务器无法对外提供服务,这样是非常危险的情况。
监控死锁的方法有很多,其中最常用的是把死锁记录到MySQL的错误日志中
set global innodb_print_all_deadlocks = on;
之后,一旦发生死锁我们就可以通过查看MySQL错误日志的方式,来获得产生死锁的一些信息。
值得注意的是,日志里记录的并不是在一个事务中所执行的所有SQL,而是死锁产生的时候正在执行的那些SQL,所以,通常来说呀,为了判断为什么会产生死锁,我们还需要利用这些信息来查询程序的方式,来获得事务中所执行的所有SQL,只有这样才能名确产生死锁的真正原因在哪里。
我们看一个例子:
事务1 | 事务2 |
---|---|
start transaction; |
|
update imc_course SET score=9.7 WHERE course_id=35 |
|
start transaction; |
|
update imc_user set score=score+10 where user_id=10 |
|
update imc_user set score=score+10 where user_id=10 /*产生阻塞,因为事务2占中了该资源*/ |
|
update imc_course SET score=9.8 WHERE course_id=35 /*这个资源被事务1占有,死锁就产生了*/ |
这是为什么呢?事务1中占用了course_id=35的这行数据的资源,事务2占用了user_id=10这行数据的资源;接着事务2需要对course_id=35的资源修改,事务1需要对user_id=10的资源进行修改,相互之间就产生了死锁。
实际上我们并不需要专门对死锁进行处理: