Spring
生态越来越好之后,使用事务仅仅需要一个注解就可以了,开发过程中对数据库的事务的感知也越来越弱了。今天我就重新去理解数据库事务,争取写完这篇文章就能记住为啥会出现这些词语。
事务就是一组原子性的 SQL 查询,或者说一个独立的工作执行单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么执行该组查询。如果其中的一条语句因为崩溃互殴其他原因无法执行,那么所有的语句都不会执行。总的来说,事务内的语句,要么全部执行成功,要么全部执行失败。
数据库事务的 ACID
是一种准则。也就是说,一个合格的事务应该具备 ACID
这几个特性。ACID
由以下几个选项组成:
我们看见上面几个特性,虽然看起来挺简单的。但是实际上,数据库对此作出了大量大量工作,确保了 ACID
这个特性。
直接这么说可能你感受不了太多,让我分别举例子。
假设你开发了一个银行的应用。这个应用的数据库有一张储蓄表。假设储蓄表存储了小明和小丁的账户余额信息。
有一天,小明想要转1000块给小丁。那么作为程序猿的你,需要在代码层面有几个操作
开启事务
数据库执行扣费 sql,小明的账号扣除1000,小明的账号扣除1000
然后给小丁的账户加1000
提交事务
假设如果当前数据库引擎事务不支持原子性,当执行到第3步的时候,数据库突然宕机了,第三步执行失败。那么就发生这种情况的后果是:小明的账户少了1000,但是小丁的账户却没有多1000,钱不翼而飞。这种涉及钱的事情,当然不能忍。所以,如果数据库引擎是支持原子性的话,即使因为宕机或者别的原因导致事务执行失败,事务会直接回滚,钱会回到小明的账户,小丁的账户也不会多钱。
数据库总是从一个一致性的状态到另外一个一致性的状态。
或许你会说,这个怎么和原子性这么像?
其实和原子性是有区别的。原子性侧重点在于能否将将整个事务完成并提交,要么一起成功要么一起失败;而一致性的侧重点在于事务执行前的执行目标结果是否能和执行后的结果一致。
隔离性,通常来说正在执行的事务对别的事务不可见的。只有事务执行完后提交结果,别的事务才可见。
或许你已经注意到了我那几个大大的红字,这说明隔离性不是绝对的看不见。这个,下文会讲清楚的。
假设有一天,小明账户余额为小明做了两件事:转1000块给小丁和充话费。那么作为程序猿的你,意识到了这两件事不是同一个 业务层实现的,你决定开启两个事务,分别为 A 事务和 B 事务。
A 事务
开启事务
数据库执行扣费 sql,小明的账号扣除1000
然后给小丁的账户加1000
提交事务
B 事务
开启事务
购买充值服务
数据库执行扣费 sql,小明的账号扣除500
提交事务
假设事务 A 和事务 B 在多并发事务下操作:
其实要求事务具有隔离性,实现起来是非常复杂的。因为在实现不同隔离级别,数据库引擎需要做更多的操作去保证这种特性例如加锁等等。
但是其实并不是所有业务都需要需要最高级的隔离级别。这个权衡,其实是留给不同的业务做准备的。
下面介绍四种隔离级别
在这个级别当中,事务的修改,即使没有提交,对其他事物是可见的。这个级别会导致一个问题,就是别的事务可能会读到错误的数据,这称为脏读。
一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事物都是不可见的。
这个隔离级别是满足了 ACID
中隔离性的定义。
这个级别有时候也叫做不可重复读,因为两次执行统一的查询,可能会得到不一样的结果。
此隔离级别解决脏读的问题。
保证了在同一个事务中多次读取同样的记录的结果都是一致的。
但是理论上,可重复读隔离级别还是无法解决另外一个幻读的问题。
所谓的幻读,指的是当某个事务在读取某个范围的记录时,另外一个事务又在该范围内插入新记录,当之前的事务再次读取该范围的
记录时,会出现幻行。InnoDB
和 XtraDB
通过引擎通过多版本并发控制,解决了幻读的问题。
可重复读是 MySQL 的默认事务隔离级别。
SERIABLIZABLE 是最高的隔离级别。 它通过强制好事物串行执行,避免了前面说的幻读的问题。
但是为了绝对的数据一致性以及没有并发的情况下,SERIABLIZABLE 会在读取每一行数据都加上锁,这样会引起大量的超时已经锁的争用。
隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 | 加锁读 |
---|---|---|---|---|
READ UNCOMMITED | yes | yes | yes | no |
READ COMMITED | no | yes | yes | no |
REPEATABLE READ | no | no | yes | no |
SERIABLIZABLE | no | no | no | yes |
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对象的请求。
事务 A
start transaction;
UPDATE student SET name = '章三' where id = 1;
UPDATE student SET name = '里斯' where id = 2;
commit;
事务B
start transaction;
UPDATE student SET name = '王五' where id = 2;
UPDATE student SET name = '老刘' where id = 1;
commit;
假如事务 A 和事务 B 都执行到了第一条 SQL,这是事务 A 更新了该行数据,并锁定了该行。
当它尝试去执行第二条数据的时候,会发现第二行数据被事务 B 占有了。所以只有等待事务 B 释放锁。
同样事务 B 也在等事务 A 释放锁。最后产生了死锁。
数据库引擎会通过非常多的途径去检测是否有死锁。
不同的数据库引擎实现的策略是不同的。
InnoDB
目前处理死锁的方法是,将持有最少行排她锁的事务进行回滚。
MySQL
直吹两种事务型的存储引擎: InnoDB
和 NDB Cluster
。
设置 AUTOCOMMIT
变量启用或禁用自动提交模式
SHOW VARIABLES LIKE 'AUTOCOMMIT';
set AUTOCOMMIT = 1;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITED;
上面说了,MVCC 是用来解决幻读问题的。所谓的 MVCC,其实就是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。
根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能不一样的。
不同的存储引擎的 MVCC 实现不同,典型的有乐观并发控制和悲观并发控制。
我们来看看 InnoDB 的 MVCC。InnoDB 主要是通过每行记录后面保存两个隐藏的列来实现的。这两个列,一个是用来存储行的创建时间,另外一个是保存行的过期时间(或删除时间)。
当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
下面是 REPEATABLE READ 隔离级别下,MVCC 具体是如何操作的。
InnoDB 会根据以下两个条件检查每行记录:
符合了以上两点则返回查询结果。
InnoDB 为每个新增行记录当前系统版本号作为创建ID
InnoDB 为每个删除行的记录当前系统版本号作为行的删除ID。
InnoDB 复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。
仅仅多了两个额外的系统版本号,使大多数的读操作不用加锁。这样设计使得读数据操作更简单,性能更好,并且也会读到符合标准的行。不足之处在于每行记录都需要额外的空间,需要做更多的检查工作,以及一些额外的维护工作。
MVCC 只在 REPEATABLE READ 和 READ COMMITTED 两个隔离下工作。其他两个隔离级别都和 MVCC 不兼容。因为 READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行,而 SERIABLIZABLE 会对所有读取的行都加锁。