Mysql事务

这里写自定义目录标题

  • 什么是事务
    • 事务
    • 事务的ACID特性
  • 并发带来的问题
    • 脏读
    • 不可重复读
      • 不可重复读和脏读的差别
    • 幻读
  • INNODB的几种事务隔离级别
    • 设置事务的隔离级别
  • 事务的阻塞
    • INNODB中的锁
    • 什么是阻塞?
    • 如何发现阻塞
    • 如何处理阻塞
  • 死锁
    • 什么是死锁
    • 如何发现死锁?
    • 如何处理死锁

什么是事务

一涉及到数据库的事情,你总不会感到孤单,总是有人在和你同时操作相同的数据。那么允许多人同时操作相同的数据是为了增强数据库的并发性,而并发可以带给我们更大的吞吐量,更优的资源利用率,更好的性能。

但是并发还会给我们带来一些问题,例如当多个用户同时修改数据时,很容易破坏数据的一致性,而为了解决并发带来的资源征用和数据一致性的问题,数据库就引入了事务和基于锁的这种多版本的并发控制机制。

事务

  • 是数据库执行操作的最小的逻辑单元
  • 在这个逻辑单元中,事务可以包含一个sql也可以包含多个sql组成的一组
  • 组成事务的多个sql要么全部执行成功,其中一条执行失败都失败
START TRANSACTION / BEGIN
SELECT ...
UPDATE ...
INSERT ...
COMMIT / ROLLBACK 

事务开启后,可以执行一系列的DML操作,但是不能执行DLL(像create table之类的)操作,因为DLL操作在执行前都会执行一个COMMIT操作,会将事务结束掉。

事务的ACID特性

特征 说明
原子性(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;

当第二个事务最终回滚了执行的操作,第一个事务就读取了错误的数据,这就是脏读

不可重复读

  • 一个事务前后两次读取的同一数据不一致
    当事务A读取了一条数据后,这条数据被事务B进行了修改,当事务A再次读取这条数据的时候呢,再次查询的结果出现了不一致的现象
事务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的几种事务隔离级别

在工作中,我们一般并没有遇到上面说的并发带来的几种情况,是因为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
		}
  • PERSIST
    对当前的SESSION,和此次修改后所有连接到MySQL服务器的新的连接都会有效,并且在MySQL重启后也不会丢失
  • GLOBAL
    只会对修改后连接到MySQL服务器的新的连接有效,并且当MySQL服务器重启后,我们所做的修改就会丢失
  • SESSION
    是开发人员常用的修改,只会对当前SESSION有效,当前连接断开后,所做的修改就会失效

事务的阻塞

在顺序读的隔离级别下会产生阻塞,在可重复读的隔离级别下我们对同一数据集进行修改,同样也会产生阻塞。我看下例子:

--设置隔离级别为可重复读
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实现事务隔离级别的方式说起

INNODB中的锁

为了在各个事务之间实现隔离,MySQL引入了锁的概念,锁的主要作用就是保证一个事务不能对另一个事务正在读取或修改的数据进行修改,基本的锁呢有两种类型:

  • 查询需要对资源增加共享锁(S)
    被加锁的对象只能被持有锁的事务读取但不能修改,其它事务无法对改对象进行修改,但是也可以对这个对象加共享锁进行读取
  • 数据修改时需要对资源做排它锁(x)
    加了排它锁的对象,只能被持有锁的事务读取和修改,而其它事务无法读取和修改被加了排它锁的对象。
    两种锁的关系:
排它锁 共享锁
排它锁 不兼容 不兼容
共享锁 不兼容 兼容

排它锁之间和排它锁与共享锁之间是不能加在相同的对象上的,排它锁同共享锁是不兼容的。
对于共享锁来说,由于查询并不会修改数据,所以多个查询时可以并发查询相同的数据的,也就是共享锁和共享锁是兼容的。
刚才的示例中两个事物为什么在修改相同的数据时候,后面的事物会被阻塞这也就是原因了,因为第一个事物在数据上加了排它锁,第二个事物只要在第一个事务释放排它锁后,才能获取到数据上的锁,才能对数据进行修改,也就产生了阻塞。

什么是阻塞?

阻塞是由不同锁之间的兼容关系问题,造成的一个事务需要等待另一个事务释放其所占用的资源后才能再继续执行的一种现象
举个生活中的例子,阻塞就像是在高速上并排行驶的多辆汽车,当这个车道突然变窄,所有车全挤在同一车道的时候,并且这条车道在前面的车开的还很慢的情况下,是不是后面的车就会排成很长的队伍从而造成堵车呢,这时每一辆汽车都可以看成一个事务,而车道就是这个每个事务都要占用的资源。其实数据库中的阻塞也是这样产生的,通常是前面执行的线程执行缓慢,从而阻塞了后面线程的执行。

如何发现阻塞

在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

我们为什么这么关心阻塞呢?因为阻塞往往意味着存在性能问题,如果大量的阻塞很可能会把数据库服务器的所有可用资源占满,导致服务器无法对外提供服务,这样是非常危险的情况。

如何处理阻塞

  • 终止占用资源的事务
    手动终止(KILL 线程ID),使其释放资源,这样后续的事务就可以继续执行了。治标不治本,虽然我们杀掉了占用资源的SQL,但当这个SQL再次执行的时候同样会阻塞后面的查询,所以并不是一个好的方法
  • 优化占用资源事务的SQL,使其尽快释放资源
    这是一种更好的方法,减少占用资源的时间,避免长时间阻塞发生。具体的优化SQL就是让这个SQL的性能更高,执行时间更少,阻塞更短。

死锁

什么是死锁

  • 并行执行的多个事物相互之间占有了对方所需要的资源
    举个生活中的例子,信号灯坏了后,十字路口交叉行驶的车辆各不相让,最后就会全堵在一起,谁也走不了。
    同样如果死锁中的两个事务,如果一方不让步回滚释放所占用的资源,让另一个事务先执行的话,也造成两个事务谁都无法执行的情况。不过好在,MySQL内部可以对死锁进行监控并进行处理,主动回滚两个事务中占用资源较少的那个事务,让另一个事务继续执行,这些都不需要人为的进行干预。
    既然MySQL会自动处理,为什么我们还要对死锁进行监控呢?这是因为死锁虽然不会对数据库带来太大的性能的影响,有时却会对我们的业务带来了一定的影响。比如我们会发现一个对数据库数据修改的事务有时可以执行成功有时不可以执行成功,这就有可能是死锁造成的。

如何发现死锁?

监控死锁的方法有很多,其中最常用的是把死锁记录到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的资源进行修改,相互之间就产生了死锁。

如何处理死锁

实际上我们并不需要专门对死锁进行处理:

  • MySQL可以自行发现死锁,并回滚占用资源少的事务来解死锁
    这不是一种真正解决问题的方式,只能治标不治本
  • 并发事务按相同顺序占有资源
    这是真正解决死锁的方式,可以对程序进行修改来实现。这样就可以把死锁转为阻塞,当然如果每个SQL都执行非常快的话,阻塞也会阻塞很短时间,这样就不会影响到业务。

你可能感兴趣的:(事务,MySQL)