数据库(MySQL)—— 事务
- 什么是事务
- 事务操作
- 未控制事务
- 测试异常情况
- 控制事务一
- 查看/设置事务提交方式:
- 提交事务
- 回滚事务
- 控制事务二
- 开启事务
- 提交事务
- 回滚事务
- 并发事务问题
- 脏读(Dirty Read)
- 不可重复读(Non-Repeatable Read)
- 幻读(Phantom Read)
- 丢失更新(Lost Update)
- 并发事务问题演示及事务隔离级别
我们今天来学习MySQL中的事务:
什么是事务
MySQL中的事务(Transaction)是一个逻辑上不可分割的工作单元,由一系列数据库操作组成,这些操作要么全部成功执行,要么全部不执行。事务的主要目的是为了维护数据库的一致性和完整性,尤其是在多用户同时访问和修改数据的并发环境下。
事务具备以下四个关键特性,即ACID特性:
- 原子性(Atomicity):事务是一个原子操作,事务中的所有操作要么全部完成,要么完全不起作用。如果事务中任何一部分操作失败,整个事务都会被回滚,仿佛从未发生过。
- 一致性(Consistency):事务执行前后,数据库从一种合法的状态转换到另一种合法的状态。即使在事务执行期间出现错误,数据库也应该保持一致的状态,不因事务失败而残留部分结果。
- 隔离性(Isolation):并发执行的事务之间互不干扰,一个事务内部的操作对其他事务是不可见的,直到该事务完成并提交。MySQL通过不同的事务隔离级别来实现这一特性,如读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和串行化(SERIALIZABLE)。
- 持久性(Durability):一旦事务被提交,它对数据库的更改应该是永久性的,即使系统发生故障也不会丢失。这意味着事务的结果会被稳定地存储在数据库中。
在MySQL中,特别是使用InnoDB存储引擎时,可以通过BEGIN或START TRANSACTION语句显式地开始一个事务,使用COMMIT语句提交事务,以确认所有更改,或者使用ROLLBACK语句回滚事务,取消所有在事务中做出的更改。此外,还可以通过设置事务的隔离级别来调整不同事务之间的可见性,以适应不同的应用场景需求。
举个例子:
张三给李四转账1000块钱,张三银行账户的钱减少1000,而李四银行账户的钱要增加1000。 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败。
正常情况: 转账这个操作, 需要分为以下这么三步来完成 , 三步完成之后, 张三减少1000, 而李四增加1000, 转账成功:
异常情况: 转账这个操作, 也是分为以下这么三步来完成 , 在执行第三步是报错了, 这样就导致张三减少1000块钱, 而李四的金额没变, 这样就造成了数据的不一致, 就出现问题了。
为了解决上述的问题,就需要通过数据的事务来完成,我们只需要在业务逻辑执行之前开启事务,执行完毕后提交事务。如果执行过程中报错,则回滚事务,把数据恢复到事务开始之前的状态:
注意: 默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务。
事务操作
数据准备:
drop table if exists account;
create table account(
id int primary key AUTO_INCREMENT comment 'ID',
name varchar(10) comment '姓名',
money double(10,2) comment '余额'
) comment '账户表';
insert into account(name, money) VALUES ('张三',2000), ('李四',2000);
未控制事务
测试正常情况:
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
测试异常情况
我们把数据都恢复到2000:
update account set money = 2000;
然后再次一次性执行下面的SQL语句:
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
出错了....
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
(“出错了…” 这句话不符合SQL语法,执行就会报错)
检查最终的数据情况, 发现数据在操作前后不一致了:
控制事务一
查看/设置事务提交方式:
SELECT @@autocommit ;
SET @@autocommit = 0 ;
在MySQL中,SELECT @@autocommit;
和 SET @@autocommit = 0;
是用来查看和设置当前会话的自动提交状态的命令。
SELECT @@autocommit;
这条命令用于查询当前会话的自动提交(autocommit)设置。@@autocommit
是一个系统变量,表示是否启用自动提交模式。如果返回值为1,意味着自动提交是开启的,即每次执行完一条SQL语句后,改动会立即保存到数据库中,成为永久性的变更。如果返回值为0,则表示自动提交是关闭的,你需要手动控制事务的提交与回滚。
SET @@autocommit = 0;
这条命令用于设置当前会话的自动提交模式为关闭状态。将@@autocommit
的值设为0后,意味着在该会话中执行的SQL语句不会自动提交,直到你显式执行COMMIT
命令才将事务中的更改永久保存。这样可以让你在一系列操作之间保持数据的一致性,便于实现事务的原子性、一致性、隔离性和持久性(ACID特性)。在需要手动控制事务边界的应用场景中非常有用。
提交事务
此时我们又把数据恢复到2000,又执行那三条语句:
我们查看结果发生变化了,但是这只是针对当前会话,如果我们重新开一个会话,就会发现数据没有发生变化,这里我用Navicat重开一个会话:
但是如果我执行commit,提交之后:
再在Navicat中查询结果:
所以我们设置提交模式为非自动的时候,如果我们想要我们的提交彻底生效,要记得执行commit
回滚事务
如果我在事务的流程当中出错了:
可以执行ROLLBACK操作,使数据回到开始前的状态:
控制事务二
开启事务
START TRANSACTION 或 BEGIN ; 1
提交事务
COMMIT;
回滚事务
ROLLBACK;
举个例子:
-- 开启事务
start transaction;
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
-- 如果正常执行完毕, 则提交事务
commit;
-- 如果执行过程中报错, 则回滚事务
-- rollback;
并发事务问题
并发事务问题是数据库管理系统中常见的挑战,特别是在多用户环境下,不同的事务可能同时对相同的数据进行读取或修改,从而引发以下几种典型的问题:
- 脏读(Dirty Read):事务A读取了事务B尚未提交的数据,如果事务B随后进行了回滚,那么事务A读取到的就是无效的“脏”数据。
- 不可重复读(Non-Repeatable Read):在同一个事务内,两次相同的查询返回了不同的结果,这是因为在两次查询之间,有其他事务提交了对该数据的修改或删除操作。
- 幻读(Phantom Read):在一个事务内,第一次查询时没有特定条件的某些行,但是在同一事务内的第二次查询时,由于其他事务插入了新行,这些新行就仿佛“幻影”一样出现了。
- 丢失更新(Lost Update):两个事务同时读取同一条数据,然后基于原始数据进行修改并提交,如果数据库系统没有适当的并发控制机制,后提交的事务可能会覆盖前一个事务的更改,导致前一个事务的更新丢失。
脏读(Dirty Read)
场景:事务A正在更新一条记录但尚未提交,事务B在此时读取了事务A更改但未提交的数据。
-
事务A:
BEGIN; UPDATE accounts SET salary = salary - 100 WHERE id = 1; -- 减少张三账户100元,但没提交
-
事务B (同时进行):
SELECT salary FROM accounts WHERE id = 1; -- 查询张三账户余额,看到减少了100元
-
假设:事务A因为某种原因执行了ROLLBACK。
-
结果:事务B读取到了实际上未生效的“脏”数据(减少了100元的余额),而最终张三的账户余额并未发生变化。
不可重复读(Non-Repeatable Read)
场景:在同一个事务内,两次查询返回了不同的结果,因为中间有其他事务提交了修改。
-
事务A:
BEGIN; SELECT salary FROM accounts WHERE id = 1; -- 第一次查询张三余额为500元
-
事务B (并发执行,并提交):
UPDATE accounts SET salary = salary + 100 WHERE id = 1; -- 给张三账户加100元并提交
-
事务A 继续:
SELECT salary FROM accounts WHERE id = 1; -- 第二次查询张三余额变为600元
-
结果:事务A在同一个事务内两次查询同一数据得到不同结果,违反了可重复读原则。
幻读(Phantom Read)
场景:在一个事务内,两次相同的查询因其他事务插入新行而返回不同的行数。
-
事务A:
BEGIN; SELECT * FROM orders WHERE customer_id = 1; -- 初始查询客户1的订单有5个
-
事务B (并发执行,并提交):
INSERT INTO orders(customer_id, order_date) VALUES(1, CURDATE()); -- 给客户1新增一个订单并提交
-
事务A 继续:
SELECT * FROM orders WHERE customer_id = 1; -- 再次查询客户1的订单,现在有6个
-
结果:事务A第二次查询时出现了额外的“幻影”行,即原本不存在但在查询过程中被其他事务插入的记录。
丢失更新(Lost Update)
场景:两个事务同时修改同一数据,后提交的事务覆盖了前一个事务的更改。
-
事务A:
BEGIN; SELECT salary FROM accounts WHERE id = 1; -- 张三余额为500元
-
事务B (几乎同时开始,并提交快):
BEGIN; SELECT salary FROM accounts WHERE id = 1; -- 也看到张三余额为500元 UPDATE accounts SET salary = salary + 100 WHERE id = 1; -- 增加100元并提交
-
事务A 继续, 未知事务B已更新:
UPDATE accounts SET salary = salary - 100 WHERE id = 1; -- 减少100元并提交,基于旧值操作
-
结果:尽管事务B给张三增加了100元,但事务A的减操作基于事务B之前的数据,最终张三的账户余额没有变化,事务B的更新被事务A无意中“丢失”了。
为了解决这些问题,数据库系统提供了不同的事务隔离级别,分别是:
- 读未提交(Read Uncommitted):最低的隔离级别,允许脏读、不可重复读和幻读。
- 读已提交(Read Committed):只允许不可重复读和幻读,禁止脏读。
- 可重复读(Repeatable Read):MySQL的InnoDB默认隔离级别,禁止脏读和不可重复读,但可能出现幻读。
- 串行化(Serializable):最高的隔离级别,通过完全锁定读取和更新的行,避免了脏读、不可重复读和幻读,但可能导致大量锁竞争,影响性能。
并发事务问题演示及事务隔离级别
图中打钩的表示在此模式下,该问题可能发生,打叉表示可防止此问题发生。
SELECT @@TRANSACTION_ISOLATION;
这里我们要看脏读,首先要把事务的隔离级别设置为Read uncommitted,设置事务隔离级别用下面的代码:
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED |
READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
session是设置当前对话,global是全局。